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连接池用完的情况

解决过程

由于系统本身就没有设计成高并发的,所以一开始没有想到在代码层面进行修改,毕竟代码层面的业务执行是没问题的。

  1. 针对连接池用完的情况,当时是使用hiari默认参数,maximum-pool-size=10,后面改成maximum-pool-size=100,minimum-idle=20。就没有出现过同样的情况了,但是感觉最大连接数改到100也有点大,不过至少解决了一个问题。

  2. 增加服务器,但是问题依旧。

  3. 检查代码,并修改。

分析代码

可以很容易看出整个方法放在一个事务当中,然后quota加锁又是在方法中间的位置。也就是说拿到锁后还要处理一堆非quota相关的事情,这个过程中其他事务要等待,就导致了超时。

代码优化

  1. 大事务改成小事务。把校验、JSON解析、普通查询、发送邮件、初始化信息、检查状态等操作都放在事务外面。

  2. 改变执行顺序,目前事务只包含创建会员(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内响应。