redis 分布式锁的实现方式

情景如下:

我们有一批任务需要由多个分布式线程处理,每个任务都有一个taskId,为了保证每个任务只被执行一次,在工作线程执行任务之前,先获取该任务的锁,锁的key可以为taskId

方式1:set(key,value)方式

原理:在获取锁之前先查询一下以该锁为key对应的value存不存在,如果存在,则说明该锁被其他客户端获取了,否则的话就尝试获取锁,获取锁的方法很简单,只要以该锁为key,设置一个随机的值就行了

代码:

func getLock(taskID int) bool {
	// 若存在该key,说明其他线程已获取到锁,返回失败
	// 若不存在该key,则先设置key,标记自己获取到锁,返回成功
	if(existKey(taskID)) {
		return false
	} else {
		setKey(taskID)
		return true
	}
}

缺陷:该函数并不是原子性的,当一个线程执行existKey()时,检测到某个锁不存在,并在执行setKey()之前,其他线程可能也执行了existKey(),同样检测到该锁不存在,也会紧接着执行setKey方法,这样一来,同一把锁就有可能被不同的线程获取到了

方式2:setnx(key,value,timeout)方式 ,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果

原理:如果 setnx() 返回1,说明该线程获得锁,SETNX将键 key 的值设置为value

            如果 setnx() 返回0,说明其他线程已经获得了锁,可以在一个循环中不断地尝试 setnx()操作,以获得锁

代码:

func getLock(taskID int) bool {
	// setnx返回1,说明自己已获取到锁,返回成功
	// setnx返回0,说明其他线程已经获取到锁,返回失败
	if(SETNX key value,,time.Second*2) == 1{
		return true;
	}
	return false;
}

缺陷:客户端A获取锁的时候设置了key的过期时间为2秒,然后客户端A在获取到锁之后,业务逻辑方法doSomething执行了3秒(大于2秒),当执行完业务逻辑方法的时候,客户端A获取的锁已经被Redis过期机制自动释放了,因此客户端A在获取锁经过2秒之后,该锁可能已经被其他客户端获取到了。当客户端A执行完doSomething方法之后接下来就是执行releaseLock方法释放锁了,由于前面说了,该锁可能已经被其他客户端获取到了,因此这个时候释放锁就有可能释放的是其他客户端获取到的锁

扫描二维码关注公众号,回复: 9326084 查看本文章

方式3:setnx(key,value,timeout)方式,value为一个随机值

原理:既然方式二可能会出现释放了别的客户端申请到的锁的问题,那么该如何进行改进呢?有一个很简单的方法是,我们设置key的时候,将value设置为一个随机值r,当释放锁,也就是删除key的时候,不是直接删除,而是先判断该key对应的value是否等于先前设置的随机值r,只有当两者相等的时候才删除该key,由于每个客户端产生的随机值是不一样的,这样一来就不会误释放别的客户端申请的锁了

代码:

// 获得锁
func getLock(taskID int) bool {
	// setnx返回1,说明自己已获取到锁,返回成功
	// setnx返回0,说明其他线程已经获取到锁,返回失败
	if(SETNX key value,,time.Second*2) == 1{
		return true;
	}
	return false;
}

// 释放锁
func releaseLock(taskID, value) {
	// 获取key对应的值,若和之前设置的随机值相等,则删除
	if getKey(taskID) == value {
		deleteKey(taskID)
	}
}

缺陷:releaseLock()函数不是原子性的,不是原子性操作意味着当一个客户端A执行完getKey()并在执行deleteKey()之前,也就是在这2个函数执行之间,其他客户端是可以执行其他命令的。考虑这样一种情况,在客户端A执行完getKey(),并且该key对应的值也等于先前的随机值的时候,接下来客户端A将会执行deleteKey()。假设由于网络或其他原因,客户端A执行getKey()之后过了1秒钟才执行deleteKey(),那么在这1秒钟里,该key有可能也会因为过期而被Redis清除了,这样一来另一个客户端,姑且称之为客户端B,就有可能在这期间获取到锁,然后接下来客户端A就执行到deleteKey()了,如此一来就又出现误释放别的客户端申请的锁的问题了

方式4:setnx(key,value,timeout)方式,value为一个随机值,删除时执行lua代码,来保证原子性

原理:既然方式三的问题是因为释放锁的方法不是原子操作导致的,那么我们只要保证释放锁的代码是原子性的就能解决该问题了。有另外一种方式,就是Lua脚本。由于Lua脚本的原子性,在Redis执行该脚本的过程中,其他客户端的命令都需要等待该Lua脚本执行完才能执行,所以不会出现方案三所说的问题。至此,使用Redis实现分布式锁的方案就相对完善了

代码:go语言版本

/**
***基于单节点redis 分布式锁
**/
package redislock

import (
	"crypto/rand"
	"encoding/base64"
	"errors"

	"github.com/garyburd/redigo/redis"
)

type RedisLock struct {
	lockKey string
	value   string
}

//保证原子性(redis是单线程),避免del删除了其他client获得的lock
var delScript = redis.NewScript(1, `
if redis.call("get", KEYS[1]) == ARGV[1] then
	return redis.call("del", KEYS[1])
else
	return 0
end`)

// 获得锁
func (this *RedisLock) Lock(rd *redis.Conn, timeout int) error {

	{ //随机数
		b := make([]byte, 16)
		_, err := rand.Read(b)
		if err != nil {
			return err
		}
		this.value = base64.StdEncoding.EncodeToString(b)
	}
	lockReply, err := (*rd).Do("SET", this.lockKey, this.value, "ex", timeout, "nx")
	if err != nil {
		return errors.New("redis fail")
	}
	if lockReply == "OK" {
		return nil
	} else {
		return errors.New("lock fail")
	}
}

// 释放锁
func (this *RedisLock) Unlock(rd *redis.Conn) {
	delScript.Do(*rd, this.lockKey, this.value)
}

结论:

上述分布式锁的实现方案中,都是针对单节点Redis而言的,然而在实际的生产环境中,我们使用的通常是Redis集群,并且每个主节点还会有从节点。由于Redis的主从复制是异步的,因此上述方案在Redis集群的环境下也是有问题的。比如主节点刚设置了一个key用做锁,还没同步到从节点,此时主节点崩溃了,稍后从节点升级为主节点,自然没有这个key,那么其他客户端请求时就又请求到了锁,造成混乱。

关于在Redis集群中如何优雅地实现分布式锁,后续再写文章详述

发布了105 篇原创文章 · 获赞 58 · 访问量 11万+

猜你喜欢

转载自blog.csdn.net/yzf279533105/article/details/100524700