Redis实现分布式锁

一、什么是分布式锁?

在分布式环境下,系统被拆分,代码可能会被不同的jvm运行,在单进程的情况下,我们可以使用java语言和本身的类库提供的锁,完成高并发的需求。

二、常见的分布式锁:

  • Memcached分布式锁
  • redis分布式锁
  • Zookeeper分布式锁
  • Chubby 底层使用了Paxos一致性算法

三、redis如何实现分布式锁?

三要素:

1. 加锁

sentnx命令
setnx(key,1) ,key一般可以用商品ID
当一个线程返回1,说明key不存在;
返回0说明key已存在,抢锁失败

2. 解锁

解锁伪代码如下:
del(key)

3. 锁超时

锁超时就是说获得锁的线程在运行的时候挂掉了,来不及释放锁
所以设置锁的时候必须设置超时时间。
setnx不支持超时参数,所以有单独的指令:expire(key, 30)
分布式的伪代码如下:

if(setnx(key, 1) == 1) {  //代码执行到这里,线程挂掉
    expire(key, 30)
    try {
        do something...
    } finally {
        del(key)
    }
}
存在问题
1. 上面代码在并发的时候会有大问题:

见注释
代替指令:set(key, 1, 30, NX)

2. del误删

假如A线程锁超时时间是30毫秒,A线程执行的慢,锁释放
B线程拿到锁,执行,上面释放的可能是B线程的锁,所以加锁的时候要用自己线程的ID作为value,再释放锁的时候验证key对应的value是否属于当前线程。
加锁:

String threadId = Thread.currentThread.getId();
set(key, threadId, 30, NX);

解锁:

if(threadId.equals(redClient.get(key))) {
    del(key)
}

这里有一个问题,判断和释放锁不是一个原子性操作,需要使用lua脚本实现

"local currentValue = redis.call('get', KEYS[1]);\n" + "if currentValue == ARGV[1]\n" + "then\n"
                + "redis.call('del',KEYS[1]);\n" + "return true;\n" + "end\n" + "return nil;";
3. 守护线程

上面虽然在value中记录了版本号,并且根据版本号判断释放锁,但是,还是没有解决A执行慢,导致超时后锁释放,B线程获取锁执行造成的并发问题。

解决方法:
方案1:
对于上述问题,我们必须设置锁超时时间>线程执行时间,但是这个时间很难把握,网络波动等一系列因素,会让线程执行时间不确定。可以在项目中强行指定超时时间,超过规定时间,抛出超时异常,这时锁超时时间比较容易确定。

方案2:
获得锁的线程开启守护线程,给快要过期的锁续航,这样A线程就可以完成任务再释放。

守护线程伪代码:

public void run() {
	int waitTime = lockTime * 1000 * 2 / 3;
	while(线程存活) {
		Thread.sleep(waitTime);
		重置超时时间
	}
}

重置超时时间
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1],ARGV[2]) else return '0' end";

首先需要在合适的时间对执行的线程续航,这里选择了锁超时时间的三分之二,其次重置时间需要使用lua脚本,和锁释放很像,判断了当前持有锁的对象是否一致,避免随意续航的情况。

由于守护线程和A线程在同一个进程中,如果jvm挂掉,这把锁到了超时时间也没人续命,自动释放。

第二种方案,会让系统代码变得更加复杂,推荐使用第一种

4. 主从切换导致锁问题

Redis集群是高可用的,一般使用哨兵机制进行主备切换,但是,由于Redis的主从复制是异步的,可能存在数据丢失,Redis主从复制原理可以看我的这篇博客

流程分析:

  • 客户端1从Master获取了锁。
  • Master宕机了,存储锁的key还没有来得及同步到Slave上。
  • Slave升级为Master。
  • 客户端2从新的Master获取到了对应同一个资源的锁

解决方案:
进行合理的参数配置,降低丢失数据的可能性

min-slaves-to-write 1
min-slaves-max-lag 10

至少有一个slave与master的同步复制不超过10s,一旦达到10s,master不会再接受任何请求

对于上述情况,业内还存在大名鼎鼎的RedLock算法,但是该算法争议较大,好像也存在问题,后续有时间会研究下

四、Redis分布式锁代码

这里再提出一个问题,如果线程过来拿锁失败怎么办?

我们可以借鉴jdk自旋锁的思想,如果拿锁失败,也就是有线程正在执行,那么让当前线程休眠一会儿,然后再次尝试拿锁,如果循环几次,最后拿不到锁抛出异常

至于休眠时间和重试此处可以做成可配置

代码实现:

1. 加锁 用会员编号作为key


value = redisUtil.tryLock(req.getCustNum()); 

//tryLock() 方法实现:
public String tryLock(String key) {
	//重试次数小于指定最大自旋次数
 	while(retryCount < maxRetryCount) {
 		if (redisManager.setnx(key, value, REDIS_EXPIRE_TIME)) {
       		return value;   //返回value,在释放锁时验证释放的是同一个锁
       }
       	//休眠
		Thread.sleep(maxWaitTime)
		retryCount++;
 	}
 	throw new RuntimeExeception("当前线程获取锁失败,请稍后重试");
}

2. 释放锁:

//和上面value值相同,防止误删锁
public void releaseLock(String key, String value) {
	//三个参数,key值会员编号,value返回值:可以放当前线程id,第三个参数是lua脚本
	execDelLuaScript(key, value, delScript);  
}

//lua简单脚本实例
"local currentValue = redis.call('get', KEYS[1]);\n" + "if currentValue == ARGV[1]\n" + "then\n"
                + "redis.call('del',KEYS[1]);\n" + "return true;\n" + "end\n" + "return nil;";
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值