@Transactional事务的使用
在Service层中,给service类或者指定的方法添加@Transactional注解即可
@Transactional注解还有一些可以设置的属性,后面会说到
事务的失效场景
一、抛出检查异常
原因: 使用@Transactional注解的时候,没有加rollBackFor类型
rollBackFor默认抛出的是RuntimeException异常(查看Spring源码注释得知),所以当没有声明rollBackFor类型,并且方法内抛出非RuntimeException异常时,是不会造成事务的回滚的,我们来测试一下
复现问题:
@Transactional
public void save(User user) throws IOException {
userMapper.insert(user);
throw new RuntimeException("模拟异常");
}
@Transactional
public void save(User user) throws IOException {
userMapper.insert(user);
throw new IOException("模拟异常");
}
调用方法验证:
@Test
public void test02() throws IOException {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String name = format.format(new Date());
User user = new User();
user.setName(name);
user.setAge(10);
userService.save(user);
}
上面两个事务方法,只有第一个的事务会回滚,第二个不会,导致数据仍然被插入
解决办法:
-
声明rollBackFor类型为Exception,因为Exception是所有异常类的父类,可以对方法内抛出的任何异常进行捕获
(注:Exception是所有异常类的父类,Error是所有错误类的父类,两者同时又是Throwable类的子类)
-
不声明rollBackFor类型的话,就要手动在方法内捕获异常,然后根据业务抛出RuuntimeException
另:想要看类的继承关系,在IDEA中点击类名,CTRL+H会看到继承结构,还能全选SHOW DIAGRAM转成UML图来看
二、业务方法内try-catch
原因: 异常在业务方法内被捕获了,导致Spring无法捕获异常,自然无法进行回滚
@Transactional
public void save03(User user) {
userMapper.insert(user);
try {
throw new RuntimeException("模拟异常");
} catch (Exception e) {
e.printStackTrace();
}
}
解决方法:
-
抛出异常让Spring捕获,从而进行回滚
-
不依赖Spring,手动进行回滚操作
@Transactional public void save04(User user) { userMapper.insert(user); try { throw new RuntimeException("模拟异常"); } catch (Exception e) { e.printStackTrace(); // 手动进行回滚 TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); } }
三、切面顺序
原因: 自定义切面和事务切面的执行顺序引起异常捕获冲突
自定义切面不声明切面优先级,那么优先级是和事务注解(@Transactional)的切面优先级一样,并且要注意的是,Spring会优先扫描事务相关,所以事务切面会先执行,然后执行自定义切面,加入自定义切面内进行了手动catch异常,那么事务切面调用的方法将不会有异常抛出,自然无法回滚。
上面这段话有点绕,因为我们需要掌握AOP切面的基础知识才可以搞懂其中的逻辑关系,这里写一个例子,声明两个切面,他们的工作内容都是一样的:
复现问题:
@Test
public void test06() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String name = format.format(new Date());
User user = new User();
user.setName(name);
user.setAge(10);
userService.save06(user);
}
@Aspect
@Component
public class YAopAspect {
@Around(value = "execution(public * com.cc.mbg.UserService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) {
System.out.println("切面YYYY");
Object result = null;
try {
result = joinPoint.proceed();
}catch (Throwable throwable) {
System.out.println("切面YY捕获到了异常");
throwable.printStackTrace();
}
return result;
}
}
@Aspect
@Component
public class ZAopAspect {
@Around(value = "execution(public * com.cc.mbg.UserService.*(..))")
public Object around(ProceedingJoinPoint joinPoint) {
System.out.println("切面ZZZZ");
Object result = null;
try {
result = joinPoint.proceed();
}catch (Throwable throwable) {
System.out.println("切面ZZ捕获到了异常");
throwable.printStackTrace();
}
return result;
}
}
userService的方法:
public void save(User user) {
userMapper.insert(user);
throw new RuntimeException("模拟异常");
}
Y和Z这两个切面,都会在执行UserService方法的时候触发,并且因为他们都没有声明切面优先级,所以他们的切面优先级都是一样的:Integer.MAX_VALUE,又因为优先级相同,所以会根据切面字母顺序来执行,即YAopAspect先执行,然后是ZAopAspect
现在说一下他们的触发顺序:
- 调用userService.save(),在方法实际调用前触发了AOP切面,即此时方法尚未调用
- Y切面先执行around,输出:”切面YYYY”
- Y切面继续执行,一直到joinPoint.proceed();
- Y切面暂停,Z切面执行around,输出“切面ZZZZ”
- Z切面继续执行,try-catch捕获到userService.save()抛出的异常,输出:“切面ZZ捕获到了异常”,然后return,此时Z切面代码执行完毕
- Y切面继续执行:result = joinPoint.proceed();因为无异常抛出(被Z切面捕获了),所以跳过catch代码块,return,此时Y切面代码执行完毕
通过分析可以知道,我们自定义切面的时候,需要注意手动异常捕获,因为可能会影响到其他切面对抛出异常的处理。
由上应该就能理解,@Transactional注解的切面,为什么会有可能被自定义切面所影响,导致事务回滚失效了。
科普一下切面优先级:
- AOP不指定Order,那么默认值是Integer.MAX_VALUE
- Order越小,越优先执行
- 当Order相同时,按照切面名称字母的顺序来执行
解决方法:
- 手动抛出异常,让Spring捕获到
- 手动回滚数据
四、事务方法的访问修饰符非public
原因: Spring为方法对象创建代理,添加切面的条件是方法必须是public的,并且不能是private、static、final的原因,可以看我的这篇文章看动态代理相关知识
解决方法: 使用public修饰方法,且注意不要用static和final修饰
五、使用this调用本类事务方法
原因: Spring的声明式事务是基于动态代理实现的,即Spring会创建一个代理对象去执行事务方法,而this是指向本身,即对象本身去执行方法,不是代理对象去执行,所以事务不会生效
复现问题:
在Service中编写代码,内部去调用事务方法:
public void save06(User user) {
this.saveByInside(user);
}
@Transactional
public void saveByInside(User user) {
userMapper.insert(user);
throw new RuntimeException("模拟异常");
}
调用测试:
@Test
public void test06() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String name = format.format(new Date());
User user = new User();
user.setName(name);
user.setAge(10);
userService.save06(user);
}
此时事务不生效,数据仍被插入
解决办法:
-
通过注入自己来获得代理对象,再进行调用,不过这种方法Spring官方不提倡,建议是使用新的类
改造后:
@Autowired private UserService self; public void save06(User user) { self.saveByInside(user); } @Transactional public void saveByInside(User user) { userMapper.insert(user); throw new RuntimeException("模拟异常"); } -
通过ApplicationContext从容器中获取代理对象,再进行调用
@Autowired ApplicationContext applicationContext; public void save06(User user) { UserService bean = applicationContext.getBean(UserService.class); bean.save06(user); } @Transactional public void saveByInside(User user) { userMapper.insert(user); throw new RuntimeException("模拟异常"); }
六、多线程调用导致的事务不一致
复现问题:
@Autowired
private PriceService priceService;
@Transactional
public void save09(User user) {
userMapper.insert(user);
new Thread(() -> {
priceService.save();
});
}
原因: save09方法中调用了priceService的事务方法save(),因为两个事务不在同一个线程中,所以获取的数据库连接不一样,也就是两个不同的事务,假如priceService.save()抛出了异常,save09()是不能回滚的,只有在同一个数据库连接中才能同时提交和回滚。
解决办法:
使用线程安全的AtomicBoolean作为事务执行状态标记,等事务方法内所有线程执行完毕后根据该状态判断是否需要回滚:
AtomicBoolean isError = new AtomicBoolean(false);
...
if (isError) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
七、数据库引擎不支持事务
比如MySQL的MyISAM引擎不支持事务
八、错误的事务传播
在使用@Transactional注解时,是可以指定propagation参数的,该参数的作用是指定事务的传播特性,Spring目前支持7中传播特性:
- REQUIRED(默认):如果当前上下文中存在事务,那么加入该事务,如果不存在事务,则创建一个事务
- SUPPORTS:如果当前上下文中存在事务,那么加入该事务,否则使用非事务方式执行
- MANDATORY:如果当前上下文存在事务,则抛出异常
- REQUIRES_NEW:每次都会新建一个事务,并且将上下文中的事务挂起,执行完当前新建事务后,上下文事务再恢复执行
- NOT_SUPPORTED:如果当前上下文存在事务,则挂起当前事务,然后新的方法再没有事务的环境中执行
- NEVER:如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行
- NESTED:如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务
九、嵌套事务回滚
原因: 在事务方法内有两个修改操作,只希望回滚其中一个,但因为事务导致两个都回滚了
复现问题:
新建一个priceService模拟嵌套事务:
@Service
public class PriceService {
...
@Transactional
public void save() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss");
String name = format.format(new Date());
User user = new User();
user.setName(name);
user.setAge(10);
userMapper.insert(user);
// 这里为可插拔,需要的时候再抛出异常
// throw new RuntimeException("模拟异常");
}
}
然后是主函数:
@Transactional
public void save10(User user) {
userMapper.insert(user);
priceService.save();
}
解决方法: 不使用@Transactinal注解或者使用事务传播特性
我们来看看嵌套事务的各种场景:
场景一:
@Transactional
public void save10(User user) {
userMapper.insert(user);
priceService.save();
if (true) {
throw new RuntimeException("模拟异常");
}
}
主函数插入数据成功,嵌套函数插入数据成功,但主函数抛出异常,此时所有数据都会回滚
场景二:
@Transactional
public void save10(User user) {
userMapper.insert(user);
if (true) {
throw new RuntimeException("模拟异常");
}
priceService.save();
}
主函数插入数据成功,嵌套函数未执行前就抛出异常,所以嵌套函数没有插入数据,又因为主函数异常,所以主函数数据回滚
场景三:
嵌套函数内抛出异常:
@Service
public class PriceService {
...
@Transactional
public void save() {
...
userMapper.insert(user);
throw new RuntimeException("模拟异常");
}
}
@Transactional
public void save10(User user) {
userMapper.insert(user);
priceService.save();
}
同场景一,数据都会回滚。
嵌套事务场景下,如何回滚指定事务
复用上面的三个场景,出现异常时,都是回滚所有数据,现在我们希望回滚指定事务,
比如主函数异常,则回滚主函数的数据修改,不回滚嵌套函数;
或者嵌套函数异常,则回滚嵌套函数的数据修改,不回滚主函数;
我们可以使用@Transactional注解的事务传播来做到这点:
-
主函数回滚,嵌套函数不回滚(前提是主函数抛出异常)
将嵌套函数的事务传播设置为:
@Transactional(propagation = Propagation.NOT_SUPPORTED)意思是当前上下文存在事务时,挂起当前事务,然后方法在没有事务的环境下执行
-
主函数不回滚,嵌套函数回滚(前提是嵌套函数抛出异常)
主函数不使用@Transactional注解即可,则主函数没有开启事务,而嵌套函数是有事务的,所以会回滚
十、编程式事务
上面使用@Transactional注解的方式叫做声明式事务,除此之外,我们还可以使用Spring提供的另一种创建事务的方式,即通过手动编写代码实现事务,这种方式叫编程式事务
示例:
@Autowired
private TransactionTemplate transactionTemplate;
public void save11(User user) {
// 带返回值的事务管理
Object execute = transactionTemplate.execute(new TransactionCallback<Object>() {
@Override
public Object doInTransaction(TransactionStatus transactionStatus) {
userMapper.insert(user);
priceService.save();
return null;
}
});
// 不带返回值的事务管理
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
public void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
userMapper.insert(user);
priceService.save();
}
});
}
编程式事务的好处:
- 可以避免Spring AOP导致的事务失效问题
- 能够以更小的粒度来控制事务范围
十一、事务中使用锁失效
原因: 锁的释放和事务的提交时间不一致,导致在并发场景下事务失效
复现问题:
假设用户1的余额只有100
@Transactional
public synchronized void save12(int userId) {
User user = userMapper.selectByPrimaryKey(userId);
// 用户有足够的余额可以扣款
if (user.getPrice() >= 100.f) {
user.setPrice(user.getPrice() - 100);
userMapper.updateByPrimaryKey(user);
} else {
throw new RuntimeException("余额不足");
}
}
这个例子并不是很恰当,但是也能体现出问题,synchroinzed给事务方法上了锁,按道理来说用户扣款只会触发一次,但是我们通过JMETER并发测试可以发现,方法给用户扣了多次款(其实和商品超卖是一个道理),这是为什么呢?
我们已经知道了AOP是基于动态代理实现的,该事务方法是由代理对象去执行,并且提交事务,可在上面的示例中,synchronized锁,锁的是事务方法,该方法执行完后锁就释放了,这时候在高并发场景下,有可能会出现锁释放了,但事务还没有提交的情况,这时候就会有另一条线程进入了这个事务方法,并且去执行业务逻辑,导致用户多次扣款(或者商品超卖)现象的出现。
额外说明:事务方法执行前,是先开启事务,再加的锁
解决办法:
在调用事务方法前加锁
public synchronized void save12(int userId) {
save12Operation(userId);
}
@Transactional
public void save12Operation(int userId) {
User user = userMapper.selectByPrimaryKey(userId);
// 用户有足够的余额可以扣款
if (user.getPrice() >= 100.f) {
user.setPrice(user.getPrice() - 100);
userMapper.updateByPrimaryKey(user);
} else {
throw new RuntimeException("余额不足");
}
}