Redis分布式锁

Redis的常见使用场景有这么几种:

  • 通用缓存
  • 计数器(点击量、下载量、pv、uv等)
  • 分布式锁

这里我们主要谈Redis分布式锁的实现。
分布式锁主要是为了解决以下几个问题:

  1. 互斥性:多个机器不能同时获得锁,同一时刻只有一台机器占有锁
  2. 安全性:保证加锁和解锁都是同一台机器,不能误释放别人的锁
  3. 死锁:节点故障,但是一直占有锁未释放,其他节点一直加锁失败
  4. 容错:一个节点故障,保证其他节点可以释放和获取到锁

Redis是通过SetNX命令来加锁
SetNX key value
当key不存在时,设置成功返回1;
当key存在,设置失败返回0
在这里插入图片描述
这里在加锁时一定要设置过期时间,避免一个节点down掉产生死锁。

在释放锁时我们很多时候会直接del key,但是这样是不安全的,可能会释放不该释放的锁
在这里插入图片描述
因此我们要合理设置过期时间 && 释放前进行判断

锁的过期时间设置合理,不应该太长或太短,锁的过期时间过长影响新的线程重新获得锁的流程,影响业务响应时间,太短导致业务未执行完,锁自动释放,另一个线程获得锁,重新开始执行逻辑,这就间接要求业务保证幂等性,非幂等性的业务会影响数据一致性。
针对这种情况解决方案:守护线程为锁延长过期时间

Value是加锁的唯一标记,它的的值我们可以设置成机器的唯一标示,加锁解锁保证在同一台机器进行。

//release
if c.Get(myLock) == myValue{
	c.Del(myValue)
}

但是判断和删除也分两步,不是原子操作,存在判断后锁过期,另一个线程获得锁,然后误释放锁。
使用lua脚本 原子操作

String luaScript = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
ok,err := c.Eval(luaScript,myLock,myValue).Result()

官方在redis2.6.12之后就支持这样的做法:
在这里插入图片描述
官方提供的防止死锁的做法是这样的:
进行SetNX操作,key为你加的锁,value为当前的时间戳加上锁的过期时间,并不给锁加真正的过期时间。

SETNX lock.foo <current Unix time + lock timeout + 1>

原理是这样的:
当线程一先执行这条命令,获取到了锁,返回1。
线程二过来尝试加锁,会失败。接着Get(lock),拿到的值和当前时间进行比较,若小于当前时间,说明该锁已经过期,此时执行Del(lock)操作,并SetNx 重新加锁
若线程三也检查之后发现过期,只是在它释放锁之前,线程二已经释放锁并重新加上了自己的锁,那么线程三再去Del并且SetNX,此时,线程二三同时获得锁,存在问题。
Redis有一个GetSet命令,它会在设置key的value时,返回原来旧的value值。这样就能解决上述问题。
线程四进来和上面一样先检查锁是否过期,若过期就执行GetSet lock currentTime+lockTimeout,若返回值小于当前时间,可以判定线程4已经获得了锁。若有一个线程五,它在线程四之前执行了GetSet操作,那么线程四执行GetSet时会返回一个大于当前时间的值,说明该锁没有超时,不能释放。

golang的代码实现:

//TryLock
func TryLock(lock string, expireTime time.Duration) (bool){
	c := getClient()
	value := now + expireTime + 1
	for{
		now := time.Now().Unix()
		ok,err := c.SetNX(lock, value).Result()
		if ok == true || ((now > c.Get(lock)) && now > c.GetSet(lock,value)){
			break
		}
		time.Sleep(1)
	}
	return true
}
//Release
func ReleaseLock(lock string, expireTime time.Duration){
	c := getClient()
	now := time.Now().Unix()
	value := now + expireTime + 1
	if now < c.Get(lock) {
		c.Del(lock)
	}
}

存在两个问题:

  1. 在锁竞争较高的情况下,会出现Value不断被覆盖,但是没有一个Client获取到锁
  2. 在获取锁的过程中不断的修改原有锁的数据,设想一种场景C1,C2竞争锁,C1获取到了锁,C2锁执行了GETSET操作修改了C1锁的过期时间,如果C1没有正确释放锁,锁的过期时间被延长,其它Client需要等待更久的时间

这里我个人更推荐使用lua脚本+Redis分布式锁结合的方式,这也是目前很多人在使用的方式。

上面描述的是单点情况,若在redis集群环境依然存在问题:
由于Redis集群数据同步为异步,假设在Master节点获取到锁后未完成数据同步情况下Master节点crash,此时在新的Master节点依然可以获取锁,所以多个Client同时获取到了锁。
解决方案:
redlock算法,set向多半节点发送命令,过半节点成功即为成功,释放时想全部节点发送。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值