**
通过看大神的文章,写下我的心得体会,留给自己今后参考吧–@Transactional注解
**
文章通过引用商品售出,减库存,生成订单信息来引入了@Transactional这个注解
他说他有一个 func 方法,这个方法里面干了两件事:
1.先查询数据库里面的商品库存。
2.如果还有库存,那么对库存进行减一操作,模拟商品卖出。
对于第二件事,提问的同学其实写了两个操作在里面,所以我再细分一下:
2.1 对库存进行减一操作。
2.2 在订单表插入订单数据。
很显然,这两个操作都会对数据库进行操作,且应该是应该原子性的操作。
所以,在方法上加了一个 @Transactional 注解。
接着,为了解决并发问题,又将业务用lock包裹起来,非常完美!
那么问题来了,mysql数据库默认的的隔离机制是可重复读
事务的开始是在lock之后,那么事务的提交呢,是在unlock之前还是之后呢?
如果之前的话:
加锁–>查询库存–> 库存减一–>生成订单–>解锁
但是,如果事务的提交是在 unlock 之后,那么有意思的事情就出现了,你很有可能发生超卖的情况。直接上图
这个例子就是:
AB两个人来抢库存,
假如只剩1件商品,
A先进来,加锁,查询库存,1,减库存,事务还未提交,解锁,
此时,库存仍未1,
B抢到锁,查询库存,==1,
现在就导致了超卖的问题
事务开启
把数据库连接切换为手动提交。
那么事务的启动有哪些方式呢?
第一种:使用启动事务的语句,这种是显式的启动事务。比如 begin 或 start transaction 语句。与之配套的提交语句是 commit,回滚语句是 rollback。
第二种:autocommit 的值默认是 1,含义是事务的自动提交是开启的。如果我们执行 set autocommit=0,这个命令会将这个线程的自动提交关掉。意味着如果你只执行一个 select 语句,这个事务就启动了,而且并不会自动提交。这个事务持续存在直到你主动执行 commit 或 rollback 语句,或者断开连接。
很显然,在 Spring 里面采用的是第二种方式。
而上面的代码 con.setAutoCommit(false) 只是把这个链接的自动提交关掉。
事务真正启动的时机是什么时候呢?
前面说的 begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个操作 InnoDB 表的语句,事务才算是真正启动。
如果你想要马上启动一个事务,可以使用 start transaction with consistent snapshot 这个命令。需要注意的是这个命令在读已提交的隔离级别(RC)下是没意义的,和直接使用 start transaction 一个效果。
回到在前面的问题:什么时候才会执行第一个 SQL 语句?
就是在 lock 代码之后。
所以,显然事务的开启一定是在 lock 之后的。
这一个简单的“显然”,先给大家铺垫一下。
接下来,给大家上个动图看一眼,更加直观。
首先说一下这个 SQL:
select * from information_schema.innodb_trx;
不多解释,你只要知道这是查询当前数据库有哪些事务正在执行的语句就行。
最后,我们把目光转移到这个方法的注释上:
写这么长一段注释,意思就是给你说,这个参数我们默认是 ture,原因就是在某些 JDBC 的驱动中,切换为自动提交是一个很重的操作。
那么在哪设置的为 true 呢?
没看到代码,我一般是不死心的。
所以,一起去看一眼。
可以看到,默认确实是 true。
先给大家看一下我的代码
整个的流程是这样的:
1.先拿锁。
2.查询库存。
3.判断是否还有库存。
4.有库存则执行减库存,创建订单的逻辑。
5.没有库存则返回。
6.释放锁。
大神的案例是这个样子的:
在上面的示例代码的情况下,事务的提交在 unlock 之后。
原因在这里
前面我们聊事务开启的时候,说的是第 382 行代码。
然后 try 代码块里面执行的是我们的业务代码。
现在,我们要研究事务的提交了,所以主要看我框起来的地方。
首先 catch 代码块里面,392 行,看方法名称已经非常的见名知意了:
completeTransactionAfterThrowing 在抛出异常之后完成事务的提交。
你看我的代码,只是用到了 @Transactional 注解,并没有指定异常。
那么问题就来了:
Spring 管理的事务,默认回滚的异常是什么呢?
如果你不知道答案,就可以带着问题去看源码。
如果你知道答案,但是没有亲眼看到对应的代码,那么也可以去寻找源码。
如果你知道答案,也看过这部分源码,温故而知新。
先说答案:默认回滚的异常是 RuntimeException 或者 Error。
如果异常类型是 RuntimeException 或者 Error 的子类,那么就返回 true,即需要回滚,调用 rollback 方法:
如果返回为 false,则表示不需要回滚,调用 commit 方法:
这个代码块里面,try 我们也聊了,catch 我们也聊了。
就差个 finally 了。
我看网上有的文章说 finally 里面就是 commit 的地方。
错了啊,老弟。
这里只是把数据库连接给重置一下。
Spring 的事务是基于 ThreadLocal 来做的。在当前的这个事务里面,可能有一些隔离级别、回滚类型、超时时间等等的个性化配置。
不管是这个事务正常返回还是出现异常,只要它完事了,就得给把这些个性化的配置全部恢复到默认配置。
所以,放到了 finally 代码块里面去执行了。
真正的 commit 的地方是这行代码:
那么问题又来了:
走到这里来了,事务一定会提交吗?
话可别说的那么绝对,兄弟,看代码:
org.springframework.transaction.support.AbstractPlatformTransactionManager#commit
在 commit 之前还有两个判断,如果事务被标记为 rollback-only 了,还是得回滚。
而且,你看日志。
我这事务还没提交呢,锁就被释放了?
现在我们知道问题的原因了。解决方案其实都呼之欲出了嘛。正确的使用锁,把整个事务放在锁的工作范围之内:
上面的代码事务是失效的,正确的如下