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

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

作者: 名字是乱打的 | 来源:发表于2022-07-11 23:17 被阅读0次

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

一. 业务简单伪代码贴一下:

   /**
     * 机会增加接口
        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 = extChanceAddRecordService.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多组并发出现了问题;

相关文章

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

    背景:我们这里有个限购活动可以对某些商品进行机会限购,用户可以通过积极参与平台游戏或者购物等获取购买机会。今天突然...

  • 第三弹:MySQL事务和锁

    第三弹:MySQL事务和锁 事务特点:ACID 未提交读:事务A可以读取到事务B已修改但未提交的数据读已提交RC:...

  • mysql事务隔离级别

    未提交读 A事务已执行,但未提交;B事务查询到A事务的更新后数据;A事务回滚;---出现脏数据 已提交读 A事务执...

  • 分布式锁及分布式事务

    分布式锁是解决并发时资源争抢的问题,分布式事务和本地事务是解决流程化提交问题。一、其中分布式锁实现:1.基于数据库...

  • 关于Mysql(5.6)的一些思考

    事物级别 MySQL 默认事物级别是可重复读;Oracle默认为读已提交 事务级别的优先级:1:读未提交 2:读已...

  • MySQL InnoDB事务隔离级别

    一、读未提交(Read Uncommitted)这种事务隔离级别下,select语句不加锁。 此时,可能读取别的事...

  • Mysql 可重复读

    事务隔离级别 RR 可重复读 在RC 已提交读 级别下,同一事务中: 当我们使用读语句时: 确定扫描行 多次读取都...

  • 四种事务隔离级别与三种异常

    四种事务隔离级别: 未提交读:一个事务可以读任何已提交或未提交的数据。这可以通过“读操作不需要请求任何锁”来实现。...

  • 分布式事务

    目录 简介 单一分布式事务与嵌套分布式事务 原子提交协议两阶段提交协议嵌套事务的两阶段提交协议 分布式事务的并发控...

  • 分布式事务理论研究

    1 传统的分布式事务 基于数据库支持的xa两阶段提交事务 缺点 : 1性能差,再xa 两阶段提交锁一直占有,...

网友评论

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

      本文链接:https://www.haomeiwen.com/subject/qkhnbrtx.html