欢迎关注公众号(通过文章导读关注),发送笔记
可领取 Redis、JVM 等系列完整 pdf!
【11来了】文章导读地址:点击查看文章导读!
当使用 SpringBoot 进行项目开发,如果需要使用事务的话,只需要通过在方法上添加注解 @Transactional
就可以开启该方法的事务执行
但是如果不正确地使用事务,会导致 SpringBoot 中的事务失效,如果没及时发现,可能导致严重的生产问题!
因此,在使用事务之前,需要了解事务在哪些场景下会失效,否则,如果事务失效可能会导致 数据不一致
问题的出现
在学习事务失效之前,首先了解一下 事务的传播类型
,在事务处理中,事务的传播类型(Propagation)是指在多个事务方法相互调用的情况下,事务如何进行传播和协调的方式。Spring 提供了多种事务传播类型,以便为不同的业务需求提供灵活的事务管理机制:
//如果有事务, 那么加入事务, 没有的话新建一个(默认)
@Transactional(propagation=Propagation.REQUIRED)
//容器不为这个方法开启事务
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//不管是否存在事务, 都创建一个新的事务, 原来的挂起, 新的执行完毕, 继续执行老的事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
//必须在一个已有的事务中执行, 否则抛出异常
@Transactional(propagation=Propagation.MANDATORY)
//必须在一个没有的事务中执行, 否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.NEVER)
//如果其他bean调用这个方法, 在其他bean中声明事务, 那就用事务, 如果其他bean没有声明事务, 那就不用事务
@Transactional(propagation=Propagation.SUPPORTS)
上边的这个是 Spring 中提供的事务的传播类型设置,还可以使用 isolation
设置底层数据库的事务隔离级别,这些就很熟悉了,在 MySQL 中就已经学过很多了:
// 读取未提交数据(会出现脏读, 不可重复读) 基本不使用
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 读取已提交数据(会出现不可重复读和幻读)
@Transactional(isolation = Isolation.READ_COMMITTED)
// 可重复读(会出现幻读) MySQL默认
@Transactional(isolation = Isolation.REPEATABLE_READ)
// 串行化
@Transactional(isolation = Isolation.SERIALIZABLE)
那么 Spring 是如何通过 @Transactional 为方法开启事务的呢?
底层原理就是通过 动态代理
技术,Spring 会创建一个代理对象,当执行被注解标注的方法时,是通过代理对象来执行的
在代理对象中,会在方法执行的前开启事务,并且当方法执行成功时将事务提交;如果方法抛出异常,代理对象对事务进行回滚
既然需要 @Transactional 注解开启事务,那么 Spring 就需要可以扫描到这个注解,也就是 Spring 必须将 @Transactional 标注的方法所在类注册为 Spring 的 Bean,之后才可以扫描到这个注解,并且创建代理对象,如下是 错误示例
:
没有给 Service 添加 @Service 注解,Spring 没有管理到这个类,自然也就无法开启事务
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void update(Goods goods) {
// ...
}
}
如果一个方法被定义为 private了,那么同样事务会失效,因为在代理模式中只可以对 公共接口暴露的方法
进行代理拦截并且添加事务管理逻辑,如下是 错误示例
:
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void update(Goods goods) {
// ...
}
}
如果是在同一个类中,比如 GoodsServiceImpl 类中有两个方法 A 和 B,B 是事务方法,如果在 A 方法中直接调用 B
,会导致事务失效
这是因为,A 直接去调用 B,并没有走到动态代理对象的逻辑,也就是没有被动态代理所拦截添加事务操作,事务自然就失效了,错误示例如下:
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Override
private void A(Goods goods) {
update(goods);
}
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void update(Goods goods) {
// ...
}
}
如果在事务方法中,爆出异常,但是通过 try-catch
将异常捕捉,导致动态代理并没有感知到事务方法所出现的异常,所以事务也就没有办法回滚,导致事务失效,错误示例如下:
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void update(Goods goods) {
try {
...
} catch (Exception e) {
// 自定义逻辑吞掉异常
log.error("捕获异常: {}", e);
}
}
}
对下边这个例子来说,在 A 方法中,通过多线程调用了 sendMsg()
方法,但是执行 A() 的线程和执行 sendMsg() 的线程并不是同一个线程
而 Spring 中的事务是通过 ThreadLocal 保证线程安全的,将事务和当前线程绑定,那么多个线程就会导致事务失效,如果 sendMsg() 方法执行失败了,会导致 A 方法也无法回滚
@Service
public class GoodsServiceImpl extends ServiceImpl<GoodsMapper, Goods> implements GoodsService {
@Transactional
private void A(Goods goods) {
new Thread(() -> {
sendMsg();
}).start();
}
}
@Service
public class MessageServiceImpl{
@Transactional
private void sendMsg() {
// ...
}
}