V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
linuxsteam
V2EX  ›  数据库

mysql 第一个事务 创建数据,第二个事务修改同一条数据 Lock wait timeout exceeded 怎么解决?

  •  
  •   linuxsteam · 2023-11-29 09:14:04 +08:00 · 2237 次点击
    这是一个创建于 367 天前的主题,其中的信息可能已经有所发展或是发生改变。

    伪代码:

    Class A {
            @Transactional(rollbackFor = Exception.class)
            public String transactionA() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
    
                    // 省略可能报错的代码
                    TableA a = new TableA();
                    a.setId(1);
    
                    tableAMapper.insert(a);
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    
    
        Class B {
            @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
            public void transactionB() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
                    // 省略可能报错的代码
                    TableA a = new TableA();
                    a.setId(1);
                    a.status(2);
    
                    tableAMapper.update(a);
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    

    TableA 表的 id 有主键约束,唯一索引。其他没有任何索引。

    上面代码会产生 Lock wait timeout exceeded 的错误。 我通过检索资料,大概了解到问题原因,是事务 A 中tableAMapper.insert(a);触发了(行、排他锁),事务 B 里一直拿不到锁,导致超时。

    但是我还想事务 B 拥有自己的事务,并且进行手动回滚。(因为我想在哪怕报错的时候 也要进行一段日志记录的数据库插入操作)

    不知道有没有解决办法。或者其他思路?

    第 1 条附言  ·  2023-11-29 10:05:27 +08:00
    Class A {
            @Transactional(rollbackFor = Exception.class)
            public String transactionA() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
    
                    // 省略可能报错的代码
                    TableA a = new TableA();
                    a.setId(1);
    
                    tableAMapper.insert(a);
    
                    //调用B
                    transactionB();
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    
    
        Class B {
            @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
            public void transactionB() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
                    // 省略可能报错的代码
                    TableA a = new TableA();
                    a.setId(1);
                    a.status(2);
    
                    tableAMapper.update(a);
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    
    第 2 条附言  ·  2023-11-29 15:13:52 +08:00

    根据各位老哥回复的启发

    更改了业务代码,把transactionB的操作,根据返回状态,拿到了transactionA中执行了。就是代码难看了点,功能实现了。就不知道有没有更好的办法

    Class A {
            @Transactional(rollbackFor = Exception.class)
            public String transactionA() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
    
                    // 省略可能报错的代码
                    TableA a = new TableA();
                    a.setId(1);
    
                    tableAMapper.insert(a);
    
                    //调用B
                    transactionB();
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    
    
        Class B {
            @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
            public boolean transactionB() {
                Object savepoint = TransactionAspectSupport.currentTransactionStatus().createSavepoint();
                try {
                    // 省略可能报错的代码
                    return true;
                }catch(Exception e) {
                    e.printStackTrace();
                    TransactionAspectSupport.currentTransactionStatus().rollbackToSavepoint(savepoint);
                    return false;
                }finally {
                    LogMapper.insert(new Log("time","method","request","response"));
                }
            }
        }
    
    33 条回复    2024-03-20 19:03:03 +08:00
    cnhongwei
        1
    cnhongwei  
       2023-11-29 09:31:52 +08:00
    那你不要在 B 中开新事务,而是记录 Log 的类中开新事务。
    missya
        2
    missya  
       2023-11-29 09:40:39 +08:00
    A 和 B 方法是怎么调用的,A 中调用 B ?还是同时分别调用?
    SvenWong
        3
    SvenWong  
       2023-11-29 09:43:08 +08:00
    看错误就是锁等待超时了,但是具体解决还是要看场景,比如你的 B 事务中对 TableA 的更新是否在单独线程,以及先后顺序,A 事务 insert TableA 之后,是否还有大量的代码操作,导致 A 事务迟迟无法提交等等
    linuxsteam
        4
    linuxsteam  
    OP
       2023-11-29 10:05:53 +08:00
    @missya A 调用 B ,对不起 没贴上,我附言了
    linuxsteam
        5
    linuxsteam  
    OP
       2023-11-29 10:07:04 +08:00
    @SvenWong
    A 事务 insert TableA 之后,是否还有大量的代码操作:
    没有了 就是调用事务 B ,因为事务 B 获取不到锁 就挂了
    dengkj
        6
    dengkj  
       2023-11-29 10:08:08 +08:00
    尽量缩短事务 A 的执行时间,相关性不强的业务可以异步执行。
    linuxsteam
        7
    linuxsteam  
    OP
       2023-11-29 10:08:44 +08:00
    @cnhongwei 插入日志也不会报错。
    在抽出来插入日志的代码上新开事务 我理解没有实际作用呀
    bcllemon
        8
    bcllemon  
       2023-11-29 10:22:06 +08:00
    A 和 B 是要一起完成, 就放到一个事物里。
    A 完成后,执行 B ,就先提交 A 事物,再执行 B 。
    SvenWong
        9
    SvenWong  
       2023-11-29 10:23:00 +08:00
    @linuxsteam #5 哦,我漏看了一句代码,我的理解是:你的 A 方法里同步调用了 B 方法,但是 B 方法起了一个新事务,它执行 update 的时候,需要等待 insert 的那个锁,但是 insert 的锁现在还在 A 事务里没提交,但是 A 要等待 B 方法执行结束返回了,事务才能提交。

    尝试把 B 方法放到异步操作去,或者把调用 B 方法,放到调用 A 的上层去
    missya
        10
    missya  
       2023-11-29 10:24:04 +08:00
    @linuxsteam 是不是执行 transactionA()后事务还未提交,其实当时 Id=1 的数据并没有实际保存到数据库中,然后紧接着 transactionB()又执行,而又是独立的事务,所以获取不到 Id=1 的数据(其实是事务 A 还在 lock 中)造成无法更新超时,可是尝试下异步调用 transactionB()看看结果如何
    kivmi
        11
    kivmi  
       2023-11-29 10:38:33 +08:00
    事务 A 改成手动提交,应该可以吧

    TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
    try {
    // 提交事务
    transactionManager.commit(status);
    } catch (Exception ex) {
    // 发生异常时回滚事务
    transactionManager.rollback(status);
    }
    weiqlog
        12
    weiqlog  
       2023-11-29 10:40:27 +08:00
    设置数据库隔离级别为读未提交 Read Uncommitted 呢?
    vczyh
        13
    vczyh  
       2023-11-29 10:50:14 +08:00
    我觉的这是业务问题,既然 A 和 B 事务是分开的,那就可以说明他们不是强一致的,那就采用最终一致,即 A 提交后,再执行 B , 不然就把 AB 放到同一个事务,事务和业务是有关联的。
    linuxsteam
        14
    linuxsteam  
    OP
       2023-11-29 11:17:40 +08:00
    @dengkj A 不慢 B 慢(调用第三方接口)
    linuxsteam
        15
    linuxsteam  
    OP
       2023-11-29 11:20:17 +08:00
    @bcllemon 需要一起完成,并且同步的。
    现在也改成一个事务了,但是就不能在 B 抛出异常交给 A 处理了(这样 B 的数据库操作就全部回滚了)
    B 只能改成 手动回滚到某个点,然后 return 。感觉这么写不太好
    linuxsteam
        16
    linuxsteam  
    OP
       2023-11-29 11:22:15 +08:00
    @SvenWong B 之前就是异步的
    但是业务想要同步,所以才这么写代码的。

    调用 B 倒是可以,效果一样。就是可读性怪怪的。不过是 OK 的
    linuxsteam
        17
    linuxsteam  
    OP
       2023-11-29 11:24:10 +08:00
    @weiqlog 可行的,不过不敢调。怕影响太大了,对事物理解不是特别透彻。 来点其他问题 就得不偿失了
    linuxsteam
        18
    linuxsteam  
    OP
       2023-11-29 11:26:18 +08:00
    @bcllemon 说错了,我是 B 去掉事务了。A 和 B 加入一个事务也会 锁超时
    linuxsteam
        19
    linuxsteam  
    OP
       2023-11-29 11:27:48 +08:00
    @missya 就是这样的。
    之前就是异步调用 transactionB 。现在业务改了,想同步返回结果。所以才出现这个问题的
    linuxsteam
        20
    linuxsteam  
    OP
       2023-11-29 11:30:48 +08:00
    @SvenWong B 方法没法放到上面,因为上面新建的数据,然后 B 才能去更新 😂
    SvenWong
        21
    SvenWong  
       2023-11-29 11:39:09 +08:00
    @linuxsteam #16 那既然要同步,就没必要 Propagation.REQUIRES_NEW 了吧,在一个事务里面做就好了,一起成功一起失败
    kivmi
        22
    kivmi  
       2023-11-29 11:43:21 +08:00
    @linuxsteam 既然是需要第三方的接口,为啥不先拿到数据,然后执行插入更新事务呢?
    vishun
        23
    vishun  
       2023-11-29 11:49:20 +08:00
    @linuxsteam #18 同一个事务内也会超时?不可能吧。
    zhuzhibin
        24
    zhuzhibin  
       2023-11-29 11:54:36 +08:00 via iPhone
    没人问隔离级别么? RR 还是 RC ?以及不太明白业务上为啥插入 id =1 的,立马又事物更新这行记录的其他字段,所以是期望同个事务内,或者能否描述下业务诉求
    kivmi
        25
    kivmi  
       2023-11-29 11:58:24 +08:00
    @zhuzhibin 感觉它这个为啥使用事务,推测是它是先插入,然后推送数据到第三方,之后第三方修改状态,然后回传字段,感觉就没必要使用事务
    linuxsteam
        26
    linuxsteam  
    OP
       2023-11-29 14:23:25 +08:00
    @zhuzhibin 当然是 Mysql 默认隔离级别,允许重复度。
    业务是新插入这条数据。
    然后再拿这条数据去请求第三方系统。(最早时候是异步请求第三方系统的,但是后来要求同步了)

    然后请求第三方系统是单独一个代码 transtranB 。里面只有更新对应数据状态,记录日志的数据库操作。
    linuxsteam
        27
    linuxsteam  
    OP
       2023-11-29 14:24:26 +08:00
    @kivmi 因为要通过事务保证异常数据不要落库,否则脏数据比较难受。而且业务所处部分是 数据处理。一切日志都要记录
    linuxsteam
        28
    linuxsteam  
    OP
       2023-11-29 14:35:28 +08:00
    @vishun 啊,没有超时,我上午回答错了。是 B 的操作都被回滚了(日志表就没有记录)。我以为超时报错了。
    vishun
        29
    vishun  
       2023-11-29 15:26:15 +08:00
    别费这些劲了,要么就都放到同一个事务中,要么就 B 单独一个事务,当 B 单独一个事务时,必须保证 A 事务提交后再执行 B 相关代码,如果用 spring ,那么可以这样:
    ```
    //A 相关代码
    //A 结束后
    TransactionSynchronizationManager.registerSynchronization(
    new TransactionSynchronization() {
    @Override
    public void afterCommit() {
    //这里执行 B
    }
    }
    );
    ```
    linuxsteam
        30
    linuxsteam  
    OP
       2023-11-29 16:02:21 +08:00
    @vishun 谢谢,我去搜素下 这个是什么意思
    lancelee01
        31
    lancelee01  
       2023-11-29 16:46:05 +08:00
    看了一下上下文,大概意思是先落单,然后请求 RPC 接口,更新状态。这个在实际业务开发中,是不加事务的,都是补偿(定时任务搂落单的数据,重复一遍后面的流程)。规范一般要求事务的粒度必须是最细的,不能包含业务逻辑,尤其是 RPC 调用可能会超时,尤其金融相关,事务中基本只能有 2-3 行,数据组装好直接操作事务。
    linuxsteam
        32
    linuxsteam  
    OP
       2023-11-30 09:21:15 +08:00
    @lancelee01 之前只有落单入库加事务,

    后来调用方要求同步返回 RPC 接口的实时状态,就只能这样了。
    补偿倒是没事,主要怕产生无效数据
    dyv9
        33
    dyv9  
       255 天前 via Android
    @linuxsteam 日志要用 RequiredsNew 事务设置 呀。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1015 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 24ms · UTC 19:47 · PVG 03:47 · LAX 11:47 · JFK 14:47
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.