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方法释放锁了,由于前面说了,该锁可能已经被其他客户端获取到了,因此这个时候释放锁就有可能释放的是其他客户端获取到的锁

方式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集群中如何优雅地实现分布式锁,后续再写文章详述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis分布式锁可以通过多种方式实现。其中一种方式是使用Redisson分布式锁Redisson解决了「过期释放,业务没执行完」的问题。它通过在加时启动一个后台线程,每隔一段时间检查是否还被持有,如果是,则延长的生存时间。这样可以确保在业务逻辑执行期间不会过期。\[1\] 除了Redisson,还有其他的分布式锁实现方式。其中一种方式是使用SETNX + EXPIRE命令,通过设置一个键对来表示的状态,并设置过期时间来自动释放。另一种方式是使用Lua脚本,通过原子性的执行SETNX和EXPIRE两条指令来实现和设置过期时间。还有一种方式是使用Redis的扩展命令SET EX PX NX,通过设置过期时间和唯一随机实现。此外,还有Redlock和Redission等开源框架可以实现多机的分布式锁。\[2\] 需要注意的是,分布式锁实现方式需要考虑一些问题,比如加后业务逻辑还未执行完成已经过期,这会导致其他客户端拿到。如果是单节点,这个问题不大,但是在集群环境下,加首先会落盘到master节点,然后再复制到slave节点。如果在复制之前master节点挂掉,就会导致丢失的问题。为了解决这些问题,Redis官方推荐使用Redisson分布式锁。\[3\] #### 引用[.reference_title] - *1* *2* [Redis实现分布式锁的7种方案](https://blog.csdn.net/qszfly/article/details/126100421)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v4^insert_chatgpt"}} ] [.reference_item] - *3* [Redis分布式锁的正确实现方式](https://blog.csdn.net/yaomingyang/article/details/104965554)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v91^insert_down28v1,239^v4^insert_chatgpt"}} ] [.reference_item] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值