【工作中问题解决实践 九】Spring中事务传播的问题排查

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 【工作中问题解决实践 九】Spring中事务传播的问题排查

最近在工作中遇到了三个关于事务操作的问题,顺便就着这三个问题又回顾了一遍Spring的事务相关的操作,想着一次性把这个问题研究明白了,后续使用事务的时候也能踏实点,让事务发挥真实的作用

什么是事务?什么是事务管理?什么是Spring事务

什么是事务?事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用,关乎数据准确性的地方我们一定要用到事务,防止业务逻辑出错。

什么是事务管理,事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到操作过程中机器突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过取款机一样,以保证用户和银行的利益都不受损失

Sping事务简而言之就是一种JTA事务,这里不再详细展开。

一个用来演示的例子

我们还是沿用:【Spring学习笔记 九】Spring声明式事务管理实现机制这篇文章中的例子,只不过为了更贴近工作实战,这里我重构了一下代码实现。

单元测试入口

package com.example.springboot;
import com.example.springboot.model.Person;
import com.example.springboot.service.PersonAggService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;
    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.addPerson(person, 100086L);
    }
}

聚合的Service方法

package com.example.springboot.service;
import com.example.springboot.model.Person;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonAggService {
    @Resource
    private PersonService personService;
    @Resource
    private PersonMaintainService personMaintainService;
   public void addPerson(Person person, Long creatorId) {
        //本地新增人员
        personService.insert(person);
        //保存人员创建者
        personMaintainService.savePersonCreator(creatorId);
    }
}

数据服务方法

package com.example.springboot.service;
import com.example.springboot.dao.PersonDao;
import com.example.springboot.model.Person;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;
@Service
public class PersonService {
    @Resource
    PersonDao personDao;
    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }
    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }
    public void insert(Person person) {
        personDao.insert(person);
    }
}

人员维护人添加方法

package com.example.springboot.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

数据表落库

不使用事务的情况

不使用事务的情况虽然单元测试报错了

但是数据库落库还是成功了:

遇到的三个事务问题

依据以上的基本case示例,模拟我遇到的两个问题和解决方案

问题一:Transaction rolled back because it has been marked as rollback-only

为了保证整体数据与预期一致可以回滚,我使用了事务,首先在外层加事务:

@Transactional(rollbackFor = Exception.class)
    public void addPerson(Person person, Long creatorId) {
        //本地新增人员
        personService.insert(person);
        try {
            //发送人员同步到下游系统
            personMaintainService.savePersonCreator(creatorId);
        } catch (Exception e) {
            System.out.println("保存人员维护人异常但是被catch住了");
        }
    }

同时呢人员创建人这块我认为这里不需要报错阻塞整体操作,如果这里有问题只要有日志记录就行了,我通过巡检检查关注到即可,所以对这块代码加了try catch,但是呢因为内部代码不知道是谁写的也加了事务,

@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

因为它们用的都是默认的传播机制,所以可以看做一个事务,使用REQUIRED传播模式,addAndSendPerson和savePersonCreator在同一个事务里面,savePersonCreator抛出异常要回滚,addAndSendPerson try Catch了异常正常执行commit,同一个事务一个要回滚,一个要提交,会报read-only异常,结果就是全部回滚,而外层所以这里就会出现rollback-only

解决方法有两种,一种是

干掉内层事务

内层的savePersonCreator事务干掉,这时数据也能落库成功了,事实上因为JTA的事务是有非常强的业务含义的,所以对于DAO层或简单的数据操作指令,不要加事务,否则对于较长的外部调用链路,会在传播过程中导致意外情况发生

内层声明为新事务

还有一种解决思路就是内层的事务声明为新事务

package com.example.springboot.service;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

声明后再跑单测:

数据也落库成功了,因为是两个独立事务,所以内层事务遇到异常回滚,外层事务捕获到了异常catch住了,没有继续回滚

问题二:事务设置为什么不生效?

还有个例子是方法设置了事务但是不生效,我们再调整下以上的代码,模拟一种场景:savePerson要执行很多事项,但是不希望saveDate的执行异常回滚影响整体回滚,所以saveDate中的核心数据操作被try catch,并且声明内部的savePersonCreator方法为新事务,符合上边我们提到的那种场景,这种情况下理论上savePersonCreator抛出异常后会使 personDao.insert(person);回滚,数据不能写入

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;
    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }
}
@Service
public class PersonAggService {
    @Resource
    PersonDao personDao;
    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        try {
            savePersonCreator(person, creatorId);
        } catch (Exception e) {
            System.out.println("捕获到创建人员异常");
        }
    }
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

但事实上,数据库写入数据能成功:

数据库数据写入成功了

这是因为:Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用,所以实际上saveDate和savePersonCreator的事务都没有生效

把需要成为事务的方法单独抽出来

上述代码我们把需要有事务机制的savePersonCreator单独抽到一个方法中

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;
    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }
}
@Service
public class PersonAggService {
    @Resource
    PersonService personService;
    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        try {
            personService.savePersonCreator(person, creatorId);
        } catch (Exception e) {
            System.out.println("捕获到创建人员异常");
        }
    }
}
@Service
public class PersonService {
    @Resource
    PersonDao personDao;
    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }
    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

这样savePersonCreator的事务就生效了,数据没有插入成功

问题三:为什么会发生org.springframework.dao.CannotAcquireLockException

经过上一步的调整事务总算生效了,这里我们对上述代码再做一个调整,让内层和外层的事务都对同一张表进行操作,代码清单如下:

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;
    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }
}
@Service
public class PersonAggService {
    @Resource
    PersonService personService;
    @Resource
    PersonDao personDao;
    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        Person personOut = new Person();
        personOut.setUsername("outPerson");
        personOut.setAge(30);
        personOut.setEmail("111@qq.com");
        personOut.setPassword("111111");
        personOut.setPhone(11111111);
        personOut.setHobby("跳远");
        personDao.insert(personOut);
        try {
            personService.savePersonCreator(person, creatorId);
        } catch (Exception e) {
            System.out.println("捕获到创建人员异常");
        }
    }
}
@Service
public class PersonService {
    @Resource
    PersonDao personDao;
    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }
    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

我们发现外部事务的数据提交成功,而内部事务的异常被捕获了

同时这句话也没有打印出来,说明还没有执行到手动抛异常的位置:

外部事务数据正确落库

为了更细致的看,我们把异常捕获干掉

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;
    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }
}
@Service
public class PersonAggService {
    @Resource
    PersonService personService;
    @Resource
    PersonDao personDao;
    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        Person personOut = new Person();
        personOut.setUsername("outPerson");
        personOut.setAge(30);
        personOut.setEmail("111111@qq.com");
        personOut.setPassword("111111");
        personOut.setPhone(11111111);
        personOut.setHobby("跳远");
        personDao.insert(personOut);
        personService.savePersonCreator(person, creatorId);
    }
}
@Service
public class PersonService {
    @Resource
    PersonDao personDao;
    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }
    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
    }
}

其原因就是两个不同的事务,对同一张表进行操作:

  • 事务的传播行为: Spring 允许定义事务的传播行为,即在一个事务方法中调用另一个事务方法时如何处理事务。其中一种传播行为是 Propagation.REQUIRES_NEW,它表示每次方法被调用时都会启动一个新的事务,而不管是否已经存在一个事务。如果在嵌套事务中使用 Propagation.REQUIRES_NEW,可能会导致内层事务与外层事务并发执行,从而产生并发问题。于是抛出了获取锁失败的异常。为了方便大家看是不是一个问题,把这个异常详细粘贴出来

而多次调用实验也表明,有时候数据是会落库成功的,所以就看内外事务的并发执行时机了。

org.springframework.dao.CannotAcquireLockException: 
### Error querying database.  Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
### The error may exist in file [F:\JavaWeb\springboot\target\classes\mapper\personMapper.xml]
### The error may involve com.example.springboot.dao.PersonDao.insert-Inline
### The error occurred while setting parameters
### SQL: insert into person (id,username,password,age,phone,email,hobby) values (?,?,?,?,?,?,?)
### Cause: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
; Lock wait timeout exceeded; try restarting transaction; nested exception is com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
  at org.springframework.jdbc.support.SQLErrorCodeSQLExceptionTranslator.doTranslate(SQLErrorCodeSQLExceptionTranslator.java:267)
  at org.springframework.jdbc.support.AbstractFallbackSQLExceptionTranslator.translate(AbstractFallbackSQLExceptionTranslator.java:70)
  at org.mybatis.spring.MyBatisExceptionTranslator.translateExceptionIfPossible(MyBatisExceptionTranslator.java:91)
  at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:441)
  at com.sun.proxy.$Proxy107.selectOne(Unknown Source)
  at org.mybatis.spring.SqlSessionTemplate.selectOne(SqlSessionTemplate.java:160)
  at org.apache.ibatis.binding.MapperMethod.execute(MapperMethod.java:87)
  at org.apache.ibatis.binding.MapperProxy$PlainMethodInvoker.invoke(MapperProxy.java:145)
  at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:86)
  at com.sun.proxy.$Proxy108.insert(Unknown Source)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:344)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
  at org.springframework.dao.support.PersistenceExceptionTranslationInterceptor.invoke(PersistenceExceptionTranslationInterceptor.java:137)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:215)
  at com.sun.proxy.$Proxy109.insert(Unknown Source)
  at com.example.springboot.service.PersonService.savePersonCreator(PersonService.java:27)
  at com.example.springboot.service.PersonService$$FastClassBySpringCGLIB$$2df9c1a9.invoke(<generated>)
  at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
  at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
  at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
  at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
  at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
  at com.example.springboot.service.PersonService$$EnhancerBySpringCGLIB$$7b8fd37c.savePersonCreator(<generated>)
  at com.example.springboot.service.PersonAggService.savePerson(PersonAggService.java:32)
  at com.example.springboot.service.PersonAggService$$FastClassBySpringCGLIB$$7eb0a300.invoke(<generated>)
  at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:779)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
  at org.springframework.transaction.interceptor.TransactionInterceptor$1.proceedWithInvocation(TransactionInterceptor.java:123)
  at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:388)
  at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:119)
  at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
  at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:750)
  at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:692)
  at com.example.springboot.service.PersonAggService$$EnhancerBySpringCGLIB$$ec6485e1.savePerson(<generated>)
  at com.example.springboot.SpringbootApplicationTests.springTransTest(SpringbootApplicationTests.java:24)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:688)
  at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
  at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)
  at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:149)
  at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:140)
  at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:84)
  at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
  at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
  at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)
  at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)
  at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)
  at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)
  at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
  at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
  at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:210)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:206)
  at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:131)
  at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:65)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
  at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
  at java.util.ArrayList.forEach(ArrayList.java:1257)
  at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
  at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
  at java.util.ArrayList.forEach(ArrayList.java:1257)
  at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:143)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:129)
  at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:127)
  at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:126)
  at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:84)
  at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
  at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
  at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:108)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:88)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:54)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:67)
  at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:52)
  at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:96)
  at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:75)
  at com.intellij.junit5.JUnit5IdeaTestRunner.startRunnerWithArgs(JUnit5IdeaTestRunner.java:57)
  at com.intellij.rt.junit.IdeaTestRunner$Repeater$1.execute(IdeaTestRunner.java:38)
  at com.intellij.rt.execution.junit.TestsRepeater.repeat(TestsRepeater.java:11)
  at com.intellij.rt.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:35)
  at com.intellij.rt.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:235)
  at com.intellij.rt.junit.JUnitStarter.main(JUnitStarter.java:54)
Caused by: com.mysql.cj.jdbc.exceptions.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
  at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:123)
  at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122)
  at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:953)
  at com.mysql.cj.jdbc.ClientPreparedStatement.execute(ClientPreparedStatement.java:370)
  at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3461)
  at com.alibaba.druid.wall.WallFilter.preparedStatement_execute(WallFilter.java:663)
  at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3459)
  at com.alibaba.druid.filter.FilterEventAdapter.preparedStatement_execute(FilterEventAdapter.java:440)
  at com.alibaba.druid.filter.FilterChainImpl.preparedStatement_execute(FilterChainImpl.java:3459)
  at com.alibaba.druid.proxy.jdbc.PreparedStatementProxyImpl.execute(PreparedStatementProxyImpl.java:167)
  at com.alibaba.druid.pool.DruidPooledPreparedStatement.execute(DruidPooledPreparedStatement.java:497)
  at org.apache.ibatis.executor.statement.PreparedStatementHandler.query(PreparedStatementHandler.java:64)
  at org.apache.ibatis.executor.statement.RoutingStatementHandler.query(RoutingStatementHandler.java:79)
  at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:63)
  at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325)
  at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109)
  at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89)
  at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:151)
  at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:145)
  at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140)
  at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76)
  at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
  at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
  at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.lang.reflect.Method.invoke(Method.java:498)
  at org.mybatis.spring.SqlSessionTemplate$SqlSessionInterceptor.invoke(SqlSessionTemplate.java:427)

内部不声明新事务

解决方案一就是内部不声明新事务,同一个事务下就不存在这个问题了

@Service
public class PersonService {
    @Resource
    PersonDao personDao;
    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }
    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }
    @Transactional(rollbackFor = Exception.class)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
    }
}

在运行一下发现两条记录都插入成功了

内部事务抽出来

还有一种方式就是不要搞事务嵌套,把内部方法抽出来和外层的平行,内部方法执行依赖的入参由原外部事务的返回值给出,做到不相互依赖。

问题场景

基于保密原则,原始代码不能贴出来,所以用一些示例表示,其实真实场景比较复杂,我们正在做的事情是数据迁移,一个加盟商可能带一堆门店迁移,它们都有公司的数据,并且我们希望一个门店迁移失败不影响整体【加盟商是外层,门店是内层】

  1. 所以我开始在加盟商和门店的同步方法都加了事务(默认传播机制),然后catch门店抛出的异常,于是就导致了问题一:Transaction rolled back because it has been marked as rollback-only
  2. 为了解决问题一,我将门店的同步方法声明为新事务,结果发现事务不生效,原来是我解决问题二的时候把门店方法放回到了加盟商同步方法一个类,导致注解不生效
  3. 把门店方法又挪出来后又发现数据同步时会偶发org.springframework.dao.CannotAcquireLockException,发现是加盟商和门店都对公司表有插入行为,产生了并发死锁。于是我就把门店的同步单独提出来和加盟商同步平级按顺序执行,门店需要的参数由加盟商执行结果返回,同时每个门店执行加了catch,只影响自己

这样总算解决了问题,表面看好像很简单,但经历了一段时间的排查过程,总结一句话:事务是一系列紧密相关行为的集合。按这个定义其实我早就应该想到把门店的同步抽出来,不要搞什么嵌套事务。不过怎么说排查过程中也加深了对Spring事务的理解吧

Spring事务的更多传播机制

以上两个示例是真实工作中遇到的,基于安全原则模拟了两个类似的case,其实spring还有更多的花式的事务使用机制,可以参照带你读懂Spring 事务——事务的传播机制

总结一下

照例总结一下,在单一的数据操作方法不要加事务,事务应该是一系列操作指令的聚合,添加了细粒度的事务可能会导致上层使用者在方法添加事务时产生了非预期的传播机制。当然如果内外层的方法调用都很复杂,则基于自己的预期进行考虑,如果不希望内层方法影响外层方法,可以使用外层方法异常捕获加内层事务的REQUIRES_NEW传播机制解决。需要注意的是Spring的事务是基于AOP实现的,所以对象内部方法调用,不会通过Spring代理,也就是事务不会起作用,这点非常重要。

相关实践学习
如何快速连接云数据库RDS MySQL
本场景介绍如何通过阿里云数据管理服务DMS快速连接云数据库RDS MySQL,然后进行数据表的CRUD操作。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
打赏
0
0
0
0
33
分享
相关文章
Spring中事务失效的场景
因为Spring事务是基于代理来实现的,所以某个加了@Transactional的⽅法只有是被代理对象调⽤时, 那么这个注解才会⽣效 , 如果使用的是被代理对象调用, 那么@Transactional会失效 同时如果某个⽅法是private的,那么@Transactional也会失效,因为底层cglib是基于⽗⼦类来实现 的,⼦类是不能重载⽗类的private⽅法的,所以⽆法很好的利⽤代理,也会导致@Transactianal失效 如果在业务中对异常进行了捕获处理 , 出现异常后Spring框架无法感知到异常, @Transactional也会失效
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——常见问题总结
本文总结了Spring Boot中使用事务的常见问题,虽然通过`@Transactional`注解可以轻松实现事务管理,但在实际项目中仍有许多潜在坑点。文章详细分析了三个典型问题:1) 异常未被捕获导致事务未回滚,需明确指定`rollbackFor`属性;2) 异常被try-catch“吃掉”,应避免在事务方法中直接处理异常;3) 事务范围与锁范围不一致引发并发问题,建议调整锁策略以覆盖事务范围。这些问题看似简单,但一旦发生,排查难度较大,因此开发时需格外留意。最后,文章提供了课程源代码下载地址,供读者实践参考。
23 0
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——Spring Boot 事务配置
本文介绍了 Spring Boot 中的事务配置与使用方法。首先需要导入 MySQL 依赖,Spring Boot 会自动注入 `DataSourceTransactionManager`,无需额外配置即可通过 `@Transactional` 注解实现事务管理。接着通过创建一个用户插入功能的示例,展示了如何在 Service 层手动抛出异常以测试事务回滚机制。测试结果表明,数据库中未新增记录,证明事务已成功回滚。此过程简单高效,适合日常开发需求。
26 0
微服务——SpringBoot使用归纳——Spring Boot事务配置管理——事务相关
本文介绍Spring Boot事务配置管理,阐述事务在企业应用开发中的重要性。事务确保数据操作可靠,任一异常均可回滚至初始状态,如转账、购票等场景需全流程执行成功才算完成。同时,事务管理在Spring Boot的service层广泛应用,但根据实际需求也可能存在无需事务的情况,例如独立数据插入操作。
17 0
Spring中的事务是如何实现的
1. Spring事务底层是基于数据库事务和AOP机制的 2. ⾸先对于使⽤了@Transactional注解的Bean,Spring会创建⼀个代理对象作为Bean 3. 当调⽤代理对象的⽅法时,会先判断该⽅法上是否加了@Transactional注解 4. 如果加了,那么则利⽤事务管理器创建⼀个数据库连接 5. 并且修改数据库连接的autocommit属性为false,禁⽌此连接的⾃动提交,这是实现Spring事务⾮ 常重要的⼀步 6. 然后执⾏当前⽅法,⽅法中会执⾏sql 7. 执⾏完当前⽅法后,如果没有出现异常就直接提交事务 8. 如果出现了异常,并且这个异常是需要回滚的就会回滚事务
Spring事务失效,常见的情况有哪些?
本文总结了Spring事务失效的7种常见情况,包括未启用事务管理功能、方法非public类型、数据源未配置事务管理器、自身调用问题、异常类型错误、异常被吞以及业务和事务代码不在同一线程中。同时提供了两种快速定位事务相关Bug的方法:通过查看日志(设置为debug模式)或调试代码(在TransactionInterceptor的invoke方法中设置断点)。文章帮助开发者更好地理解和解决Spring事务中的问题。
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
本文介绍了如何使用Spring Cloud Alibaba 2023.0.0.0技术栈构建微服务网关,以应对微服务架构中流量治理与安全管控的复杂性。通过一个包含鉴权服务、文件服务和主服务的项目,详细讲解了网关的整合与功能开发。首先,通过统一路由配置,将所有请求集中到网关进行管理;其次,实现了限流防刷功能,防止恶意刷接口;最后,添加了登录鉴权机制,确保用户身份验证。整个过程结合Nacos注册中心,确保服务注册与配置管理的高效性。通过这些实践,帮助开发者更好地理解和应用微服务网关。
98 0
🛡️Spring Boot 3 整合 Spring Cloud Gateway 工程实践
微服务架构设计与实践:用Spring Cloud实现抖音的推荐系统
本文基于Spring Cloud实现了一个简化的抖音推荐系统,涵盖用户行为管理、视频资源管理、个性化推荐和实时数据处理四大核心功能。通过Eureka进行服务注册与发现,使用Feign实现服务间调用,并借助Redis缓存用户画像,Kafka传递用户行为数据。文章详细介绍了项目搭建、服务创建及配置过程,包括用户服务、视频服务、推荐服务和数据处理服务的开发步骤。最后,通过业务测试验证了系统的功能,并引入Resilience4j实现服务降级,确保系统在部分服务故障时仍能正常运行。此示例旨在帮助读者理解微服务架构的设计思路与实践方法。
150 17
【SpringFramework】Spring事务
本文简述Spring中数据库及事务相关衍伸知识点。
57 9
Spring Cloud Alibaba AI 入门与实践
本文将介绍 Spring Cloud Alibaba AI 的基本概念、主要特性和功能,并演示如何完成一个在线聊天和在线画图的 AI 应用。
557 7

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等