文章参考 几行烂代码,用错 Transactional,我赔了16万
1. MySQL 事务
MySQL 事务主要用于处理操作量大,复杂度高的数据。比如说,在人员管理系统中,你删除一个人员,你既需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!
- 在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
- 事务处理可以用来维护数据库的完整性,保证成批的 SQL 语句要么全部执行,要么全部不执行。
- 事务用来管理 insert,update,delete 语句
一般来说,事务是必须满足4个条件(ACID)::原子性(Atomicity,或称不可分割性)、一致性(Consistency)、隔离性(Isolation,又称独立性)、持久性(Durability)。
-
原子性:一个事务(transaction)中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
-
一致性:在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设规则,这包含资料的精确度、串联性以及后续数据库可以自发性地完成预定的工作。
-
隔离性:数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。
-
持久性:事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
2. mysql事务的隔离级别
3 解决方案1——将锁提到事务外面
加上@Transactional注解,将锁提到事务外面,没有超卖
@PostMapping("/buyLock")
public String buyLock(Integer quantity,Long skuId){
lock.lock();
try {
return buyLock1(quantity,skuId);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
return "购买失败";
}
@Transactional(rollbackFor = Exception.class)
public String buyLock1(Integer quantity,Long skuId){
PmsSkuStock skuStock = skuStockMapper.selectByPrimaryKey(skuId);
int left = skuStock.getStock() - skuStock.getLockStock();
if (left < quantity){
return "库存不足";
}
int s = skuStockMapper.updateLockStock(skuId,quantity);
if (s == 1){
return "购买成功";
}
return "购买失败";
}
如果是高并发的情况下,假设真的就有多个线程同时调用 func 方法。
要保证一定不能出现超卖的情况,那么就需要事务的开启与提交能完整的包裹在 lock 与 unlock之间。
显然事务的开启一定是在 lock 之后的。
故关键在于事务的提交是否一定在 unlock 之前?
如果事务的提交在 unlock 之前,没有问题。
因为事务已经提交了,代表库存一定减下来了,而这个时候锁还没释放,所以,其他线程也进不来。
等 unlock 之后,再进来一个线程,执行查询数据库的操作,那么查询到的值一定是减去库存之后的值。
但是,如果事务的提交是在 unlock 之后,那么有意思的事情就出现了,你很有可能发生超卖的情况。
上面的图就变成了这样的了,注意最后两个步骤调换了:
举个例子。
假设现在库存就只有一个了。
这个时候 A,B 两个线程来请求下单。
A 请求先拿到锁,然后查询出库存为一,可以下单,走了下单流程,把库存减为 0 了。
但是由于 A 先执行了 unlock 操作,释放了锁。
B 线程看到后马上就冲过来拿到了锁,并执行了查询库存的操作。
注意了,这个时候 A 线程还没来得及提交事务,所以 B 读取到的库存还是 1,如果程序没有做好控制,也走了下单流程。
哦豁,超卖了。
所以,再次重申问题:
在上面的示例代码的情况下,如果事务的提交在 unlock 之前,是没有问题的。但是如果在 unlock 之后是会有问题的。
4. 解决方法2:锁+串性事务
spring事务默认使用mysql默认的事务隔离级别,是可重复读,将spring事务级别设置成序列化,没有超卖
@PostMapping("/buyLock")
@Transactional(rollbackFor = Exception.class,isolation = SERIALIZABLE)
public String buyLock(Integer quantity,Long skuId){
lock.lock();
try {
PmsSkuStock skuStock = skuStockMapper.selectByPrimaryKey(skuId);
int left = skuStock.getStock() - skuStock.getLockStock();
if (left < quantity){
return "库存不足";
}
int s = skuStockMapper.updateLockStock(skuId,quantity);
if (s == 1){
return "购买成功";
}
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
return "购买失败";
}
优缺点:
事务隔离级别为串行化时,读写数据都会锁住整张表。
当读写锁出现冲突的时候,后面来的事务就排队等着,如果不追求性能的场景,这个操作也是可以的。