概述
什么是分布式锁?
为了防止分布式系统中的多个进程之间相互干扰,我们需要一种分布式协调技术来对这些进程进行调度,而这个分布式协调技术的核心就是来实现这个分布式锁。 说人话: 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行。
代码实践
1. 上来就看代码
/**
* 测试Redis实现分布式锁
* 扣减库存
* @return
*/
@GetMapping("/deduct_stock")
public String deductStock() {
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
return "ok";
}
2. 思考,有啥问题?
多线程并发请求时,可能导致超卖。
/**
* 测试Redis实现分布式锁
* @return
*/
@GetMapping("/deduct_stock")
public String deductStock() {
synchronized (this) {
// 将下面两个redis关键操作,放入同步代码块中即可,JDK1.8性能还好,底层原理做了升级
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if(stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
}
return "ok";
}
3. 思考,还有啥问题?
synchronized 只在同一个jvm级别内有效,如果集群呢? 2个应用集群,外加一个NGINX做负载,可以用压测工具压下试试,看下效果。
/**
* 测试Redis实现分布式锁
*
* @return
*/
@GetMapping("/deduct_stock")
public String deductStock() {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "qinchen");
if(!lock) {
log.info("未获取到锁,下单失败");
return "5001";
}
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
// 释放锁
stringRedisTemplate.delete("lockKey");
return "ok";
}
4. 思考,还有啥问题?
中间可以出现异常,导致锁释放失败,最后导致死锁。
/**
* 测试Redis实现分布式锁
*
* @return
*/
@GetMapping("/deduct_stock")
public String deductStock() {
try {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lockKey", "qinchen");
if(!lock) {
log.info("未获取到锁,下单失败");
return "5001";
}
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
} finally {
// 释放锁
stringRedisTemplate.delete("lockKey");
}
return "ok";
}
5. 思考,还有啥问题?
有没有可能代码还没执行到finally,正好运维杀进程,或者宕机了。怎么办?
@GetMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey:10001";
try {
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen");
// 给锁设置一个过期时间
stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
if(!lock) {
log.info("未获取到锁,下单失败");
return "5001";
}
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
} finally {
// 释放锁
stringRedisTemplate.delete(lockKey);
}
return "ok";
}
6. 思考,还有啥问题?
设置锁与设置过期时间是原子操作吗?如果在这两行代码直接出现了意外呢?
// Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen");
// 给锁设置一个过期时间
// stringRedisTemplate.expire(lockKey, 10, TimeUnit.SECONDS);
// 设置锁并同时给一个过期时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "qinchen", 10, TimeUnit.SECONDS);
7. 思考,还有啥问题吗?
如果业务执行时间超过10s呢?比如15秒,可能会出现上一个线程加的锁被下一个误删除。 仔细思考,上一个线程10s还未执行完时,锁已经过期了,下一个线程是不是就可以获取到锁了。 而这个时间,如果下一个线程执行的时间假定为8秒,在上一个线程15秒执行完时,是不是把刚刚第二个线程加的锁给删除了?而第二个线程可能才执行5秒,还有3秒才执行完。 以此类推,3秒后,第二个线程执行完了,是不是有可能把第三个获取到锁的线程的锁又给删了。 最终导致锁失效现象。
@GetMapping("/deduct_stock")
public String deductStock() {
String lockKey = "product:10001";
String clientId = UUID.randomUUID().toString();
try {
// 设置锁并同时给一个过期时间
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId , 10, TimeUnit.SECONDS);
if(!result) {
log.info("未获取到锁,下单失败");
return "5001";
}
// 获取库存
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
} finally {
// 释放锁
if(clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))) {
stringRedisTemplate.delete(lockKey);
}
}
return "ok";
}
如果是数据库,这条SQL语句是否可以解决此问题?思考: update tb_stock set sotck_tatal = sotck_tatal - 1 where (sotck_tatal - 1 >= 0)
8. 再讨论下,还有啥问题吗?
finally里面的get与delete操作未保证原子性,会怎么样?可能出现一定时间导致死锁。 其实一般软件公司,实现到这里,算笔记完美了,可以用了。 但是在真正互联网场景下,还是存在一定问题。 主要是超时设置问题。上面说的,如果业务执行时间超10s以上,还是存在两个或以上的线程同时执行我们业务代码的可能性。超时时间到底设置多大合适? 这个时候,提到一个概念:锁续命。
解决思路:
- 应用内部使用一个定时器子线程
- 取1/3超时时间,比如每隔3秒,判断下锁是否还存在?
- 存在,则再讲该锁设置过期时间为10s
- 如果不存在,则定时任务子线程直接结束。
Redison框架
- 这里提到Redison框架,用来实现我们一把强壮的分布式锁场景。
- Redisson底层实现原理:
- 怎么用?
@GetMapping("/deduct_stock_redisson")
public String deductStockRedisson() {
String lockKey = "product:10002";
RLock redissonLock = redisson.getLock(lockKey);
try {
// 加锁,外加内部实现了锁续命,默认超时时间为30s
redissonLock.lock();
// 获取库存
int stock_redisson = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock_redisson"));
if (stock_redisson > 0) {
int realStock = stock_redisson - 1;
stringRedisTemplate.opsForValue().set("stock_redisson", realStock + "");
log.info("下单成功,剩余库存为 = {}", realStock);
} else {
log.warn("下单失败,库存不足");
}
} finally {
redissonLock.unlock();
}
return "ok";
}
- Redisson底层使用了较多lua脚本,保证了较多redis操作的原子性,从性能上来说,减少了网络开销,提升了redis命令操作性能。
- Redisson默认模式为:自旋锁(while循环,重复尝试加锁)
- Redisson支持:可重入锁、公平锁、联锁,红锁、读写锁、信号量、闭锁等特性。
- 更深入学习可点击我查看文档。
8. 再全局考虑,还有啥问题吗?
- Redis高可用结构下,若在主节点获取到锁后,设置的锁key还未同步到slave
- 此时Master挂了,Slave提升为新的Master
- 后面的线程是不是即可再次加锁成功
- 但前面那个线程有可能还未执行完成,此时的锁机制还是存在一定不可靠性
扩展学习zk分布式锁
- 可扩展学习基于zk实现的分布式锁,可以解决此问题,能够得到一把可靠性高的锁
- 基于zk支持数据强一致性特性
- 但是基于zk实现的分布式锁,性能不如redis
了解下Redlock红锁
- 主要原理就是同时从多个RedisServer中拿锁
- 达到高可用,但牺牲性能较大
- 一般作为了解,不建议使用
总体上,绝大部分公司的业务,基于Redis的分布式锁都能满足需求了.
思考,如何提升分布锁性能?
- 大量流量集中引入,Redis服务器压力大
- 思路:分段锁
- 将库存进行分段,放入redis中,例如1000库存,可分10段放入Redis
- key的设计可以为Product:10001:0 | Product:10001:1 ....
- Redis底层集群,将根据key,计算器槽位,放入不同节点中
- 这样便分担了压力