gball个人知识库
首页
基础组件
基础知识
算法&设计模式
  • 操作手册
  • 数据库
  • 极客时间
  • 每日随笔
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 画图工具 (opens new window)
关于
  • 网盘 (opens new window)
  • 分类
  • 标签
  • 归档
项目
GitHub (opens new window)

ggball

后端界的小学生
首页
基础组件
基础知识
算法&设计模式
  • 操作手册
  • 数据库
  • 极客时间
  • 每日随笔
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • 友情链接
  • 画图工具 (opens new window)
关于
  • 网盘 (opens new window)
  • 分类
  • 标签
  • 归档
项目
GitHub (opens new window)
  • 面试

  • 数据库

  • linux

  • node

  • tensorFlow

  • 基础组件

  • 基础知识

  • 算法与设计模式

  • 分布式

  • 疑难杂症

    • 多线程下的事务数据问题
    • 幂等性解决方案
  • go学习之旅

  • 极客时间

  • 知识库
  • 疑难杂症
ggball
2022-02-18

多线程下的事务数据问题

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

现场问题:一个业务流程的接口,包含五个左右的操作步骤(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
1

d步骤更新返回结果

image-20220217214531056

    @Transactional
    public void business() {
        
        a();
        b();
        c();
        // b方法加了锁
        d();
        e();
    }

	public void b() {
        int a = get();
        ...
        set(a);
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

我想会不会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减为0,才会往下执行,否则一直阻塞
        countDownLatch.await();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

测试结果

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
1
2
3
4
5
6

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
1

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);
//            updateStudentName(num);
            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减为0,才会往下执行,否则一直阻塞
        countDownLatch.await();
    }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52

测试结果

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

1
2
3
4
5
6
7

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

结局方案:

  1. 在整个业务上加锁,同一时间只能有一个线程访问(当时没分析出来,临时先用)
  2. 写原生sql,只根据id更新金额

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

上次更新: 2025/06/04, 15:06:15
分布式事务概览
幂等性解决方案

← 分布式事务概览 幂等性解决方案→

最近更新
01
AIIDE
03-07
02
githubActionCICD实战
03-07
03
windows安装Deep-Live-Cam教程
08-11
更多文章>
Theme by Vdoing
总访问量 次 | 总访客数 人
| Copyright © 2021-2025 ggball | 赣ICP备2021008769号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式
×

评论

  • 评论 ssss
  • 回复
  • 评论 ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
  • 回复
  • 评论 ssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssssss
  • 回复
×