MARK-并发调优
2026-01-21
背景:活动系统本身有个注册功能,包含新增会员,初始化会员信息,发送邮件。
新需求:有个活动有名额限制,需要注册时选择参与人数。
第一版逻辑
@Transactional(rollbackFor = Exception.class)
public void memberRegister(RequestBody requestBody, String token) {
// 1. 校验CloudFlare Token
turnstileUtil.validateToken(token, null);
// 2. email转小写
requestBody.setEmail(StringUtils.lowerCase(requestBody.getEmail()));
// 3. 获取活动、商家信息
MerchantCampaign campaign =
merchantCampaignRepository.loadByPrimaryKey(requestBody.getCampaignId());
Merchant merchant = merchantRepository.loadByPrimaryKey(campaign.getMerchantId());
// 4. quota检查和扣减
// 会执行select for update,先查询quota,判断额度,进行扣减,更新db
checkQuota(requestBody, campaign, merchant);
// 5. 获取自定义配置信息
HashMap<String, Object> validAdditionalMap = getValidAdditionalMap(merchant, requestBody.getAdditionalInfo());
// 6. 初始化自定义信息
initAdditionalInfo(validAdditionalMap, requestBody);
// 7. 检查商户状态
checkMerchantEffective(campaign.getMerchantId());
// 8. 开始创建会员 (参与人数为n,则会创建n个会员)
Integer count = requestBody.getParticipants();
String batchNo = requestBody.getBatchNo();
for (int i = 1; i <= count; i++) {
String batchRemark = i + " / " + count;
CampaignMember campaignMember= new CampaignMember();
campaignMember.setEmail(requestBody.getEmail());
campaignMember.setAdditionalInfo(validAdditionalMap);
campaignMember.setBatchNo(batchNo);
campaignMember.setBatchRemark(batchRemark);
// 8.1 创建会员以及初始化会员信息(balance、expiredDate等)
buildRegisterMember(campaign, campaignMember);
// 8.2 发送邮件
sendLoginEmail(campaign, campaignMember);
}
}功能测试时并没有什么问题,甚至使用几个线程模拟用户抢名额也没有任何问题。
性能测试
当并发到达1000/s时,平均响应时间为9秒多,最大响应时间为28秒。
当并发到达1500/s时,平均响应时间13秒,最大响应时间为33秒多,并且有0.2%的请求是执行超时了。
过程中有出现过服务器hiari连接池用完的情况
解决过程
由于系统本身就没有设计成高并发的,所以一开始没有想到在代码层面进行修改,毕竟代码层面的业务执行是没问题的。
针对连接池用完的情况,当时是使用hiari默认参数,maximum-pool-size=10,后面改成maximum-pool-size=100,minimum-idle=20。就没有出现过同样的情况了,但是感觉最大连接数改到100也有点大,不过至少解决了一个问题。
增加服务器,但是问题依旧。
检查代码,并修改。
分析代码
可以很容易看出整个方法放在一个事务当中,然后quota加锁又是在方法中间的位置。也就是说拿到锁后还要处理一堆非quota相关的事情,这个过程中其他事务要等待,就导致了超时。
代码优化
大事务改成小事务。把校验、JSON解析、普通查询、发送邮件、初始化信息、检查状态等操作都放在事务外面。
改变执行顺序,目前事务只包含创建会员(insert)和quota扣减(update)。然后这两个顺序可以更换,也就是说先insert member,然后再update quota,这样等update quota拿到锁时只用做更新一个操作,完事就提交事务,把锁释放了。
最终优化
做出以上的修改后还是感觉有点问题,然后问了一下AI,AI表示可以把checkQuota方法再优化,目前是先查询quota(select for update),判断额度,进行扣减,更新db。然后可以直接写sql来判断和更新,并且通过更新结果(更新条数)来判断是否超出quota。这样并发都交给db来处理了。
最后结果
当并发到达5000/s时,平均响应时间为100ms,最大响应时间为1秒。95%请求在160ms内响应。99%请求在479ms内响应。