Reentrantlock锁+事务@Transaction
项目中遇到一个问题。对售出商品业务的代码加上该锁,保证不能超卖。
首先分析一下,保证多线程的并发安全,
1、引入锁Reentrantlock,2 、开启spring的事务管理,保证出现异常进行事务回滚。
这一个开发设计的代码
代码业务流程
这段代码 库存 10个 执行结果 卖出 14个,直接血亏4个 没有货可就是欺诈消费者咱可担待不起!!!!
package com.szj.videoblog.articleservice.service.impl;
import com.szj.videoblog.articleservice.mapper.StockMapper;
import com.szj.videoblog.articleservice.mapper.StockOrderMapper;
import com.szj.videoblog.articleservice.model.entity.Stock;
import com.szj.videoblog.articleservice.model.entity.StockOrder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.concurrent.locks.ReentrantLock;
@Service
@Slf4j
public class StockServiceImpl {
ReentrantLock lock = new ReentrantLock();
@Resource
private StockMapper stockMapper;
@Resource
private StockOrderMapper stockOrderMapper;
@Transactional
public void lock() {
//上锁
lock.lock();
try {
//查库存
Stock stock = stockMapper.selectById(1L);
if (stock.getStockNumber() > 0) {
log.info("下单");
//下单
StockOrder stockOrder = new StockOrder();
stockOrder.setName("平板");
stockOrderMapper.insert(stockOrder);
//减库存
Stock stock1 = new Stock();
stock1.setId(1L);
stock1.setStockNumber(stock.getStockNumber() - 1);
stockMapper.updateById(stock1);
} else {
//商品售空
log.info("商品售空");
}
} finally {
//解锁
lock.unlock();
}
}
}
我贴一下我的订单表,这是血淋淋的超卖了4个!!是哪里出错了,我这代码写没错啊。锁也加了,事务也加了,为什么会超卖啊。我直接告诉你答案,spring的@Transaction 事务是在方法unlock之后提交的,总结方法结束后提交事务。所有必然是先解锁这里就出现了问题。
一、首先分析spring事务是什么时候开启,又什么时候结束。
根据spring的事务源码发现是在执行到第一个操作 InnoDB 表的语句,事务才算是真正启动,
很显然是先加锁lock.lock(); 执行查询库存触发第一个sql语句,此时任务开启。
流程: 第一步,是先加锁 —> 执行sql开启spring事务;
二、那么spring事务什么时候结束呢。
在上面的示例代码的情况下那么事务的提交到底是在 unlock 之前还是之后呢?
根据我测试 100个线程执行下单发现超卖了,从而发现spring事务是在方法结束之后提交,
那么必然是先解锁-->>在提交spring事务
三、超卖的原因:
这里就有一个bug A线程解锁之后此时事务还没有被提交,B线程获取到锁查到库存,此时查的还是旧的库存数,执行下单流程导致超卖。
小明说:我还有点深信不疑,难道敲了一年的代码我才发现这个锁和事务存在这个bug,我不信,我要换锁,我要用redis的分布式锁试试。
Redisson 使用这个组件,简单粗暴 分布式锁。 very 好用。代码我就不贴了。
<!--大家也可以单独引入Redisson依赖 ->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.13.6</version>
</dependency>
分布式锁的使用 案例自己把业务改一下
public void testLockOne(){
try {
RLock lock = redissonClient.getLock("bravo1988_distributed_lock");
log.info("testLockOne尝试加锁...");
lock.lock();
log.info("testLockOne加锁成功...");
log.info("testLockOne业务开始...");
TimeUnit.SECONDS.sleep(5);
log.info("testLockOne业务结束...");
lock.unlock();
log.info("testLockOne解锁成功...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
经过我测试,一开始竟然被他蒙混过了,开启了100个线程 竟然没有超卖,说明情况很理想。没有出现获取锁之后,拿到就得库存值。
此时我就去问了分享这个bug的博主。博主让我在finally 里 执行unlock,在延迟一秒试试。
延迟1秒的原因:测试 先解锁 延迟1秒 判断 spring的事务是不是在方法执行结束之后提交的。
果然 :延迟1秒后,超卖了,那么又一次说明,spring事务的提交是在方法之后,所以此时A线程执行的sql 并没有被提交到mysql上,从而库存没有被减一。B线程在解锁后的1秒内 读取到的库存是旧值。此时就多卖了一个。
小明说:又多卖了一个,假一赔三,老铁,;老板需要这个场景打折10个苹果电脑,我却卖了14个,我还要补差价太难了。
解决方案
第一种 :手动提交事务。自己开启事务,自己在提交事务。
核心代码 在 解锁unlock之前提交事务。
第二种:既然是方法之后提交事务,
那么用A方法(负责上锁解锁) 调用B方法 (负责处理减库存操作)
测试流程:
正好是个10个订单记录 ,此时代码用的锁依然是 Reentrantlock
最好补充一句 @Transaction 在同一个service中 多个方法声明 Transaction 会出现不生效,这点下次再分析,可以百度自己注意一下。
这里只讲了spring的事务 ,mysql事务隔离级别默认RR 不可重复读,当查询库存的保证了读取的已提交事务 库存数。(mvcc会生成一个事务id 有版本号,查询大于当前版本号)保证了重复查询库存数都不会改变,这里就是Mvcc知识点,学习了,大致理解 mvcc 解决了以前读写要上锁的问题,解决了并发阻塞问题。
还有今天测试,mysql inndb下 当时我在思考假设 A线程 B 线程事务同时修改一条数据,
mysql执行流程如下 此时出现并发,inndb引擎 支持行锁,这时候A修改一条记录,但是未提交,B也去修改这条记录的时候会发生阻塞,这就是所谓的行锁,(也叫做排它锁);只有当A提交了事务,B的 update语句才会被执行,B提交事务,最终结果是 最后一次事务的更新值。
这里分享下,mysql 阻塞时间默认是50s 这个可自行修改,如果B事务被则塞超过50s 此时B事务提交不会生效。