【黑马点评-3秒杀优惠券】三、悲观锁 synchronized 解决一人一单 + 单体秒杀初始逻辑
解决一人一单的逻辑
乐观锁
- 乐观锁需要判断数据是否修改,而当前是判断当前是否存在,所以无法像解决库存超卖一样使用CAS机制,
- 但是可以使用版本号法,但是版本号法需要新增一个字段,
悲观锁
所以这里为了方便,就直接演示使用悲观锁解决超卖问题
加悲观锁的逻辑
1. 使用Mybatis-Plus判断各种字段
开始时间等
2. 从ThreadLocal中获取userId
UserHolder里的属性
3. 使用synchronized加锁
synchronized 关键字用于控制对共享资源的访问,确保在同一时间只有一个线程可以执行被同步的代码块或方法。
在这个代码片段中,synchronized 用于同步一个代码块,锁对象是 userId.toString().intern() 返回的字符串。这意味着对于相同的 userId,在同一时间只有一个线程可以执行该代码块,从而防止同一用户重复下单。
String.intern() 的作用
intern() 方法返回字符串常量池中的引用。通过对 intern() 返回的字符串进行同步,可以确保对于相同的 userId,使用的是同一个锁对象,从而实现同步控制。
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(userId, voucherId);
}
4. 避免事务失效
- 失效原因:Spring 的事务管理是基于 AOP(面向切面编程)实现的。当一个方法被调用时,Spring 会通过代理对象来增强该方法,以实现事务管理等功能。然而,如果在同一个类内部,一个非事务方法直接调用另一个标注了 @Transactional 的方法,这种调用不会经过代理对象,导致事务注解失效。
- 以使用 AopContext.currentProxy() 方法获取当前的代理对象,并通过代理对象调用目标方法,从而确保事务注解生效。
5. 调用的create优惠券订单函数逻辑
-
VoucherOrder实体对应的tb_voucher_order表
名称 注释 id 主键 user_id 下单的用户id voucher_id 购买的代金券id pay_type 支付方式 status 订单状态 create time 下单时间 pay_time 支付时间 use time 核销时间 refund time 退款时间 update time 更新时间 -
用持久层接口count方法,返回符合该用户id条件的记录总数
-
如果小于1,则借助包装实体,更新记录
-
创建VoucherOrder实体并更新
public Result createVoucherOrder(Long userId, Long voucherId) {
// 判断当前用户是否是第一单
int count = this.count(new LambdaUpdateWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, userId));
if (count >= 1) {
return Result.fail("用户已购买");
}
boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>()
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
.setSql("stock=stock-1"));
if (!flag) {
throw new RuntimeException("秒杀券扣减失败");
}
// 秒杀成功,创建对应的订单,保存到数据库中
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("seckill_voucher_order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
flag = this.save(voucherOrder);
if (!flag) {
throw new RuntimeException("创建秒杀券订单失败");
}
return Result.ok(orderId);
}
重点
锁的范围尽量小。synchronized尽量锁代码块,而不是方法,锁的范围越大性能越低
锁的对象一定要是一个不变的值。我们不能直接锁 Long 类型的 userId,每请求一次都会创建一个新的 userId 对象,synchronized 要锁不变的值,
- 所以我们要将 Long 类型的 userId 通过 toString()方法转成 String 类型的 userId,toString()方法底层(可以点击去看源码)是直接 new 一个新的String对象,显然还是在变,
- 所以我们要使用 intern() 方法从常量池中寻找与当前 字符串值一致的字符串对象,这就能够保障一个用户 发送多次请求,每次请求的 userId 都是不变的,从而能够完成锁的效果(并行变串行)
我们要锁住整个事务,而不是锁住事务内部的代码。如果我们锁住事务内部的代码会导致其它线程能够进入事务,当我们事务还未提交,锁一旦释放,仍然会存在超卖问题
Spring的@Transactional注解要想事务生效,必须使用动态代理。Service中一个方法中调用另一个方法,另一个方法使用了事务,此时会导致@Transactional失效,所以我们需要创建一个代理对象,使用代理对象来调用方法。
示例代码
@Transactional
// 悲观锁(Pessimistic Lock)解决一人一单问题
public Result seckillVoucherPess(Long voucherId) {
// 1. 查询秒杀券
SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
return Result.fail("秒杀尚未开始");
}
if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
return Result.fail("秒杀已结束");
}
if (voucher.getStock() < 1) {
return Result.fail("库存不足");
}
Long userId = UserHolder.getUser().getId();
synchronized (userId.toString().intern()) {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(userId, voucherId);
}
}
@Override
public Result createVoucherOrder(Long userId, Long voucherId) {
// 判断当前用户是否是第一单
int count = this.count(new LambdaUpdateWrapper<VoucherOrder>().eq(VoucherOrder::getUserId, userId));
if (count >= 1) {
return Result.fail("用户已购买");
}
boolean flag = seckillVoucherService.update(new LambdaUpdateWrapper<SeckillVoucher>()
.eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
.setSql("stock=stock-1"));
if (!flag) {
throw new RuntimeException("秒杀券扣减失败");
}
// 秒杀成功,创建对应的订单,保存到数据库中
VoucherOrder voucherOrder = new VoucherOrder();
long orderId = redisIdWorker.nextId("seckill_voucher_order");
voucherOrder.setId(orderId);
voucherOrder.setUserId(UserHolder.getUser().getId());
voucherOrder.setVoucherId(voucherId);
flag = this.save(voucherOrder);
if (!flag) {
throw new RuntimeException("创建秒杀券订单失败");
}
return Result.ok(orderId);
}

浙公网安备 33010602011771号