我的代码逻辑如下:
@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;
}
从该接口中可以看到如下:
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类)所有父类方法的调用,顺势织入横切逻辑。
可参考:【动态代理】CGLIB 动态代理的使用及原理_sco5282的博客-CSDN博客_cglib动态代理使用
解决方案:1、将方法改为public; 2、修改TansactionAttributeSource,将publicMethodsOnly改为false;3、开启 AspectJ 代理模式
失效原因: 因为spring事务是用动态代理实现的,因此如果方法使用了final修饰,则代理类无法对目标方法进行重写,植入事务功能
方法是static
失效原因: 原因和final一样
解决方案:使用InnoDB引擎
解决方案:1、将异常原样抛出; 2、设置TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
解决方案:配置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;
}