【并发】ConcurrencyFailureException:Deadlock found when trying to get lock; try restarting transaction

消失的任务单问题

问题描述:

生产环境,客户冻结流程(MDM创建任务单–>提交OA审批),发现一个用户创建的任务单提交OA后,任务单数据在系统不见了!!!

这也导致后面再次走冻结流程时,这个任务单号又重新使用了一遍,对应了一个新的冻结流程

通过查日志,找到了对应任务单号(CTDJ202307150002),找到异常信息:ConcurrencyFailureException 并发失败异常,导致事务被回滚

但是:

代码设计,保存时出现异常,捕获异常后抛出异常并回滚事物,提交时出现异常,捕获异常但不抛出异常不回滚事物
在代码里面也测试了,提交时模拟抛出同样异常并且不抛出情况下,保存时的操作不会被回滚。

代码没有问题,但是实际上,这个流程提交OA时异常时,事物回滚了!!!为什么呢?因为ConcurrencyFailureException这个异常比较特殊


java代码中模拟发生了一个ConcurrencyFailureException异常,并捕获了这个异常,但是没有抛出这个异常,代码层面没有回滚,但是生产环境数据库层面回滚了,这是什么原因?

chatGPT回答:

这可能是因为你的代码在Spring的事务管理上下文中运行。在Spring中,如果你的方法被@Transactional注解标记,那么在该方法中发生的任何运行时异常都会导致事务回滚。

ConcurrencyFailureException是Spring框架中的一个运行时异常,它通常表示并发问题,如乐观锁失败。即使你的代码捕获了这个异常并没有重新抛出,Spring事务管理仍然会检测到这个异常并回滚事务。

如果你希望在捕获ConcurrencyFailureException异常后不回滚事务,你可以在@Transactional注解中指定noRollbackFor属性


Spring事务管理器会检测到ConcurrencyFailureException异常并回滚事务的原因是因为该异常被认为是一个致命的异常,可能会导致数据的不一致性。

即使在代码中捕获了ConcurrencyFailureException异常并没有重新抛出,Spring事务管理器会在异常被捕获后检测到该异常,并根据事务的配置进行回滚操作。这是因为Spring事务管理器会在方法执行过程中监视异常的发生,并根据异常的类型来决定是否回滚事务。

ConcurrencyFailureException异常通常表示在并发操作中发生了并发冲突,可能是由于多个线程同时修改相同的数据而导致的。为了确保数据的一致性,Spring事务管理器会将这种异常视为需要回滚事务的情况,以避免数据的不一致性。

因此,即使在代码中捕获了ConcurrencyFailureException异常并没有重新抛出,Spring事务管理器仍然会检测到该异常并回滚事务,以确保数据的完整性和一致性。 

注意

ConcurrencyFailureException是Spring框架中的一个运行时异常,它通常表示并发问题,如乐观锁失败。即使你的代码捕获了这个异常并没有重新抛出,Spring事务管理仍然会检测到这个异常并回滚事务。

如果你希望在捕获ConcurrencyFailureException异常后不回滚事务,在@Transactional注解中指定noRollbackFor属性,如下所示:

@Transactional(noRollbackFor = ConcurrencyFailureException.class)
public void yourTransactionalMethod() {
    // your code
}

报错原因:

ConcurrencyFailureException

是因为 11:22:43 时,两个线程同时对一个表 tn_work_task 数据进行了更新操作;而表没有建索引,导致锁表;
(MySQL InnoDB 引擎下,delete/update操作where后面的条件如果没有走索引,会锁表(MySQL 5.6/7 版本验证))

日志报错提示:

11:22:43.833 [http-nio-8080-exec-91] INFO  c.t.m.c.s.i.TnMdCustWorkTaskServiceImpl - [info,26] - [TYPE NORMAL-][className com.tn.mdm.cust.service.impl.TnMdCustWorkTaskServiceImpl-][method submitCTDJCustWorkInfo-][tag MDM-][error 经销商-客户冻结-提交OA时异常org.springframework.dao.ConcurrencyFailureException: 
### Error updating database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
### The error may exist in URL [jar:file:/tn-mdm/backend/tnmdm-admin.jar!/BOOT-INF/lib/tnmdm-mdm-3.8.3.jar!/mapper/task/TnWorkTaskMapper.xml]
### The error may involve com.tn.mdm.mdm.mapper.TnWorkTaskMapper.updateTnWorkTask-Inline
### The error occurred while setting parameters
### SQL: update tn_work_task          SET task_type = ?,             status = ?,             exam_status = ?,             applicant_id = ?,             apply_count = ?,             apply_time = ?,                          current_wf_instance = ?,             create_by = ?,             create_time = ?,             update_by = ?,             update_time = ?,             remark = ?,             del_flag = ?,             attachments = ?          where task_id = ?
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction
; Deadlock found when trying to get lock; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Deadlock found when trying to get lock; try restarting transaction-]

解决办法:

给任务单号加上唯一索引

image-20230720150537658

业务代码:

    @Override
    @Transactional(rollbackFor = Exception.class)
    public ServiceResponse submitCTDJCustWorkInfo(CustWorkTaskInfoPO custWorkTaskInfoPO) {
        ServiceResponse serviceResponse;
        try {
            serviceResponse = saveCTDJCustWorkInfo(custWorkTaskInfoPO);
        } catch (Exception e) {
            e.printStackTrace();
            Log.info(LogBuilder.builder().tag("MDM").error("经销商-客户冻结-提交时保存异常"+ e).build());
           // log.warn("经销商-客户冻结-提交时保存异常", e);
            throw new RuntimeException(e);
        }
        if (serviceResponse.ok()) {
            try {
                // 提交oa
                CustWorkTempPo custWorkTempPo = new CustWorkTempPo() {{
                    setTnWorkTask(custWorkTaskInfoPO.getTnWorkTask());
                    if (!CollectionUtils.isEmpty(custWorkTaskInfoPO.getTnMdCustTempBases())) {
                        setTnMdCustTempBase(custWorkTaskInfoPO.getTnMdCustTempBases().get(0));
                    } else {
                        setTnMdCustTempBase(new TnMdCustTempBase());
                    }
                    setTnMdCustTempBusiScope(custWorkTaskInfoPO.getTnMdCustTempBusiScopes());
                    if (!CollectionUtils.isEmpty(custWorkTaskInfoPO.getTnMdCustTempBases())) {
                        setTnMdCustTempLink(custWorkTaskInfoPO.getTnMdCustTempLinks());
                    } else {
                        setTnMdCustTempLink(new ArrayList<>());
                    }
                }};
                custWorkTempPo.getTnMdCustTempBase().setAcctCode(custWorkTempPo.getTnMdCustTempBusiScope().get(0).getAcctCode());
                String acctName = tnMdCustBaseMapper.selectTnMdCustBaseList(new TnMdCustBase() {{
                    setAcctCode(custWorkTempPo.getTnMdCustTempBusiScope().get(0).getAcctCode());
                }}).get(0).getAcctName();
                custWorkTempPo.getTnMdCustTempBase().setAcctName(acctName);
                ServiceResponse oaFlowResponse = oaFlowService.createCustomerFreezeThawOAFlowPath(custWorkTempPo);
                if (oaFlowResponse.ok()) {
                    TnWorkTask tnWorkTask = tnWorkTaskMapper.selectTnWorkTaskList(new TnWorkTask() {{
                        setTaskId(custWorkTempPo.getTnWorkTask().getTaskId());
                    }}).get(0);
                    // 提交OA成功, 更新审批状态为审批中
                    tnWorkTask.setStatus(WorkTaskStatus.审核中.value());
                    tnWorkTask.setExamStatus(CustWorkTaskExamStatus.examing.getCode());
                    tnWorkTask.setCurrentWfInstance(oaFlowResponse.getData().toString());
                    tnWorkTask.setUpdateBy("OA");
                    tnWorkTask.setUpdateTime(DateUtils.getNowDate());
                    int a =1;
                    //if(a == 1){
                    //    throw new NullPointerException("e");
                    //}
                    tnWorkTaskMapper.updateTnWorkTask(tnWorkTask);
                } else {
                    return oaFlowResponse;
                }
            } catch (Exception e) {
                e.printStackTrace();
                Log.info(LogBuilder.builder().tag("MDM").error("经销商-客户冻结-提交OA时异常"+ e).build());
                return ServiceResponse.fail("提交OA时异常:" + serviceResponse.getData().toString());
            }
        }
        return serviceResponse;
    }

SQL模拟:死锁

在这里插入图片描述

在这里插入图片描述
按照执行步骤执行后,出现这个问题:(当尝试获取锁的时候死锁;尝试重新启动事务)
在这里插入图片描述

加上索引后,就不会有这个问题了,(第二次测试的时候,记得先执行提交下两个事务,重新开启)
在这里插入图片描述

事务1窗口:

-- 创建一个测试表
CREATE TABLE test_table (
  id INT PRIMARY KEY,
  value INT
);

-- 插入初始数据
INSERT INTO `tn_mdm_test`.`test_table` (`id`, `value`, `task_id`) VALUES (1, 75, 1111);
INSERT INTO `tn_mdm_test`.`test_table` (`id`, `value`, `task_id`) VALUES (2, 90, 2222);
INSERT INTO `tn_mdm_test`.`test_table` (`id`, `value`, `task_id`) VALUES (3, 100, 3333);
INSERT INTO `tn_mdm_test`.`test_table` (`id`, `value`, `task_id`) VALUES (4, 100, 4444);


-- 开启两个事务并同时修改同一张表数据,没有索引
-- 事务1
START TRANSACTION;
INSERT INTO test_table (id, value, task_id) VALUES (5, 100, 4445);
UPDATE test_table SET value = value + 10 WHERE task_id = 4445;

-- 提交事务
COMMIT;

事务2窗口:

-- 事务2
START TRANSACTION;
UPDATE test_table SET value = value - 5 WHERE task_id = 3333;

-- 提交事务
COMMIT;

SQL模拟:锁超时

在这里插入图片描述

在这里插入图片描述

按照步骤执行后,出现锁超时问题(锁等待超时;尝试重新启动事务)

在这里插入图片描述

锁超时和死锁场景不同

锁超时:是在事务1中执行 update 操作,不提交事务前,开启事务2去执行 update 操作,事务2获取锁超时
死锁:是在事务1中执行 add 操作,不提交事务前,开启事务2去执行 update 操作,再在事务1中执行 update 操作,会发生死锁

锁超时是一个拿着东西不放手,另一个一直等
死锁对方持有自己想要的资源,都不放手

锁超时:事务1 update 时,因为更新时不是通过索引更新,所以锁表,这时,事务2就一直等待
死锁:现在设想为两个事务的update操作,因为task_id不是索引,所以都想升级为表锁,升级前提是都要获取对方行锁,这样互相持有了对方想要的资源,产生死锁

锁超时猜想的理解:事务锁:理解为一个大锁,里面包含多个行锁,处在同一个事务下,可以对同一条记录insert、update操作,但是不同事务,对同一行数据操作,就会获取不到锁,因为第一个事务锁会在提交后释放
在这里插入图片描述

死锁猜想的理解:没有索引时,更新时,事务2行锁想要升级为表锁,就要拿到每一行的行锁,但是事务1没有提交,行锁111没有释放,相互等待对方释放资源
在这里插入图片描述
问:在sql中,在同一个事务中,先执行insert一条记录,再更新这条记录,行锁中间会被释放吗?


在同一个事务中,先执行插入一条记录,再更新这条记录,在默认的事务隔离级别下,行锁会被保持并不会被释放。

在默认的事务隔离级别(可重复读)下,事务在执行更新操作时会获取并持有行级锁,直到事务提交或回滚。因此,在同一个事务中,先执行插入操作后再执行更新操作,行锁会一直被保持,不会被释放。

只有当事务提交或回滚后,行锁才会被释放,其他事务才能获取到该行的锁并进行操作。

总结来说,在同一个事务中,先执行插入一条记录,再更新这条记录,行锁会被保持并不会被释放,直到事务提交或回滚。 

代码模拟:

    // 测试两个线程同时对一张表操做
    public class Test {
        public static void main(String[] args) {
            //MyThread1 myThread1 = new MyThread1();
            //MyThread2 myThread2 = new MyThread2();
            //
            //new Thread(myThread1).start();
            //new Thread(myThread2).start();
            for (int i = 0; i < 10000000; i++) {
                HttpUtils.sendGet("http://192.168.52.49:8888/task/task/add");
                HttpUtils.sendGet("http://192.168.52.49:8888/task/task/update");
            }

        }
    }

	// Controller层
    @Anonymous
    @GetMapping("/add")
    public void add(){
        tnWorkTaskService.add();
    }
    @Anonymous
    @GetMapping("/update")
    public void update() throws InterruptedException {
        tnWorkTaskService.update();
    }

	

	// Service层
	public void add();

    public void update();



	// ServiceImpl层
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void add() {
        Random rand = new Random();
        int num = rand.nextInt(900000000) + 100000000;
        tnWorkTaskService.insertTnWorkTask(new TnWorkTask(){{

            setTaskId(String.valueOf(num));
            setUpdateTime(DateUtils.getNowDate());
            setUpdateBy("xiaosong");
            setApplicantId("add1");
        }});
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tnWorkTaskService.updateTnWorkTask(new TnWorkTask(){{
            setTaskId(String.valueOf(num));
            setUpdateTime(DateUtils.getNowDate());
            setUpdateBy("xiaosong2222");
            setApplicantId("add2");
        }});
    }


    @Override
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        tnWorkTaskService.updateTnWorkTask(new TnWorkTask(){{
            setTaskId("WT2022112100001");
            setUpdateTime(DateUtils.getNowDate());
            setUpdateBy("xiaosong");
            setApplicantId("update1");
        }});
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tnWorkTaskService.updateTnWorkTask(new TnWorkTask(){{
            setTaskId("WT2022112100001");
            setUpdateTime(DateUtils.getNowDate());
            setUpdateBy("xiaosong2222");
            setApplicantId("update2");
        }});
    }

扩展问题:

ConcurrencyFailureException 出现必须有事务吗?

ChatGPT回答:

是的,ConcurrencyFailureException异常通常在并发操作中出现,它表示在执行数据库操作时发生了并发冲突。在处理这种异常时,通常需要使用事务来确保数据的一致性和并发控制。

事务是一组数据库操作的逻辑单元,要么全部成功执行,要么全部回滚。通过在事务中包装数据库操作,可以确保在并发环境中的数据一致性和完整性。

因此,当你遇到ConcurrencyFailureException异常时,你应该考虑在相关的数据库操作中添加事务支持,以确保操作的原子性和并发控制。这可以通过在方法上添加@Transactional注解来实现,或者在代码中显式地启动和提交事务。

ConcurrencyFailureException出现场景是什么?

ChatGPT回答:

ConcurrencyFailureException通常在以下场景下出现:

1. 并发更新:当多个线程或进程同时尝试更新同一行数据时,可能会导致并发冲突。例如,两个线程同时尝试更新数据库中的同一条记录,其中一个线程会成功更新,而另一个线程会抛出ConcurrencyFailureException异常。

2. 乐观锁冲突:在使用乐观锁机制时,如果一个线程在读取数据后,另一个线程修改了相同的数据并提交了更改,那么在第一个线程尝试提交时,会检测到数据已经被修改,从而抛出ConcurrencyFailureException异常。

3. 事务隔离级别:在某些事务隔离级别下,例如可重复读(Repeatable Read)或串行化(Serializable),当一个事务在读取数据后,另一个事务修改了相同的数据并提交了更改,那么在第一个事务尝试提交时,会检测到数据已经被修改,从而抛出ConcurrencyFailureException异常。

总之,ConcurrencyFailureException通常在并发操作中出现,表示在执行数据库操作时发生了并发冲突。这可能是由于多个线程或进程同时访问和修改相同的数据所导致的。为了解决这个问题,可以使用事务和适当的并发控制机制来确保数据的一致性和并发安全性。 

参考文章

Deadlock found when trying to get lock; try restarting transaction 【MySQL死锁问题解决】

Mysql报Deadlock found when trying to get lock; try restarting transaction问题解决

java事务异常——Spring事务异常回滚,捕获异常不抛出就不会回滚

手写spring声明式事务@Transactional(一):手把手解析源码(配gif动态图)


Spring事务传播行为详解(有场景)

spring 事务失效的 12 种场景

18个示例详解 Spring 事务传播机制(附测试源码)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

宋大米Pro

感谢小主大赏,留言可进互助群~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值