一、超卖问题
1. 超卖场景
高并发场景下用户下单,存在如下所示的超卖问题,其产生的主要原因是一个线程刚读出库存值,还没进行修改时,另一个线程也读出来该库存值,从而导致这两个线程在进行下单时,对同一个值减了1。
2. 解决方案
1. 悲观锁
认为线程安全问题一定会发生,线程串行执行
- 优点:简单粗暴
- 缺点:性能一般
2. 乐观锁
认为线程安全问题不一定会发生,在更新数据时判断有没有线程对数据做了修改
- 优点:性能好
- 缺点:存在成功率低的问题,多个线程同时访问,查询到同一个值,只要有一个线程进行了修改,其他的线程就都失败了
3. 乐观锁的实现
难点在于判断数据是否被修改过,判断方式有:
1. 版本号法
给数据加一个版本号
先查询到版本号和库存,更新的时候判断版本号和查询时的版本号是否相同,不相同说明这段时间库存已经被更新过了,此时更新库存失败。
2. CAS 法
库存和版本号执行相同操作,用数据本身是否有变化进行判断。先查询库存数据的值,更新时再查一遍看看库存数据有没有变化,有变化就不更新了。
使用 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();
二、一人一单
1. 场景
只容许一人下一单,要对单个用户访问的高并发情况加锁
2. 基于悲观锁的实现
@Override
public 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
// 生成唯一 id
long id = redisIdWorker.nextId("order");
voucherOrder.setId(id);
voucherOrder.setVoucherId(voucherId);
voucherOrder.setUserId(1L);
save(voucherOrder);
return Result.ok();
}
导入依赖:
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>
加入允许暴露代理对象的依赖:
@EnableAspectJAutoProxy(exposeProxy = true) // 表示开启暴露代理对象
@MapperScan("com.hmdp.mapper")
@SpringBootApplication
public class HmDianPingApplication {
public static void main(String[] args) {
SpringApplication.run(HmDianPingApplication.class, args);
}
}