前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >读已提交级别下 注解事务+分布式锁结合引起的事故--活动购买机会的错乱

读已提交级别下 注解事务+分布式锁结合引起的事故--活动购买机会的错乱

作者头像
名字是乱打的
发布2022-07-17 12:22:23
4030
发布2022-07-17 12:22:23
举报
文章被收录于专栏:软件工程软件工程

背景: 我们这里有个限购活动可以对某些商品进行机会限购,用户可以通过积极参与平台游戏或者购物等获取购买机会。今天突然收到系统告警,有大量异常错误码。 事故现象: 看了下记录是给17万用户每人加了两次购买机会,而且每个人加机会是不是一次加够,而是业务测采用每调一次接口加一次机会的形式...业务层分了8万组数据,每组一个用户,每组并发调两次机会增加接口,事故造成17万用户里的350余名用户无法正常下单,受损用户比较少,还没上报完问题就有告警中心邮件发来了,在客诉之前解决问题; 事故大概原因: 排查了一下,发现这是一场由Mysql Read COMMIT级别+注解事务+分布式锁,当系统收到极端高并发情况(μs级)下引起的事故。 三个结合在一起产生的特殊bug。 下面由我细细道来

一. 业务简单伪代码贴一下:
代码语言:javascript
复制
   /**
     * 机会增加接口
        XXXXXXX等符号是我手动打码行为
    */
    @Transactional(rollbackFor = Exception.class)   //注意,就是这里有问题
    @PostMapping("chanceAdd")
    public XxxDto chanceAdd(@RequestBody xxxReq req) {
        // 快速去重\快速失败机制(借鉴AQS的addWaiter)----除此之外后面还有数据库唯一键做保底持久去重
        if (!redisUtils.setExNx(REPEAT_CHECK_PRE +XXX orderNo XXXXX)) {// 业务订单号判重,同一笔交易只能增加一次机会
            throw new CommonException(ApplicationCode.REPEAT_SUBMIT,"重复添加机会");
        }

        //按人+商家+活动分配一把锁
        RLock lock = redissonClient.getLock(REPEAT_CHECK_PRE +XXX人,商家id,活动idXXXXX);
        lock.lock();
        try {
            //活动添加记录增加
            final boolean saveRes = quotaExtChanceAddRecordService.save(ExtChanceAddRecordMapping.INSTANCE.toQuotaAddRecordPojo(req));
            if (saveRes) {
                //该人总机会增加
                //查询是否已经存在用户总机会记录
                UserExtChance userExtChance = service.getUserExtChance(req.getUserId(), req.getMallId(), req.getActivityId());
                if (userExtChance==null){//如果用户购买记录不存在
                //生成用户对该活动的总机会记录
                }else {//已存在
                //对已有机会记录做增加
                }
            }
      
        } catch (Exception e) {
            log.error("chanceAdd,data:{},errorMsg:{}",req.toString(),e.getMessage());
            throw new CommonException(ApplicationCode.REPEAT_SUBMIT);
        } finally {
            lock.unlock();
        }

        return new XxxDto();
    }

二.错误原因分析

我们按照代码线分析,模拟异常情况

  • 事务开启没有问题
  • 这里的红锁也可以保障分布式情况下对单人单商家单活动添加机会的串行化
  • 但是假如有两个线程A,B并发去调这个接口,可能出现A释放锁未提交事务,B获取锁由于A未提交的事务,获取的是A提交之前的快照,因此做出了错误判断
  • 至此 A,B均对于同一用户生成了两条总机会记录。或者出现了数据覆盖的问题(其他可能情况)。

错误流程模拟,分析

三.总结

本次错误原因是虽然我们用红锁保障了特定机会((用户,商家,活动)维度)增加的串行化,但是我们这里事务是用的注解事务导致事务在方法结束之后才提交,因此Read COMMIT级别下,并发情况可能读到了未变更的数据,导致做出错误判断

四.解决

改成声明式事务,在业务结束后提交事务或者异常回滚事务,重点要在串行化结束之前(这里是获取到红锁之前)完成整个事务的操作;

多亏系统各种告警配置....在用户还没发现之前就把问题暴露出来了,一天内完成了问题暴露,找到原因,测试复现,开发解决,发布测试,上线,刷数据,复测验证整个流程;


建议只有极简单的事务用注解事务,复杂业务还是手动比较好。 另外注意只要我方主动加锁的一般都是咱们知道这里肯定有潜在并发问题,在测试人员测试时候必须让测试人员多测几十组,确保咱们的防并发没问题; 我们这个业务之前也让测试人员测试了,用了30组 30qps的并发,但是由于这里确实比较偶发,所以没出现问题...这次是1W多组并发出现了问题;

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-07-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一. 业务简单伪代码贴一下:
  • 二.错误原因分析
  • 三.总结
  • 四.解决
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档