1、不使用分布式锁
synchronized (this){
int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
if (stock > 0) {
int realStock = stock - 1;
// 更新库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
// 此处执行业务代码
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
// 扣减库存操作执行完,删除这个键
stringRedisTemplate.delete(goodsId);
return new ResponseResult<>("执行成功", 200);
}
缺陷:
集群环境下并不能解决商品超卖BUG。真正的工作中,线上是一个集群环境(分布式环境),nginx会将请求分发到不同的后端服务上,但是因为synchronized锁是JVM进程级别的锁,也就是说是一个单机锁,并不能跨服务控制线程并发。
2、入门版分布式锁
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/deduct_stock/{goodsId}")
@ApiOperation("秒杀减库存场景")
public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
// 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
// setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁");
if (Boolean.TRUE.equals(flag)) {
int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
if (stock > 0) {
int realStock = stock - 1;
// 更新库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
// 扣减库存操作执行完,删除这个键
stringRedisTemplate.delete(goodsId);
return new ResponseResult<>("执行成功", 200);
} else {
return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
}
}
缺陷:
- 在删除锁之前,如果业务代码有异常,则锁无法删除,死锁!
- 如果请求执行到一半宕机了,锁无法删除,死锁!
2.1、优化
- 业务代码通过try-catch-finally代码块包裹一下,删除锁的操作放在finally里。
- 给锁设个过期时间。
@Autowired
private StringRedisTemplate stringRedisTemplate;
@GetMapping("/deduct_stock/{goodsId}")
@ApiOperation("秒杀减库存场景")
public ResponseResult<?> deductStock(@PathVariable(name = "商品id") String goodsId) {
// 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
// setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
// 设置锁过期时间为10s
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, "分布式锁", 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(flag)) {
return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
}
try {
int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
if (stock > 0) {
int realStock = stock - 1;
// 更新库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
// 扣减库存操作不管成功与否,都删除这个键
stringRedisTemplate.delete(goodsId);
}
return new ResponseResult<>("执行成功", 200);
}
但还是有缺陷:
上述操作虽然避免了死锁问题,但不能解决误删锁的问题。因为业务代码的执行时间是不可控的,假设给锁设置过期时间为10s,而业务代码执行完需要15s,就会导致第一个请求还没执行完,锁就已经删掉了。这时第二个请求会创建锁,恰巧执行到一半时第一个请求执行完,删了第二个请求加的锁。这种极端情况下,有锁和没锁一样,很容易造成库存的脏读,导致超卖BUG。
2.2、再优化
- 每一次请求都生成一个uuid作为锁的值,删除锁时先判断这个锁是否属于当前线程,如果是则删除这个锁
@GetMapping("/deduct_stock/{goodsId}")
@ApiOperation("秒杀减库存场景")
public ResponseResult<?> deductStock(@PathVariable String goodsId) {
String clientId = IdUtil.randomUUID();
// 将商品id作为键,存到redis。每一个线程执行减库存方法时,如果存成功,则执行扣减库存的代码,存失败说明前面有线程正在执行,需等待。
// setIfAbsent(key,value);如果key不存在,则创建这个key,否则什么也不做
// 设置锁过期时间为10s
Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(goodsId, clientId, 10, TimeUnit.SECONDS);
if (Boolean.FALSE.equals(flag)) {
return new ResponseResult<>("当前商品抢购繁忙,请稍后再试", 210);
}
try {
int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
if (stock > 0) {
int realStock = stock - 1;
// 更新库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
// 扣减库存操作不管成功与否,只要这个锁属于当前线程,都要删除这个锁
String lockValue = stringRedisTemplate.opsForValue().get(goodsId);
if (clientId.equalsIgnoreCase(lockValue)) {
stringRedisTemplate.delete(goodsId);
log.info("删除锁。。。");
}
}
return new ResponseResult<>("执行成功", 200);
}
还有BUG:
解决方案:
锁续命方案,每一次请求开一个分线程执行定时任务,定时查询锁有没有过期,如果没过期则延长过期时间到10s。
3、Redisson实现分布式锁
@GetMapping("/deduct_stock_redisson/{goodsId}")
@ApiOperation("redisson实现分布式锁")
public ResponseResult<?> deductStockByRedisson(@PathVariable String goodsId) {
// 获取锁对象
RLock redissonLock = redisson.getLock(goodsId);
// 加锁
redissonLock.lock();
try {
int stock = Integer.parseInt(Objects.requireNonNull(stringRedisTemplate.opsForValue().get("stock")));
if (stock > 0) {
int realStock = stock - 1;
// 更新库存
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减失败,库存不足");
}
} catch (Exception e) {
} finally {
// 释放锁
redissonLock.unlock();
}
return new ResponseResult<>("执行成功", 200);
}
原理:
分布式锁是串行化操作,与并发编程的并行执行相违背,但可以采取一些方法优化性能。
- 锁的范围粒度越小越好。可以不在锁代码块里的尽量挪出去。
- 线程安全的明发map。分段锁
Redisson实现分布式锁并非绝对安全:
因为redis的主从复制功能,线程1需要先向master节点上加锁,master节点加锁成功后会将加锁命令同步到各个slave节点。假设master节点向slave节点同步的时候突然挂了,这时候slave节点并没有加锁成功,那么线程2就会在slave节点上加锁,依旧会出现超卖情况。
4、redLock
实现原理:
RedLock的实现原理就是使用Redis实现分布式锁,通过搭建多个独立的没有主从关系的redis,每次加锁都要往所有redis上加锁,超过一半(也有说所有)节点加锁成功才算加锁成功。同时每一个独立redis都不要进行主从复制,以免出现主节点宕机造成锁丢失。锁记得加上过期时间避免死锁。redis的持久化也必须要选择always,即每执行一次命令,进行一次持久化。