用redis做分布式锁,看似是一个非常简单的问题,但是其中却蕴含着很多坑,我从单机方式开始把redis分布式锁的所有坑都踩一踩,豁然开朗。文字描述较少,直接上代码,相信大家一看就懂。尤其是点评把每一个情况的坑描述出来了。后一种情况会把上一种情况的坑给解决掉。
第一种情况:单机方式
我们对一个库存进行扣减的一个简单操作
public class RedisService {
@Autowired
private StringRedisTemplate redisTemplate;
public String descStock(){
//获取库存量
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if(stockNum>0){
//库存减一
stockNum--;
//重新设置库存
redisTemplate.opsForValue().set("stock",stockNum+"");
System.out.println("库存扣减成功,剩余"+stockNum);
} else {
System.out.println("库存扣减失败");
}
return "SUCCESS";
}
}
点评:
在单机环境下,这个代码有什么问题呢?如果并发高的情况下,stockNum是一个共享变量,很明显会带来线程安全问题
第二种情况:单机+同步锁
对于第一种情况,我们的解决办法也很简单,只需要加上synchronized代码块或者Lock锁即可
public String descStock() {
synchronized (this) {
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}
return "SUCCESS";
}
点评:
对于这种情况,如果在分布式集群模式下是否能实现共享变量的安全问题呢?明显不能,因为synchronized和Lock都是线程级别的锁,不能在进程之间锁住数据的安全性。这里我们用redis做分布式锁来解决(除了redis,还有zookeeper和数据库来实现分布式锁)
第三种情况:分布式环境+redis实现分布式锁–1:加分布式锁
这里用到了setnx命令:如果key存在着set失败,如果不存在则set成功。设置成功,意味着当前线程拿到锁,设置失败意味着当前线程没有拿到锁。
public String descStock() {
String lockKey = "lockKey";
//类似jedis.setnx(lockKey,"wxgLock");
//获取分布式锁
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");
if(!result){
return "被锁住了,请稍后重试:";
}
//throw new Exception(); ---step A
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
return "SUCCESS";
}
点评:
问题。锁没有释放,导致其他线程永远拿不到锁,导致死锁。
第四种情况:分布式环境+redis实现分布式锁–2:释放分布式锁
public String descStock() {
String lockKey = "lockKey";
//获取分布式锁
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");//类似jedis.setnx(lockKey,"wxgLock");
try {
if(!result){
return "被锁住了,请稍后重试:";
}
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--; // step A
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
//释放锁
redisTemplate.delete(lockKey);
}
return "SUCCESS";
}
点评:
注意这里把释放锁放到了finally中是因为如果业务代码异常的话也可以释放锁。难道就没有问题了吗?
问题:当代码执行STEP A 中的时候,如果是服务器宕机了,或者kill -9 了。那么依然会导致锁无法释放,其他服务依然拿不到锁,导致死锁。怎么办?
第五种情况:分布式环境+redis实现分布式锁–3:设置过期时间
public String descStock() {
String lockKey = "lockKey";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock");//类似jedis.setnx(lockKey,"wxgLock");
redisTemplate.expire(lockKey,30, TimeUnit.SECONDS); //设置过期时间30s -- step a
try {
if(!result){
return "被锁住了,请稍后重试:";
}
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
redisTemplate.delete(lockKey);
}
return "SUCCESS";
}
点评:
这个代码就没有问题吗?如果在设置时间前,也就是step a前就崩了,那么虽然设置成功了,但是,没有设置过期时间成功,依然会有第五种情况的问题,本质是两步操作么有实现原子性。死锁
第六种情况:分布式环境+redis实现分布式锁–4:设置过期时间
public String descStock() {
String lockKey = "lockKey";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, "wxgLock",10,TimeUnit.SECONDS);
try {
if(!result){
return "被锁住了,请稍后重试:";
}
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
redisTemplate.delete(lockKey);
}
return "SUCCESS";
}
点评:
一步操作保证原子性。但是真的没问题了吗?在并发不高的情况下这个分布式锁问题不大,但是在高并发下出现如下问题:

也就是说自己创建的锁被其他线程给删除掉了!怎么办?可以引入一个唯一值,如thread ID或者uuid等
public String descStock() {
String lockKey = "lockKey";
String tid = Thread.currentThread().getId()+"";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);//类似jedis.setnx(lockKey,"wxgLock");
try {
if(!result){
return "被锁住了,请稍后重试:";
}
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
if(tid.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
}
return "SUCCESS";
}
点评:
这样就真的没问题了吗?一脸蒙蔽。还有一个问题:我们设置了10s的过期时间真的够吗?如果业务代码的一个sql慢查询超过了10s。那么还是会导致代码没有执行完,锁已经自动释放了,其他线程可以获取锁(多个线程同时获得了锁),导致线程安全问题。那么设置几秒合适呢?好像没有合适的值吧。那怎么办?----可以用一个定时器来实现:锁的续命
第七种情况:分布式环境+redis实现分布式锁–5:锁的续命
锁的续命就是每隔一段时间判断当前线程的锁是否存在,如果存在则重置过期时间(给锁续了一条命!)
直接上代码:单开一个线程(可以用线程池优化),实现锁的续命
public String descStock() {
String lockKey = "lockKey";
String tid = Thread.currentThread().getId()+"";
Boolean result = redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);//类似jedis.setnx(lockKey,"wxgLock");
//锁续命的逻辑:可能不严谨,但是思路应该就这样
new Thread(()->{
Timer timer =new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
String tid = redisTemplate.opsForValue().get(lockKey);
if(id!=null&&tid.equals(id)){
//锁续命
redisTemplate.opsForValue().setIfAbsent(lockKey, tid,10,TimeUnit.SECONDS);
} else {
timer.cancel();
}
}
},2000,3000);//延迟2s执行,每隔3s执行一次续命逻辑
});
try {
if(!result){
return "被锁住了,请稍后重试:";
}
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
if(tid.equals(redisTemplate.opsForValue().get(lockKey))){
redisTemplate.delete(lockKey);
}
}
return "SUCCESS";
}
点评:
这样的话redis分布式锁就差不多了。但是操作到第七种的情况下已经变得复杂了,也不是所有人能考虑到高并发下的这些所有情况,所以我们可以借用Redission来实现分布式锁。
第八种情况:分布式环境+redis实现分布式锁–6:redission
public String descStock() {
String lockKey = "lockKey";
RLock lock = redisson.getLock(lockKey);
try {
lock.lock();
int stockNum = Integer.parseInt(redisTemplate.opsForValue().get("stock"));
if (stockNum > 0) {
stockNum--;
redisTemplate.opsForValue().set("stock", stockNum + "");
System.out.println("库存扣减成功,剩余" + stockNum);
} else {
System.out.println("库存扣减失败");
}
}finally {
lock.unlock();
}
return "SUCCESS";
}
点评:
lock.lock(); lock.unlock();两行代码解决,方便!且默认已经解决了以上的所有问题。
redission内部是用lua脚本实现的,本来想上源码分析的,但是码字实在太累了,有空再补上吧。撤了撤了。
本文深入探讨Redis实现分布式锁的多种方式,从单机环境到分布式环境,逐步解决并发操作带来的线程安全问题。通过代码示例,展示了如何使用setnx、设置过期时间、锁续命等技术,最终引入Redission简化分布式锁的实现。

1917

被折叠的 条评论
为什么被折叠?



