高并发场景下用户下单,存在如下所示的超卖问题,其产生的主要原因是一个线程刚读出库存值,还没进行修改时,另一个线程也读出来该库存值,从而导致这两个线程在进行下单时,对同一个值减了1。
1. 悲观锁
认为线程安全问题一定会发生,线程串行执行
2. 乐观锁
认为线程安全问题不一定会发生,在更新数据时判断有没有线程对数据做了修改
难点在于判断数据是否被修改过,判断方式有:
1. 版本号法
给数据加一个版本号
先查询到版本号和库存,更新的时候判断版本号和查询时的版本号是否相同,不相同说明这段时间库存已经被更新过了,此时更新库存失败。
2. CAS 法
库存和版本号执行相同操作,用数据本身是否有变化进行判断。先查询库存数据的值,更新时再查一遍看看库存数据有没有变化,有变化就不更新了。
// 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).eq("stock", voucher.getStock()) // 乐观锁,保证当前线程执行的时候没有其他线程修改过库存.update();
解决失败率高的问题: 只要库存大于 0 就卖
// 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).gt("stock", 0) // 对乐观锁进行改进,只要防止出现负数就行.update();
只容许一人下一单,要对单个用户访问的高并发情况加锁
@Overridepublic Result seckillVoucher(Long voucherId) {// 1. 查询优惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判断秒杀是否开始if(voucher.getBeginTime().isAfter(LocalDateTime.now())){return Result.fail("秒杀还未开始");}// 3. 判断秒杀是否已经结束if(voucher.getEndTime().isBefore(LocalDateTime.now())){return Result.fail("秒杀已经结束");}// 4. 判断库存是否充足if(voucher.getStock() < 1){return Result.fail("库存不足");}// 线程安全 —— 先获取锁,完成事务,释放锁Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) { // 对用户 id 加锁,保证字符串值相同就会被锁定IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy(); // 拿到当前对象的代理对象return proxy.createVoucherOrder(voucherId); // 当前没有事务功能,代理对象才有事务功能}}@Transactional// 实现一人一单 —— 悲观锁 —— 以用户 id 加锁 —— 处理同一个用户的并发安全问题,防止一个用户并发买很多单public Result createVoucherOrder(Long voucherId){Long userId = UserHolder.getUser().getId();// 查询订单int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();if (count > 0) {return Result.fail("您已经买过一次了,不能再买了");}// 5. 扣除库存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId)//.eq("stock", voucher.getStock()) // 乐观锁,保证当前线程执行的时候没有其他线程修改过库存.gt("stock", 0) // 对乐观锁进行改进,只要防止出现负数就行.update();if (!success) {return Result.fail("库存不足");}// 6. 创建订单VoucherOrder voucherOrder = new VoucherOrder();// 订单的参数 —— 订单 id \ 代金券 id \ 用户 id// 生成唯一 idlong id = redisIdWorker.nextId("order");voucherOrder.setId(id);voucherOrder.setVoucherId(voucherId);voucherOrder.setUserId(1L);save(voucherOrder);return Result.ok();}
导入依赖:
org.aspectj aspectjweaver
加入允许暴露代理对象的依赖:
@EnableAspectJAutoProxy(exposeProxy = true) // 表示开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {public static void main(String[] args) {SpringApplication.run(HmDianPingApplication.class, args);}}