多线程下的事务数据问题

前几天遇到的线上问题,防止以后犯这样的错误,特别写下来记录下!😒

现场问题:一个业务流程的接口,包含五个左右的操作步骤(a,b,c,d,e,每个步骤都是原子性),第三方调用这个接口,其中有两次请求(两次请求间隔几十毫秒),A请求没问题,B请求的d步骤没有执行!

d步骤的sql执行了,但是更新结果为0,这是为什么呢?

d步驟更新sql:

UPDATE tb_accounts_receivable SET amountReceivable=?, amountReceived=?, beLongType=?, beLongId=?, isActive=?, creator=?, createTime=?, modifier=?, modifyTime=? WHERE id=? AND modifyTime=? AND isDelete=0

d步骤更新返回结果

image-20220217214531056

    @Transactional
    public void business() {
        
        a();
        b();
        c();
        
        d();
        e();
    }

	public void b() {
        int a = get();
        ...
        set(a);
    }

我想会不会A,B线程同时竞争b()方法的锁,A线程获取到锁,然后修改了记录,之后释放了锁,之后继续往下执行,

然后B线程获取到锁,先获取值,再设置值,由于A线程还没有提交事务,mysql的默认隔离级别是可重复读,B线程获取的值并不是A线程修改后的值,而是修改之前的值,然后B线程继续修改值,因为A线程的事务没有结束,mysql中存储引擎InnoDB默认锁的级别是行锁,B线程修改值会阻塞在这,知道A线程事务提交,释放锁,B线程才会继续往下执行,此时B线程修改的值不是建立在A线程修改后的基础上。可能文字有点难理解,还是画图吧。

image-20220217232731971

A线程获取到锁,然后修改了记录,之后释放了锁,继续往下执行。

image-20220217233137108

然后B线程获取到锁,先获取值,再设置值,由于A线程还没有提交事务,mysql的默认隔离级别是可重复读,B线程获取的值并不是A线程修改后的值,而是修改之前的值,然后B线程继续修改值,因为A线程的事务没有结束,mysql中存储引擎InnoDB默认锁的级别是行锁,B线程修改值会阻塞在这。

image-20220217233739865

A线程事务提交,释放a记录行锁,B线程执行更新操作,将a更新为100。

image-20220217234002657

最后A,B线程都执行结束,a的值为110。

测试代码

    @Transactional
    public void multiTran(Integer num) {
        try {
            Thread.sleep(1000 * 2);
            updateStudentName(num);
            Thread.sleep(1000 * 2);
            Teacher teacher = new Teacher();
            teacher.setName("ss");
            teacher.setId(-1L);
            teacher.insert();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }

 	@Transactional
    @Klock(waitTime = 2,lockTimeoutStrategy = LockTimeoutStrategy.FAIL_FAST)
    public void updateStudentName(Integer num) {
        String threadName = Thread.currentThread().getName();
        Student student = new Student();
        student.setId(1L);
        Student studentOld = student.selectById(1L);
        studentOld.setNum(studentOld.getNum() + num);
        System.out.println("threadName= "+threadName+"num=" +num+ "studentOld = " + studentOld);
        boolean update = studentOld.updateById();
        log.info("update:{}",update);
    }
    
     @Test
    void updateStudentNameTest() throws InterruptedException {


        final CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 1; i < 4; i++) {
            int finalI = i;
            Thread thread = new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    
                    testLockTime.multiTran(finalI);
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }

        
        countDownLatch.await();
    }

测试结果

threadName= Thread-4num=3studentOld = Student{id=1, name='a学生', tId=1, num=3}
threadName= Thread-3num=2studentOld = Student{id=1, name='a学生', tId=1, num=2}
threadName= Thread-2num=1studentOld = Student{id=1, name='a学生', tId=1, num=1}
2022-02-17 22:57:52.628  INFO 14892 --- [       Thread-3] com.zhu.klock_test.service.TestLockTime  : update:true
2022-02-17 22:57:54.646  INFO 14892 --- [       Thread-4] com.zhu.klock_test.service.TestLockTime  : update:true
2022-02-17 22:57:56.654  INFO 14892 --- [       Thread-2] com.zhu.klock_test.service.TestLockTime  : update:true

image-20220217234436706

数据库记录以最后一次修改为准,前面两次均失效了。

咦~,但是这和我的情况还是不一样呀,我的问题是B线程的d操作压根就没执行呀,这上面分明是都执行了,只是以最后一次的为准。

再来看下现场的sql

UPDATE tb_accounts_receivable SET amountReceivable=?, amountReceived=?, beLongType=?, beLongId=?, isActive=?, creator=?, createTime=?, modifier=?, modifyTime=? WHERE id=? AND modifyTime=? AND isDelete=0

WHERE id=? AND modifyTime=? AND isDelete=0这个地方好奇怪,更新怎么会带上modifyTime的条件,而且更新操作不只是更改金额,还会修改modifyTime,这个字段感觉像是乐观锁,防止别的线程此前更新过记录。

乐观锁顾名思义就是在操作时很乐观,认为操作不会产生并发问题(不会有其他线程对数据进行修改),因此不会上锁。但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制CAS(compare and swap)算法实现

image-20220218001321003

翻看下代码,果然modifyTime是作为版本号

所以之前的分析少了一个时间字段,A线程更新记录会更新modifyTime值,之后提交事务,释放完行锁,B线程执行更新操作,而B线程还是拿着A线程更新之前的modifyTime值作为查询条件,那肯定查询不到呀!!附上流程图

image-20220218002122812

image-20220218002132051

image-20220218002146093

image-20220218002202013

测试代码修改,添加修改时间字段,再次测试!!

 @Transactional
    public void multiTran(Integer num) {
        try {
            Thread.sleep(1000 * 2);

            updateStudentNameByUpdateTimeAndId(num);
            Thread.sleep(1000 * 2);
            Teacher teacher = new Teacher();
            teacher.setName("ss");
            teacher.setId(-1L);
            teacher.insert();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


    }
    
      @Transactional
    @Klock(waitTime = 2,lockTimeoutStrategy = LockTimeoutStrategy.FAIL_FAST)
    public void updateStudentNameByUpdateTimeAndId(Integer num) {
        String threadName = Thread.currentThread().getName();
        Student student = new Student();
        student.setId(1L);
        Student studentOld = student.selectById(1L);
        System.out.println("threadName= "+threadName+"num=" +num+ "studentOld = " + studentOld);
        boolean update = studentMapper.updateByIdAndUpdate(studentOld.getId(),studentOld.getNum() + num,studentOld.getUpdateTime());
        log.info("update:{}",update);
    }
    
     @Test
    void updateStudentNameTest() throws InterruptedException {


        final CountDownLatch countDownLatch = new CountDownLatch(3);

        for (int i = 1; i < 4; i++) {
            int finalI = i;
            Thread thread = new Thread(new Runnable() {
                @SneakyThrows
                @Override
                public void run() {
                    testLockTime.multiTran(finalI);
                    countDownLatch.countDown();
                }
            });
            thread.start();
        }

        
        countDownLatch.await();
    }

测试结果

threadName= Thread-3num=2studentOld = Student{id=1, name='a学生', tId=1, num=0, updateTime=Fri Feb 18 00:01:39 CST 2022}
threadName= Thread-4num=3studentOld = Student{id=1, name='a学生', tId=1, num=0, updateTime=Fri Feb 18 00:01:39 CST 2022}
threadName= Thread-2num=1studentOld = Student{id=1, name='a学生', tId=1, num=0, updateTime=Fri Feb 18 00:01:39 CST 2022}
2022-02-18 00:23:22.120  INFO 18080 --- [       Thread-4] com.zhu.klock_test.service.TestLockTime  : update:true
2022-02-18 00:23:24.144  INFO 18080 --- [       Thread-3] com.zhu.klock_test.service.TestLockTime  : update:false
2022-02-18 00:23:26.154  INFO 18080 --- [       Thread-2] com.zhu.klock_test.service.TestLockTime  : update:false


果然第一次执行条数大于0了,后面线程更新条数为0。

结局方案:

  1. 在整个业务上加锁,同一时间只能有一个线程访问(当时没分析出来,临时先用)
  2. 去除乐观锁(需要考虑其他地方是否用到了)
  3. 写原生sql,只根据id更新金额

总结:锁不是越多越好,还是要分析好逻辑,这个代码里面有redis实现的锁,InnoDB数据引擎的行锁,版本控制的乐观锁,这么多锁,看问题看的眼花缭乱,还是适合就好。
欢迎关注ggball博客

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值