前言
案例来自黑马程序员视频:https://www.bilibili.com/video/BV1cr4y1671t/?spm_id_from=333.999.0.0
案例分析
- 下单时需要判断两点:
- 秒杀是否开始或者结束,如果尚未开始或者已经结束则无法下单;
- 库存是否充足,不足则无法下单
- 具体流程就是:最开始,我们会提交优惠卷信息,然后去查询优惠卷信息,判断秒杀是否开始,如果否,则结束流程;然后判断秒杀是否结束,如果是,则结束流程;然后判断库存是否充足,如果不足,则结束流程;否则的话,就执行扣减库存;如果库存失败,则结束流程;然后创建订单,返回订单;
但是,在多线程环境中,会出现一个典型问题:超卖问题。针对这个问题,我们的常见解决方案就是加锁
- 悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行。例如:synchronized、Lock都属于悲观锁;
- 乐观锁:认为线程安全问题不一定会发生,因此不加锁,只是在更新数据时去判断有没有其它线程对数据做了修改。如果没有修改则认为是安全的,自己才更新数据;如果已经被其它线程修改说明发生了安全问题,此时可以重试或异常;
关键代码:扣减库存的时候判断stock>0
boolean update = seckillVoucherService.update()
.setSql("stock = stock - 1")
.eq("voucher_id", voucherId)
.gt("stock", 0)
.update(); // 乐观锁 where id = ? and stock > 0
重复下单问题
解决方案:使用分布式锁
-
什么是分布式锁
分布式锁:满足分布式锁或集群模式下多进程可见并且互斥的锁。它具有以下几个特点:①高可用;②安全性;③多进程可见;④互斥;⑤高性能… -
基于redis实现的分布式锁
- 代码案例
业务代码
// 分布式锁
// 创建锁对象
SimpleRedisLock redisLock = new SimpleRedisLock("orderId:" + userId, stringRedisTemplate);
// 获取锁
boolean isLock = redisLock.tryLock(1200);
if (!isLock) {
// 获取锁失败,返回错误或重试
return Result.fail("不允许重复下单");
}
try {
IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
return proxy.createVoucherOrder(voucherId);
} finally {
// 释放锁
redisLock.unlock();
}
锁对象
public class SimpleRedisLock implements ILock {
private String name;
private StringRedisTemplate stringRedisTemplate;
private static final String KEY_PREFIX = "lock:";
private static final String ID_PREFIX = UUID.randomUUID() + "-";
private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
static {
UNLOCK_SCRIPT = new DefaultRedisScript<>();
UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
}
public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
this.name = name;
this.stringRedisTemplate = stringRedisTemplate;
}
@Override
public boolean tryLock(long timeoutSec) {
// 获取线程提示
String threadId = ID_PREFIX + Thread.currentThread().getId();
// 获取锁
Boolean success = stringRedisTemplate.opsForValue()
.setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);
// 拆箱有可能会有空指针异常
return Boolean.TRUE.equals(success);
}
// 释放锁
@Override
public void unlock() {
// 调用lua脚本 保证原子性
stringRedisTemplate.execute(
UNLOCK_SCRIPT,
Collections.singletonList(KEY_PREFIX + name),
ID_PREFIX + Thread.currentThread().getId());
}
// @Override
// public void unlock() {
// // 获取线程提示
// String threadId = ID_PREFIX + Thread.currentThread().getId();
// // 获取锁中的标识
// String lockValueId = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
// // 判断标识是否一致
// if (StringUtils.equals(threadId, lockValueId)) {
// // 释放锁
// stringRedisTemplate.delete(KEY_PREFIX + name);
// }
// }
}
public interface ILock {
/**
* 尝试获取锁
* @param timeoutSec 锁持有的超时时间,过期后自动释放
* @return true 代表获取锁成功;false代表获取锁失败
* */
boolean tryLock(long timeoutSec);
/*
* 释放锁
* */
void unlock();
}
lua脚本
--比较线程标识与锁中标识是否一致
if (redis.call('get', KEYS[1]) == ARGV[1]) then
--释放锁,del key
return redis.call('del', KEYS[1])
end
return 0
- 实现思路总结:
- 利用 setnx ex 获取锁,并设置过期时间,保存线程标识;
- 释放锁时先判断线程标识是否与自己一致,一致则删除锁;
- 特性
- 利用 setnx 满足互斥性
- 利用 setex 保证故障时锁依然能释放,避免死锁,提高安全性;
- 利用Redis集群保证高可用和高并发特性
转载:https://blog.csdn.net/qq_42582773/article/details/128362657
查看评论