问题分析
我的代码逻辑如下:
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED)
public synchronized boolean buy(Integer id) {
boolean b = false;
int stock = mapper.getStock(id);
if (stock > 0) {
System.out.println("库存为:" + stock);
b = mapper.updateStock(id, stock - 1);
}
return b;
}
虽然该方法加了锁,看似线程安全、人畜无害,但结果还是有可能会超卖,为啥呢?先来看看@Transactional注解的奥秘。
PlatformTransactionManager是spring处理事务的核心规范,它是一个接口:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition)
throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
从该接口中可以看到如下:
- TransactionDefinition:该接口里面规范了事务的隔离级别、传播级别、是否只读、超时时间等定义信息
- TransactionStatus:可以理解为就是一个事务,通过它可以获取事务的状态
- getTransaction()方法:根据TransactionDefinition获取一个事务对象
- commit():提交事务
- rollback():回滚事务
PlatformTransactionManager的实现类如下:
其中的DataSourceTransactionManager比较常用。
在PlatformTransactionManager上的getTransaction()打上断点,请求我们的方法:
-
进入AbstractPlatformTransactionManager类的getTransaction()上,@Transaction注解上设置的事务配置信息就是它传过来的参数,然后去校验配置、根据事务的隔离级别选择是否创建事务。
-
如果需要事务,则进入子类DataSourceTransactionManager的doBegin()方法,关键点。该方法首先会根据数据源获取数据库的connection,然后针对当前获取的connection,将当前会话的的事务开启方式改为手动提交。
注意此时事务还并未开始噢,还需要手动执行begin和start transaction这两个命令,才算开启事务
来验证一下:
-- 查看当前数据库有哪些事务存在
select * from information_schema.innodb_trx;
得到的结果为null,没有事务开启。
再看如下调用栈:
我们之前的入口方法getTransaction()其实是TransactionAspectSupport的invokeWithinTransaction()方法调用的。方法如下:
这里有个切面,可以理解为 try 里面就是在执行我们的业务代码逻辑,而try前面的create..方法就是准备好事务,时机成熟后就开启事务。
什么时候时机成熟了?请看下文
经过一定的步骤,我们从切面跑到了我们原本的代码逻辑,准备开始执行业务了。
此时还没有事务信息
继续走,当执行完数据的查询操作后,即涉及到数据库的语句后,事务就开起来了:
把我们的业务逻辑执行完后,回到刚刚的切面
1.顺着completeTransactionAfterThrowing()方法走,你会发现spring事务的默认回滚的异常是RuntimeException或者Error。使用instanceof判断的。
2.finally块中的cleanupTransactionInfo()方法并不是提交事务,而是恢复事务的默认行为(隔离级别、回滚类型等)。
3.commitTransactionAfterReturning()提交事务的方法不是一定提交,如果判断事务配置为只读,那么就会回滚。
由此我们就知道了spring事务的一个大致过程:
- 先设置事务的开启方式为手动
- 执行业务代码
- 涉及到数据库的增删改查操作时就立即开启一个事务
- 我们的业务代码执行完毕
- 如果中途有异常则回滚事务
- 否则,默认提交事务
最后可以分析出我们的业务逻辑中,获取锁的步骤是在开启事务之前,释放锁的操作也在提交事务之前
这就出现了一个问题,在释放锁和提交事务这一小块区间可能会引发线程安全问题。
比如:
线程A扣减库存为0了,然后释放锁,还没来得及提交事务,此时线程B突然冲过来,获取到锁,然后开启事务,查询库存,因为是不可重复读,所以线程B是读取不到线程A的修改的,它读取到的库存依旧充足,所以线程B也扣减库存,到最后也就超卖了。
解决方法
现在我们要避免之前的错误,正确的使用锁,把整个事务放在锁的工作范围之内:
//controller调用该方法,该方法间接的去调用我们的业务逻辑
public boolean director(Integer id){
synchronized (this){
return buy(id);
}
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ,propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {
boolean b = false;
int stock = mapper.getStock(id);
if (stock > 0) {
System.out.println("库存为:" + stock);
b = mapper.updateStock(id, stock - 1);
}
return b;
}
这样,就可以保证事务的提交一定是在 unlock 之前了。
no no no!
这样做,事务并不会生效。
如果此时事务能生效就可以保证这段代码是线程安全的,不会出现超卖问题。关于事务失效请看下文的解决方案:
事务的失效场景
CGLIB 原理:动态生成一个要代理类的子类,子类重写要代理的类的方法。在子类中采用方法拦截的技术拦截(MethodInterceptor类)所有父类方法的调用,顺势织入横切逻辑。
- 方法不是public权限修饰的,spring事务默认生效的方法权限都必须为public。
解决方案:1、将方法改为public; 2、修改TansactionAttributeSource,将publicMethodsOnly改为false;3、开启 AspectJ 代理模式
- 方法是final修饰的,final方法不能被重写
失效原因: 因为spring事务是用动态代理实现的,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能
-
方法是static
失效原因: 原因和final一样
- 数据库的存储引擎本身不支持事务,如MyISM。
解决方案:使用InnoDB引擎
- Service类没有被spring管理,没写@Service、@Component等注解。
- 异常被捕获,没有抛出方法外,该事务不会回滚。
解决方案:1、将异常原样抛出; 2、设置TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
- 异常类型和事务的rollbackFor不匹配,默认回滚的是RuntimeException和Error错误
解决方案:配置rollbackFor
- 调用自身方法
public boolean director(Integer id) {
synchronized (this) {
return this.buy(id); //或return buy(id);
}
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {
//省略
}
失效原因: Spring在扫描Bean的时候会自动为标注了@Transactional注解的类生成一个代理类(proxy),当有注解的方法被调用的时候,实际上是代理类调用的,代理类在调用之前会开启事务,执行事务的操作,但是同类中的方法互相调用,相当于this.B(),此时的B方法并非是代理类调用,而是直接通过原有的Bean直接调用,所以注解会失效。
解决方案:
1、注入自己来调用:
@Autowired
@Lazy //防止循环依赖
private ProductService service;
public boolean director(Integer id) {
synchronized (this) {
//不能使用this.调用
return service.buy(id);
}
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {
//省略
}
2、使用@EnableAspectJAutoProxy(exposeProxy = true) + AopContext.currentProxy(),通过获取代理对象调用
步骤:引入aspectjweaver依赖、启动类加@EnableAspectJAutoProxy(exposeProxy = true),暴露代理对象、获取当前代理对象调用
public boolean director(Integer id) {
synchronized (this) {
//获取当前代理类调用方法
return ((ProductServiceImpl)AopContext.currentProxy()).buy(id);
}
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {
//省略
}
- 错误的使用事务的传播机制,也会导致事务失效
顺便说下吧,看下文
事务的传播机制
事务传播机制主要用来描述由某一个事务传播行为修饰的方法被嵌套进另一个方法的事务中,该事务如何传播。这个概述可能不好理解,换句话就是当一个事务方法被另一个事务方法调用时,这个事务方法应该如何进行。
@Transaction(Propagation=XXX)
public void methodA(){
methodB();
//doSomething
}
@Transaction(Propagation=XXX)
public void methodB(){
//doSomething
}
全部的传播机制:
事务传播行为类型 | 解释说明 |
---|---|
Propagation_Required | 表示被修饰的方法必须运行在事务中。如果当前方法没有事务,则就新建一个事务;如果已经存在一个事务中,就加入到这个事务中。此类型是最常见的默认选择 |
Propagation_Supports | 表示被修饰的方法不需要事务上下文。如果当前方法存在事务,则支持当前事务执行;如果当前没有事务,就以非事务方式执行。 |
Propagation_Mandatory | 表示被修饰的方法必须在事务中运行。如果当前事务不存在,则会抛出一个异常。 |
Propagation_Required_New | 表示被修饰的方法必须运行在它自己的事务中。一个新的事务会被启动。如果调用者存在当前事务,则在该方法执行期间,当前事务会被挂起。 |
Propagation_Not_Supported | 表示被修饰的方法不应该运行在事务中。如果调用者存在当前事务,则该方法运行期间,当前事务将被挂起。 |
Propagation_Never | 表示被修饰的方法不应该运行事务上下文中。如果调用者或者该方法中存在一个事务正在运行,则会抛出异常。 |
Propagation_Nested | 表示当前方法已经存在一个事务,那么该方法将会在嵌套事务中运行。嵌套的事务可以独立与当前事务进行单独地提交或者回滚。如果当前事务不存在,那么其行为与Propagation_Required一样。 |
嵌套事务的概念就是内层事务依赖于外层事务。外层事务失败时,会回滚内层事务所做的动作。而内层事务操作失败并不会引起外层事务的回滚。
解决问题
有了上面对spring事务的分析,我们得出最后的解决超卖问题的方法为:
public boolean director(Integer id) {
synchronized (this) {
return ((ProductService)AopContext.currentProxy()).buy(id);
}
}
@Override
@Transactional(isolation = Isolation.REPEATABLE_READ, propagation = Propagation.REQUIRED)
public boolean buy(Integer id) {
boolean b = false;
int stock = mapper.getStock(id);
if (stock > 0) {
System.out.println("库存为:" + stock);
b = mapper.updateStock(id, stock - 1);
}
return b;
}