redis分布式锁实现方案深入探讨(锁重试、锁续租、Redis Module、Redisson、RedLock)

分布式锁一般有如下的特点:

  • 互斥(Mutual Exclusion): 同一时刻只能有一个线程持有锁
  • 同步:获取锁失败是可以阻塞,后续可被唤醒
  • 可重入性: 同一节点上的同一个线程如果获取了锁之后能够再次获取锁
  • 避免死锁(Dead lock free):和J.U.C中的锁一样支持锁超时,防止死锁
  • 容错(Fault tolerance): 避免单点故障,锁服务要有⼀定容错性

基本原理

Redis分布式锁,主要借助setnx和expire两个命令完成。

setnx 对一个key进行设置value时,如果这个key已经存在,就什么也不做,返回0;如果不存在,就在redis缓存里存储这个key-vlaue,并返回OK,表示获取锁成功。

setnx key “xxx”

释放锁的话就直接把这个key删除即可。

但是需要注意几个问题:

(1)持有锁的进程或线程因为异常,未释放锁,导致死锁。

(2)释放锁的时候,只有获取锁的那个进程才能删除这个key,其他进程不能。

给锁设置超时时间(setnx+expire)

第一个问题的解决方案就是给key设置一个超时时间,key超时之后自动删除就释放锁了。

setnx和expire是两个命令,虽说expire在setnx成功后执行,是线程安全的,但两个操作不是原子的,如果setnx成功,还没来得及expire,持有锁的进程就挂了,也是会造成死锁。

所以要保证setnx和expire是一个原子操作。

设置key的同时设置超时时间 set key value PX 100 NX

set key value [EX seconds] [PX milliseconds] [NX|XX]

EX seconds:设置失效时长,单位秒

PX milliseconds:设置失效时长,单位毫秒

NX:key不存在时设置value,成功返回OK,失败返回(nil)

XX:key存在时设置value,成功返回OK,失败返回(nil)

通过lua脚本将setnx和给key设置超时时间封装起来,用redis执行lua脚本达到原子性目的

使用lua脚本编写加减锁的逻辑,保证加锁和释放锁的原子性:

加锁的Lua脚本: lock.lua

--- -1 failed
--- 1 success
---
local key = KEYS[1]
local requestId = KEYS[2]
local ttl = tonumber(KEYS[3])
local result = redis.call('setnx', key, requestId)
if result == 1 then
    --PEXPIRE:以毫秒的形式指定过期时间
    redis.call('pexpire', key, ttl)
else
    result = -1;
    -- 如果value相同,则认为是同一个线程的请求,则认为重入锁
    local value = redis.call('get', key)
    if (value == requestId) then
        result = 1;
        redis.call('pexpire', key, ttl)
    end
end
--  如果获取锁成功,则返回 1
return result

释放锁逻辑(get+del)

释放锁的逻辑,必须是持有的锁的线程或进程才能释放锁,所以在加锁时,给key设置的value必须能唯一标识持有锁的对象,比如ip+port+threadId等

释放锁的时候先获取锁key的value,比对一下是不是和自己的唯一标识一样,一样就删除该key释放锁,不一样就什么也不做。

查询key和删除key,是两个动作,同样不是原子操作,所以可以通过lua脚本进行封装。

解锁的Lua脚本: unlock.lua:

--- -1 failed
--- 1 success

-- unlock key
local key = KEYS[1]
local requestId = KEYS[2]
local value = redis.call('get', key)
if value == requestId then
    redis.call('del', key);
    return 1;
end
return -1

锁续租

前面在加锁的时候,给key设置了超时时间,如果获取锁后,业务处理时间过长,还没处理完,锁就超时自动释放锁了,当前线程可能还不知道锁被超时释放,继续向下处理业务,另一个进程或者线程此时又获取锁成功,这样就出现了并发问题,锁基本没啥用。当然因为锁key设置value具有唯一性,所以不存在前一个线程把后一个线程获取到锁释放掉。

解决方法,就是可以先预估好业务处理需要的时长,然后预留多一点的超时时间给key。这种方式肯定是治标不治本,如果遇到fullgc,且时间过长,导致的stw,也会让预估的业务处理时长不准。

所以比较彻底的办法就是在加锁成功之后再起一个后台线程,定时检查锁是否过期,快过期的时候,延迟超时时间。

具体逻辑:

  1. 加锁成功,开启一个后台线程。
  2. 后台线程根据锁的超时时间,每隔3/4的超时时间去对锁进行一次锁key的超时重置。
  3. 续租时,需要先获取key的value看看是不是自己持有的锁,如果是才续租。
  4. 查看锁和续租封装在lua脚本里保证原子性。
if (redis.call('get', KEYS[1]) == ARGV[1]) then "
   return redis.call('pexpire', KEYS[1], ARGV[2]); "
else
   return nil; 
end;

锁重试

对同一把锁,假设一个进程加锁成功了,一个进程想再获取锁就得不断循环重试,这就相当于一个自旋锁,虽然没有线程上下文的切换,但是在锁竞争激烈的情况下,对cpu的占用和性能损耗是巨大的。

所以需要实现一种像java的synchronized或者AQS那样,在获取锁失败后,可以先自旋一段时间,如果还获取不到锁就进入同步队列阻塞等待。java的单进程锁,获取锁失败的线程阻塞可以由本进程持有锁的线程释放锁时唤醒,但是分布式锁,需要实现的是跨进程唤醒

具体实现逻辑:

  1. 分布式锁可以在单进程里维护一个CLH,逻辑上跟AQS差不多,获取锁失败就进队列阻塞。如果是同进程的多个线程获取同一把锁被阻塞,可以达到同进程唤醒的效果。
  2. 当前进程没有线程持有锁,就需要跨进程唤醒,此时可以开启一个后台重试线程。
  3. 重试线程的逻辑就是,定时检查锁是否空闲,如果空闲就唤醒当前正在阻塞的线程。
  4. 获取锁成功后,就回收这个重试线程。

也可以使用带超时阻塞的LockSupport.parkNanos,在获取锁的主线程里进行重试逻辑。但是为了代码解耦,所以单独开启一个重试线程。也是为了防止惊群效应。

codis 没有pub/sub功能,如果想使用监听+远程唤醒的功能,需要用redis cluster

用zk和etcd实现的分布式锁,都可以使用watcher机制。

但是redis cluster、zk、etcd v2 的发布订阅都会存在惊群效应,一个事件变更,如果有大量的客户端监听,需要一个个远程通知,对这些集群的负载影响很大。

etcd v3,实现了类似java的AQS的clh,有多个监听会形成一个链表,不会一下子唤醒,而是依托前驱唤醒后继。

百度CAS/CAD

因为Lua 脚本的相关问题,性能、内存占用等,百度内部方案:SET + CAS/CAD Module,去掉了对Lua脚本的依赖。

加锁:

SET KEY VALUE NX PX 30000

续租:

CAS KEY VALUE VALUE PX 30000

释放锁:

CAD KEY VALUE

SET + CAS/CAD 的优点:

  • 命令简单,单条命令即可实现加锁、续约、解锁的功能。

  • 避免 Lua 脚本的问题:热点问题,扩缩容时间脚本丢失,以及 Lua 内存占用问题。

缺点:

  • Redis 的主从复制是异步的,failover 过程中可能丧失锁的安全性。
  • 锁可重入的问题

https://github.com/alibaba/alibabacloud-tairjedis-sdk

https://github.com/baidu/dlock

Redisson 分布式锁实现方案

Redisson 是架设在 Redis 基础上的一个 Java 驻内存数据网格(In-Memory Data Grid)

  • 可重入锁 RedissonLock

  • 读写锁 RedissonReadWriteLock

  • 公平锁 RedissonFairLock

  • 自旋锁 RedissonSpinLock

  • 联锁、红锁 RedissonMultiLock、RedissonRedLock

整个加锁流程:

Lua脚本源码:

Redisson 加锁的数据结构是一把锁对应的hash表,外围的大key是锁名称,里面的小key客户端唯一标识,value是锁重入次数,然后给这个哈希表设置超时时间。

**KEYS[1]**代表的是你加锁的那个key,比如说:

RLock lock = redisson.getLock(“DISLOCK”);

这里你自己设置了加锁的那个锁key就是“DISLOCK”。

**ARGV[1]**代表的就是锁key的默认生存时间

调用的时候,传递的参数为 internalLockLeaseTime ,该值默认30秒。

**ARGV[2]**代表的是加锁的客户端的ID,类似于下面这样:

01a6d806-d282-4715-9bec-f51b9aa98110:1

Redis Hincrby 命令用于为哈希表中的字段值加上指定增量值。

增量也可以为负数,相当于对指定字段进行减法操作。

如果哈希表的 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。


Redisson优点:

  • 完整的解决方案,使用简单

  • 可重入、远程通知机制、自动续约(watch dog)

需要注意:Redisson 的 watchDog 只有在未显式指定加锁时间时才会生效。

缺点:

  • Redis 的主从复制是异步的,failover 过程中可能丧失锁的安全性

  • LUA 脚本的相关问题,性能,内存占用等

  • PUB/SUB 机制在集群模式下负载较重,spinlock对其进行了优化

RedLock

RedLock 是 Redis 官方针对 Redis 异步复制机制在 failover 过程可能导致数据丢失,而设计的一种分布式锁算法。RedLock也是Redisson中一种锁实现方案。

RedLock算法思想:

  • 获取锁的过程需要在一个奇数个redis实例集群上,获取超过一半以上实例的锁,才算是成功获取锁。
  • 释放锁的过程也是需要在所有获取到锁的实例上一个个释放,才是真正释放锁。
  • 获取锁失败,即获取锁的redis失败少于一半,那么需要把从其他实例获取的锁释放。

RedLock除了安全,再无其他优点:

  • 性能低,加锁和释放锁都需要操作多台redis
  • 部署实施复杂
  • 如果一台redis加锁成功,还没有持久化就宕机了,此时重启,另一个进程或线程又可以获取锁,所以要求redis宕机延迟重启,延迟时长为锁过期时间,但是redis里锁那么多,到底以哪个过期时间为准,不好判断。

使用redis分布式锁,是追求高性能, 在cap理论中,追求的是 ap 而不是cp。

所以,如果追求高可用,建议使用 zookeeper或者etcd实现分布式锁。

redis分布式锁可能导致的数据不一致性,建议使用业务补偿的方式去弥补。所以,不太建议使用红锁。

<think>好的,用户想了解如何用Go语言基于Redis实现分布式锁的最佳实践和示例代码。首先,我需要回忆一下分布式锁的基本概念和Redis的相关命令。记得Redis的SET命令加上NX和PX选项可以实现原子性的加操作,这样可以避免竞态条件。然后,解时需要确保只有的持有者才能释放,通常用Lua脚本来保证操作的原子性。 接下来,我需要考虑Go语言中的具体实现。用户可能希望看到具体的代码示例,比如使用github.com/go-redis/redis库来连接Redis,并编写加和解的函数。同时,还要处理的超时和自动续期问题,防止因为业务执行时间过长导致自动释放。此外,可重入可能也是一个需要考虑的高级特性,但根据引用内容,用户提供的引用中有提到可重入实现,但可能用户更关注基础实现,所以可能需要先介绍基本方法,再提及可重入的扩展。 然后,引用中提到了redsync这个库,根据引用3,官方推荐使用redsync,所以可能需要介绍这个库的使用方法,并给出示例代码。同时,要比较手动实现和使用库的优缺点,比如redsync支持多Redis实例的RedLock算法,提高可靠性,而单实例实现更简单但可能有单点故障风险。 另外,需要确保回答覆盖最佳实践,比如设置合理的超时时间、避免长时间阻塞、处理网络分区等。还要注意错误处理,比如加失败后的重试策略,以及解时可能遇到的错误。 最后,根据用户提供的引用,特别是引用1、2、3,需要正确引用相关内容,比如引用1提到使用SET命令,引用2提到Lua脚本和可重入,引用3提到redsync库。因此,在回答中要自然地插入这些引用,确保信息的准确性和来源的可信度。 可能还需要生成相关问题,比如如何实现可重入性,如何处理的自动续期,RedLock算法的原理等。这些问题可以帮助用户进一步深入学习。 总结步骤:介绍基本实现方法,给出代码示例;介绍redsync库的使用;讨论最佳实践和注意事项;引用相关资料;生成相关问题。</think>### Go语言基于Redis实现分布式锁的最佳实践 #### 一、基础实现方法(单Redis实例) 1. **加** 使用`SET`命令的`NX`(不存在时设置)和`PX`(过期时间)参数: ```go func AcquireLock(client *redis.Client, key string, value string, ttl time.Duration) (bool, error) { result, err := client.SetNX(context.Background(), key, value, ttl).Result() return result, err } ``` - `key`为的唯一标识,`value`通常为随机字符串(如UUID),用于安全释放[^1]。 2. **解** 使用Lua脚本保证原子性操作: ```lua if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end ``` Go代码实现: ```go func ReleaseLock(client *redis.Client, key string, value string) (bool, error) { script := redis.NewScript(` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `) result, err := script.Run(context.Background(), client, []string{key}, value).Int64() return result == 1, err } ``` **关键点**:验证`value`匹配后才删除,避免误删其他客户端的[^2]。 --- #### 二、使用Redsync库(推荐生产环境) Redis官方推荐的Go实现库`redsync`支持多节点容错(RedLock算法): 1. **安装依赖** ```bash go get github.com/go-redsync/redsync/v4 go get github.com/gomodule/redigo/redis ``` 2. **示例代码** ```go package main import ( "github.com/gomodule/redigo/redis" "github.com/go-redsync/redsync/v4" "github.com/go-redsync/redsync/v4/redis/redigo" ) func main() { // 创建Redis连接池 pool := redigo.NewRedisPool(&redis.Pool{ Dial: func() (redis.Conn, error) { return redis.Dial("tcp", "localhost:6379") }, }) // 创建Redsync实例 rs := redsync.New(pool) // 获取 mutex := rs.NewMutex("my-distributed-lock", redsync.WithExpiry(10*time.Second)) if err := mutex.Lock(); err != nil { panic(err) } // 执行业务逻辑 // ... // 释放 if ok, err := mutex.Unlock(); !ok || err != nil { panic("unlock failed") } } ``` **优势**: - 自动处理续期(需配合`WithTries`配置重试) - 支持多Redis实例的RedLock算法(需传递多个连接池)[^3] --- #### 三、最佳实践 1. **设置合理的超时时间** 根据业务逻辑耗时设置`ttl`,避免长期未释放导致死。 2. **避免单点故障** 生产环境建议使用Redis集群或RedLock算法(多实例部署)。 3. **错误处理** - 加失败时实现重试机制(如指数退避) - 解失败需记录日志并人工介入检查 4. **续期** 对于长耗时任务,需后台定期续期: ```go func ExtendLock(mutex *redsync.Mutex) { go func() { for { select { case <-time.After(5 * time.Second): ok, err := mutex.Extend() if !ok || err != nil { return } } } }() } ``` --- #### 四、注意事项 - **网络分区问题**:Redis主从切换可能导致状态不一致,RedLock可缓解此问题 - **时钟同步**:多实例部署时需确保机器时钟同步 - **性能影响**:频繁加可能增加Redis负载,需监控QPS ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

徐同学呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值