事务在逻辑上可以认为就是把一组操作看作一个动作。这个动作的内容要么都成功,要么都失败,这样才能保证结果的准确性、一致性。
如下代码所示,如果下面这段代码两个插入操作不属于同一个事务的话,结束时只有张三被插入和李四没有插入,不符合业务上的准确性。
这里还得补充一句,使用事务进行数据库增删改查操作时,必须保证当前使用的数据库引擎支持事务,以MySQL为例,MySQL默认引擎为innodb,他就是支持事务的。若时myisam则不支持事务,无法实现数据回滚。
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
//报错
throw new RuntimeException();
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
}
英文翻译成中文大致是:原子性、隔离性、一致性、持久性。分别代表的含义是:
有两种姿势,分别是手动式事务和注解式事务,前者是手动的,比较少使用,对应的类是TransactionTemplate
或者TransactionManager
,使用示例如下所示:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
或者下面这样一段代码,都是通过都是传入需要进行事务管理的bean定义,进行手动操作管理
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
而后者就比较常用了,基于注解(底层是通过AOP实现的)
,使用的示例代码如下所示
@Transactional
public void transaction_exception_nested_nested(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
先来说说事务隔离级别:
default(默认)
:PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别,除了default 其它几个Spring事务隔离级别与JDBC事务隔离级别相对应。read_uncommited(读未提交)
:一个事务可以读取另外一个事务未提交的数据,这可能出现脏读 而且不可重复度,出现幻像读等。read_commited(读已提交):
一个事务可以读取另一个事务已经提交的数据,不可以读取未提交的数据repeatTable_read(可重复读)
:一个事务可以读取另外一个事务已经提交的数据,可以避免脏读的前提下 ,也可以避免不可重复读,但是还是无法避免幻像读。serializable(串行化)
:这是一个花费较高但是比较可靠的事务隔离级别,可以避免脏读 幻像读和不可重复读(事务被处理为顺序执行)
Spring
事务传播属性:
required(默认属性)
:Propagation.REQUIRED
内外部属于统一事务,一个回滚全部回滚。(后文会有代码演示)
Mandatory
:如果当前存在事务,则支持当前事务,如果不存在事务,则抛出异常Never
:以非事务方式执行,如果当前存在事务,则抛出异常Supports
:如果当前存在事务,则支持当前事务,.如果不存在事务,以非事务方式执行Not_Supports
:以非事务方式执行操作,如果存在事务,则挂起当前事务required_new
:在外围方法开启事务的情况下Propagation.REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。Nested
:嵌套,支持当前事务,内层事务的执行失败不会导致外层事务的回滚,但是外层事务的回滚会影响内层事务导致内层事务随外层事务一同回滚.为了更好的解答后续的问题,这里我们给出了一个示例,user1
表的service
@Service
public class User1Service {
@Resource
private User1Mapper user1Mapper;
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User1 user){
user1Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User1 user){
user1Mapper.insert(user);
}
@Transactional(propagation = Propagation.NESTED)
public void addNested(User1 user){
user1Mapper.insert(user);
}
}
user2
表的service
,可以看到对于数据库的操作都在注解上标出不同的传播行为
@Service
public class User2Service {
@Resource
private User2Mapper user2Mapper;
@Transactional(propagation = Propagation.REQUIRED)
public void addRequired(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRED)
public void addRequiredException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNew(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addRequiresNewException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
@Transactional(propagation = Propagation.NESTED)
public void addNested(User2 user){
user2Mapper.insert(user);
}
@Transactional(propagation = Propagation.NESTED)
public void addNestedException(User2 user){
user2Mapper.insert(user);
throw new RuntimeException();
}
}
它是Spring
的默认传播行为,说白了发生嵌套在内部的事务会和外部的事务融合,所以外部事务报错了内部事务也会回滚。
如下面这段代码,外部的方法没有加事务,且user1Service
、user2Service
的方法都是PROPAGATION_REQUIRED
这个传播级别,所以外部报错不影响两者的内部提交
/**
* 彼此都有独立的事务,外部没有开事务,所以两者数据都会入库
*/
@GetMapping("/test/add1")
public void notransaction_exception_required_required() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
然后我们再来看看这样一段代码,外部没有加事务,所以内部两个事务彼此独立。可以看到user2Service
报错,所以只有user1Service
插入成功
@GetMapping("/test/add2")
public void notransaction_required_required_exception() {
//插入成功
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
//事务是独立的插入失败
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequiredException(user2);
throw new RuntimeException();
}
最后在看看这个,外部加了事务,也是REQUIRED
,所以内部两个事务与其融合成为一个事务,当外部方法报错,两者插入操作都失败,数据直接回滚
/**
* 外部开启事务,报错均回滚
*/
@GetMapping("/test/add3")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_required() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequired(user2);
throw new RuntimeException();
}
再来看看一个比较好玩的,内外部都有事务,第2个内部事务报错,由于三者事务融为一体,所以user2Service
的错误被外部transaction_required_required_exception
方法感知,user1Service
插入也是失败的,所以这个方法两张表都没有插入数据
@GetMapping("/test/add4")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_required_exception() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//错误被外部感知,所以所有user1的插入也被回滚了
user2Service.addRequiredException(user2);
}
这个也比较特殊,由于三个事务合为一体,所以即使user2Service
报错不被感知,两张表的数据也还是没有插入
@GetMapping("/test/add5")
@Transactional
public void transaction_required_required_exception_try() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
try {
//虽然异常被捕获,但是三个内外部事务融合了,一个报错就全部插入回滚
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
总结:Propagation.REQUIRED
内外部属于统一事务,一个回滚全部回滚
我们还是通过看代码的方式来讲述吧:
第一个例子,外部没有加事务,两个service
彼此事务独立,外部报错,但是两者事务都已提交,所以都插入了
@GetMapping("/test/add6")
public void notransaction_exception_requiresNew_requiresNew() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequiresNew(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addRequiresNew(user2);
throw new RuntimeException();
}
外部还是没有开启事务,user2Service
报错事务回滚,所以只有user1Service
插入了。
@GetMapping("/test/add7")
public void notransaction_requiresNew_requiresNew_exception() {
User1 user1 = new User1();
user1.setName("张三");
//正常插入
user1Service.addRequiresNew(user1);
User2 user2 = new User2();
user2.setName("李四");
//保存回滚了
user2Service.addRequiresNewException(user2);
}
来看一个综合的,外部加了REQUIRED
,所以内部第一个事务和外部融合,后两个事务独立,在外部报错的情况下只有addRequired
回滚。李四、王五均被插入。
@GetMapping("/test/add8")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_exception_required_requiresNew_requiresNew() {
User1 user1 = new User1();
user1.setName("张三");
//和外部事务融合,外部报错插入被回滚
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
// 事务独立,不受外部影响,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
// 事务独立,不受外部影响,正常插入
user2Service.addRequiresNew(user3);
throw new RuntimeException();
}
外部加了事务,由于王五报错被外部感知,张三的事务和外部融合,所以张三没有被插入,这题只有李四被插入了
@GetMapping("/test/add9")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception() {
User1 user1 = new User1();
user1.setName("张三");
//和外部融合
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//事务独立,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
//报错,插入被回滚,外部感知到了错误,所以张三的插入也被回滚了
user2Service.addRequiresNewException(user3);
}
王五报错回滚,但是错误没有被外部感知到,张三和外部事务融合,正常插入、李四正常插入。
@GetMapping("/test/add10")
@Transactional(propagation = Propagation.REQUIRED)
public void transaction_required_requiresNew_requiresNew_exception_try() {
User1 user1 = new User1();
user1.setName("张三");
//正常插入
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
//和外部事务彼此独立,正常插入
user2Service.addRequiresNew(user2);
User2 user3 = new User2();
user3.setName("王五");
try {
// 报错回滚,但错误并没有被外部感知,所以只有这个事务被回滚
user2Service.addRequiresNewException(user3);
} catch (Exception e) {
System.out.println("回滚");
}
}
总结: 在外围方法开启事务的情况下Propagation.REQUIRES_NEW
修饰的内部方法依然会单独开启独立事务,且与外部方法事务也独立,内部方法之间、内部方法和外部方法事务均相互独立,互不干扰。
代码如下,外部没有事务,张三、李四彼此独立一个事务,数据均插入,外部异常不影响成功提交
@GetMapping("/test/add11")
public void notransaction_exception_nested_nested() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2 = new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
同理,外部没有事务,后者报错不影响前者正常插入
@GetMapping("/test/add12")
public void notransaction_nested_nested_exception() {
User1 user1 = new User1();
user1.setName("张三");
//独立的事务,不受下方报错影响
user1Service.addNested(user1);
User2 user2 = new User2();
user2.setName("李四");
//外部没有事务,报错回滚
user2Service.addNestedException(user2);
}
外部开启事务(默认级别),内部事务与其融合,一错全部回滚
@GetMapping("/test/add13")
@Transactional
public void transaction_exception_nested_nested(){
//外部开启事务,所有nest的事务都与外部事务融合,一个报错全部回滚
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNested(user2);
throw new RuntimeException();
}
同上
@GetMapping("/test/add14")
@Transactional
public void transaction_nested_nested_exception(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
user2Service.addNestedException(user2);
}
李四的报错自己消化了
@GetMapping("/test/add15")
@Transactional
public void transaction_nested_nested_exception_try(){
User1 user1=new User1();
user1.setName("张三");
user1Service.addNested(user1);
User2 user2=new User2();
user2.setName("李四");
try {
user2Service.addNestedException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
这个就涉及到数据库的理论知识了,事务的隔离级别大致有以下4种:
READ-UNCOMMITTED(读取未提交)
:这种隔离级别在并发事务下,一个事务未提交的结果,另一个事务是可以看到的,这种情况下,就可能出现脏读、幻读、不可重复读的问题。READ-COMMITTED(读取已提交)
:这种隔离级别,只有提交的事务结果才能被看到,在并发场景下可以避免脏读,但是幻读和不可重复读的问题还有可能发生。REPEATABLE-READ(可重复读)
:对同一字段的读取多次结果都是一致的,除非当前事务修改,可以避免脏读和不可重复读,但是幻读的情况还有可能发生。SERIALIZABLE(可串行化)
: 事务隔离最高级别,事务只能按顺序串行提交,所以脏读、幻读、不可重复读问题都可以解决。这个我们从源码的注释中就能看出端倪了,如下所示,注释中已经说明了只有运行时异常
或者Error
可以触发回滚,对于检查型异常是不会回滚。
/**
* <p>By default, a transaction will be rolling back on {@link RuntimeException}
* and {@link Error} but not on checked exceptions (business exceptions). See
* org.springframework.transaction.interceptor.DefaultTransactionAttribute#rollbackOn(Throwable)
*/
Class<? extends Throwable>[] rollbackFor() default {};
那我如果要自定义一个异常使用怎么办?如下所示即可,在注解上声明我们需要回滚的错误类型即可。
@Transactional(rollbackFor= MyException.class)
@Transactional 作用在不同的地方会有不同的效果,我们最常见的用法就是作用于方法上,如果在方法上加该注解,就会将当前方法中的数据库操作加入事务中。注意方法必须是public
否则事务不会生效。
如果作用于类上,则意味着这个类中所有public
的方法都会用到事务。接口同理,但我们不建议这么用。
它的常见参数配置如下:
传播属性(propagation)
:事务的传播行为,默认值为 REQUIRED
,可选的值在上面介绍过隔离级别(isolation)
:事务的隔离级别,默认值采用 DEFAULT
,可选的值在上面介绍过回滚规则(rollbackFor)
:用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。只读属性(readOnly)
:指定事务是否为只读事务,默认值为 false。超时时间(timeout)
:事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。对于该注解的原理,实际上加了该注解的类就会交由Spring
创建出一个代理对象,如果有接口则叫由jdk代理
创建,反之cglib
,我们从源码中就能看出端倪来。
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
....
//根据判断结果决定代理策略
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
.......
}
这样看可能不够直观,我们不妨用一段示例代码来讲述这个过程吧
如下所示,这是笔者TestController
中的一个方法
@GetMapping("/test/add5")
@Transactional
public void transaction_required_required_exception_try() {
User1 user1 = new User1();
user1.setName("张三");
user1Service.addRequired(user1);
User2 user2 = new User2();
user2.setName("李四");
try {
user2Service.addRequiredException(user2);
} catch (Exception e) {
System.out.println("方法回滚");
}
}
当我们使用postman
调用时,可以看到这个类是一个cglib生成代理类,由此可以得出这个事务方法会交由代理创建
查看调用栈可以发现,这个方法最终会由我们的代理去执行调用
retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();
最终就会走到一个TransactionInterceptor
的invoke
方法。
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Class<?> targetClass = invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null;
if (KotlinDetector.isSuspendingFunction(invocation.getMethod())) {
InvocationCallback callback = new CoroutinesInvocationCallback() {
public Object proceedWithInvocation() {
return CoroutinesUtils.invokeSuspendingFunction(invocation.getMethod(), invocation.getThis(), invocation.getArguments());
}
public Object getContinuation() {
return invocation.getArguments()[invocation.getArguments().length - 1];
}
};
return this.invokeWithinTransaction(invocation.getMethod(), targetClass, callback);
} else {
Method var10001 = invocation.getMethod();
invocation.getClass();
return this.invokeWithinTransaction(var10001, targetClass, invocation::proceed);
}
}
这个问题我们可以通过阅读源码解决,问题的表象是加个事务注解的方法,被一个没有加注解的方法调用,结果报错了事务没有回滚。
这个问题,我们不妨举个例子来说吧,首先我们看看下面这段代码,很明显如果我们直接调用add17报错了事务会回滚,原因很简单,这个method1加了注解,所以如果我们通过api等工具调用method1时,真正执行这段代码的对象是结果Spring容器bpp处理过的cglib代理
@Transactional
@GetMapping("/test/add17")
public void method1(){
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1/0);
}
所以method1
被调用时,代码执行会到达CglibAopProxy
的DynamicAdvisedInterceptor
内部类的intercept代码,感兴趣的读者可以自行debug
,我们就不多赘述,在这里我就贴一下核心代码
@Nullable
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
.......
try {
.....
Class<?> targetClass = target != null ? target.getClass() : null;
//获取当前类的增强器,存到chain中
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
Object retVal;
//如果为空,则说明没有AOP逻辑直接methodProxy.invoke(target, argsToUse)
if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
retVal = methodProxy.invoke(target, argsToUse);
} else {
//反之说明这个类被cglib代理了,就会使用chain 来执行method方法,报错了事务就会回滚
retVal = (new CglibAopProxy.CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy)).proceed();
}
retVal = CglibAopProxy.processReturnType(proxy, target, method, retVal);
var16 = retVal;
} finally {
.......
}
.........
}
好了,有了上文的铺垫,我们再来说说嵌套调用失效问题,代码如下所示,当我们使用postman
等工具调用时,发现method1
执行出错,李四还是被成功插入了,这是为什么呢?
原因很简单method1执行者并不是cglib
代理对象,下面这段method1
,完整的代码应该是this.method1
,
@GetMapping("/test/add16")
public void add16() {
method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1(){
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1/0);
}
这就导致执行调用method1
调用者是this,而不是cglib代理的增强类,如下图所示,正是因为调用者不是代理,导致代理根本不知道method1被调用了,所以事务就失效了
如何解决spring自调用问题呢?
最干脆就是调用时避免嵌套使用就好了,但是有时候应该这个方法要依赖外部的处理逻辑,而外部方法又臭又长改造两量很大导致无法重构。这时候我们只能想别的办法。我以前解决的办法就比较干脆了,既然问题的根源是调用对象错误,那我就干脆找出这个对象来调用不就解决了?
所以我们的思路是这样的,如下代码所示:
首先的controller
中假如应用上下文
@Autowired
private ApplicationContext applicationContext;
用这个上下文去容器中把他捞出来调用method1,问题解决
@GetMapping("/test/add16")
public void add16() {
TestController t = (TestController) applicationContext.getBean("testController");
t.method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1() {
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1 / 0);
}
当然这里还有一种方法,将代理的service类注入,因为spring注入的类都是经过cglib增强的类,所以使用注入的bean也能解决问题,只不过写法很丑陋而已。
@Autowired
private TestController t;
@GetMapping("/test/add16")
public void add16() {
t.method1();
}
@Transactional
@GetMapping("/test/add17")
public void method1() {
User2 user2 = new User2();
user2.setName("李四");
user2Service.insert(user2);
System.out.println(1 / 0);
}
整体大概有以下几点:
@Transactional
的 rollbackFor
和 propagation
属性,否则事务可能会回滚失败;@Transactional
注解的方法,这样会导致事务失效@Transactional
注解的方法所在的类必须被 Spring
管理,否则不生效;@Transactional
注解只有作用到 public
方法上事务才生效,不推荐在接口上使用;