【Redis 】看门狗:分布式锁的自动续期

发布于:2025-07-21 ⋅ 阅读:(20) ⋅ 点赞:(0)

在分布式系统的开发中,保证数据的一致性和避免并发冲突是至关重要的任务。Redis 作为一种广泛使用的内存数据库,提供了实现分布式锁的有效手段。然而,传统的 Redis 分布式锁在设置了过期时间后,如果任务执行时间超过了锁的有效期,就会出现锁提前释放,导致并发问题。为了解决这一难题,看门狗机制应运而生。

一、Redis 分布式锁基础回顾

Redis 分布式锁通常基于 Redis 的单线程特性和原子操作来实现。最常见的方式是使用SET key value NX PX timeout命令。其中,NX表示只有当key不存在时才进行设置操作,保证了锁的唯一性;PX timeout则设置了锁的过期时间(单位为毫秒),防止因程序异常导致锁无法释放而产生死锁。例如:

SET my_lock unique_value NX PX 30000

上述命令尝试在 Redis 中设置一个名为my_lock的锁,值为unique_value(通常是一个唯一标识,如线程 ID 或 UUID),并且设置锁的过期时间为 30 秒。当一个客户端成功执行该命令,就表示它获取到了锁。在任务完成后,客户端需要通过DEL命令释放锁:

DEL my_lock

但这里存在一个问题,如果任务执行时间超过了 30 秒,锁会自动过期并被 Redis 删除,此时其他客户端就有可能获取到同一把锁,导致并发安全问题。

二、看门狗机制原理

看门狗(Watchdog)机制是一种用于自动延长 Redis 分布式锁有效期的解决方案。其核心思想是在持有锁的线程或进程内,启动一个后台线程(或定时任务),定期检查锁是否仍然由当前持有者持有。如果是,则通过 Redis 的PEXPIRE命令延长锁的过期时间,从而避免锁在任务执行过程中提前过期。

具体来说,当一个客户端成功获取到 Redis 分布式锁后,看门狗线程开始启动。该线程会按照一定的时间间隔(通常是锁过期时间的一部分,如 1/3 或 1/2)检查锁的状态。例如,如果锁的初始过期时间设置为 30 秒,看门狗线程可能每隔 10 秒检查一次。在每次检查时,它会执行类似于以下的操作:

# 检查锁是否仍由当前客户端持有(假设锁的值为unique_value)

if redis.call('GET', 'my_lock') == 'unique_value' then

# 延长锁的过期时间

redis.call('PEXPIRE','my_lock', 30000)

end

上述逻辑可以通过 Redis 的 Lua 脚本来实现,以确保检查和续期操作的原子性。这样,只要持有锁的任务还在执行,看门狗就会持续为锁续期,直到任务完成并释放锁。

三、

Redisson 中的看门狗实现及示例代码(Golang 版)​

在 Golang 生态中,虽然没有 Java 中 Redisson 那样原生的库,但可以通过go-redis客户端结合相关逻辑实现类似功能。以下是基于go-redis的示例代码:​

(一)引入依赖​

首先需要安装go-redis客户端:

go get github.com/go-redis/redis/v8

(二)golang代码示例

以下是一个使用 Redisson 实现分布式锁并利用看门狗自动续期的 Java 示例代码:

package main

import (
  context"
      
    me"

     hub.com/go-redis/redis/v8"
   ithub.com/google/uuid"
)

var ctx = context.Background()

func main() {
    配置Redis客户端
   b := redis.NewClient(&redis.Options{
   ddr:     "localhost:6379",
   assword: "", // 无密码
            0,  // 默认DB
   

      分布式锁
     Key := "my_distributed_lock"
      唯一标识
     ueValue := uuid.New().String()
    尝试获取锁,设置过期时间30秒
    kSuccess, err := rdb.SetNX(ctx, lockKey, uniqueValue, 30*time.Second).Result()
      r != nil {
    t.Printf("获取锁失败:%v\n", err)
       n
  
     lockSuccess {
      Println("获取锁失败,锁已被持有")
       n
  
     r func() {
     释放锁的Lua脚本
    leaseScript := `
        dis.call('GET', KEYS[1]) == ARGV[1] then
         n redis.call('DEL', KEYS[1])
      e
        rn 0
     d
   
     执行释放锁操作
      Eval(ctx, releaseScript, []string{lockKey}, uniqueValue)
   mt.Println("锁已释放")
    

     Println("获取到锁,开始执行业务逻辑...")

      看门狗协程自动续期
     Chan := make(chan struct{})
        go func() {
                ticker := time.NewTicker(10 * time.Second) // 每隔10秒检查一次
                defer ticker.Stop()
                for {
                        select {
                        case <-ticker.C:
                                // 检查锁是否仍由当前客户端持有
                                val, err := rdb.Get(ctx, lockKey).Result()
                                if err != nil || val != uniqueValue {
                                        // 锁已释放或不属于当前客户端,停止续期
                                        return
                                }
                                // 续期30秒
                                rdb.Expire(ctx, lockKey, 30*time.Second)
                                fmt.Println("看门狗:锁已续期")
                        case <-stopChan:
                                // 收到停止信号,退出
                                return
                        }
                }
        }()

        // 模拟业务逻辑执行(60秒)
        time.Sleep(60 * time.Second)
        fmt.Println("业务逻辑执行完毕")

        // 通知看门狗停止
        close(stopChan)
        // 关闭Redis客户端
        rdb.Close()
}   stop  // 启动    }() 

在上述代码中:​

  1. 使用go-redis客户端连接 Redis 服务器,并通过SetNX方法获取分布式锁,SetNX对应 Redis 的SET NX命令,第三个参数为过期时间。​
  1. 生成 UUID 作为锁的唯一标识,确保释放锁时的安全性。​
  1. 获取锁成功后,启动一个看门狗协程,通过定时器每隔 10 秒检查一次锁的状态。如果锁仍由当前客户端持有(通过对比 value 值),则调用Expire方法延长锁的过期时间。​
  1. 使用defer语句确保业务逻辑执行完毕后释放锁,释放锁通过 Lua 脚本实现,保证原子性。​
  1. 模拟 60 秒的业务逻辑执行,期间看门狗会自动续期,避免锁提前过期。​

(三)优势说明​

  1. 自动续期:通过 Golang 的协程和定时器实现看门狗功能,自动延长锁的有效期。​
  1. 安全性:使用 UUID 作为唯一标识,结合 Lua 脚本释放锁,避免误释放其他客户端的锁。​
  1. 简洁高效:基于go-redis客户端,代码简洁,性能高效。

四、

手动实现看门狗机制示例(纯 Golang 原生逻辑)​

如果不依赖第三方库,也可以通过 Golang 的net/http包中的 Redis 客户端相关逻辑手动实现,但实际开发中建议使用成熟的go-redis客户端。以下是更贴近手动实现思想的示例:

package main

import (
        "context"
        "fmt"
        "time"

        "github.com/go-redis/redis/v8"
        "github.com/google/uuid"
)

var ctx = context.Background()

// acquireLock 获取分布式锁
func acquireLock(rdb *redis.Client, lockKey, uniqueValue string, expireTime time.Duration) (bool, error) {
        return rdb.SetNX(ctx, lockKey, uniqueValue, expireTime).Result()
}

// releaseLock 释放分布式锁
func releaseLock(rdb *redis.Client, lockKey, uniqueValue string) error {
        releaseScript := `
                if redis.call('GET', KEYS[1]) == ARGV[1] then
                        return redis.call('DEL', KEYS[1])
                else
                        return 0
                end
        `
        _, err := rdb.Eval(ctx, releaseScript, []string{lockKey}, uniqueValue).Result()
        return err
}

// watchdog 看门狗协程,定期续期
func watchdog(rdb *redis.Client, lockKey, uniqueValue string, expireTime time.Duration, stopChan <-chan struct{}) {
        ticker := time.NewTicker(expireTime / 3) // 每隔过期时间的1/3检查一次
        defer ticker.Stop()
        for {
                select {
                case <-ticker.C:
                        val, err := rdb.Get(ctx, lockKey).Result()
                        if err != nil || val != uniqueValue {
                                return
                        }
                        // 续期
                        rdb.Expire(ctx, lockKey, expireTime)
                        fmt.Println("看门狗:锁已续期")
                case <-stopChan:
                        return
                }
        }
}

func main() {
        rdb := redis.NewClient(&redis.Options{
                Addr: "localhost:6379",
        })
        defer rdb.Close()

        lockKey := "my_distributed_lock"
        uniqueValue := uuid.New().String()
        expireTime := 30 * time.Second

        // 获取锁
        lockSuccess, err := acquireLock(rdb, lockKey, uniqueValue, expireTime)
        if err != nil || !lockSuccess {
                fmt.Println("获取锁失败")
                return
        }
        defer releaseLock(rdb, lockKey, uniqueValue)
        defer fmt.Println("锁已释放")

        fmt.Println("获取到锁,开始执行业务逻辑...")

        // 启动看门狗
        stopChan := make(chan struct{})
        go watchdog(rdb, lockKey, uniqueValue, expireTime, stopChan)

        // 模拟业务逻辑
        time.Sleep(60 * time.Second)
        fmt.Println("业务逻辑执行完毕")

        // 停止看门狗
        close(stopChan)
}

在这个示例中:

  1. acquire_lock函数使用redis_client.set方法尝试获取分布式锁,nx=True表示只有当锁不存在时才设置,ex=expire_time设置了锁的过期时间。
  1. release_lock函数通过 Redis 的 Lua 脚本实现了安全的锁释放操作,只有当锁的值与当前持有锁的唯一标识相同时才删除锁。
  1. watchdog函数是看门狗线程的执行函数,它每隔expire_time / 3秒检查一次锁是否仍由当前线程持有,如果是,则调用redis_client.expire方法延长锁的过期时间。
  1. 在main函数中,首先尝试获取锁。如果获取成功,启动看门狗线程,然后模拟业务逻辑执行 60 秒。最后在业务完成后,释放锁。

五、总结与注意事项

看门狗机制为 Redis 分布式锁的可靠性提供了重要保障,尤其适用于任务执行时间不确定或较长的场景。使用 Golang 实现时,需要注意以下几点:​

  1. 协程管理:Golang 中通过协程实现看门狗,需确保协程能正常退出,避免泄漏。可通过channel传递退出信号。​
  1. 唯一标识:必须使用唯一标识(如 UUID)作为锁的值,避免释放锁时误删其他客户端的锁。​
  1. 原子操作:检查锁状态和续期操作需保证原子性,虽然 Golang 中通过分步操作实现,但实际通过短间隔和唯一标识降低了冲突风险,更严谨的方式是使用 Lua 脚本。​
  1. 异常处理:需处理 Redis 连接异常、网络中断等情况,可在看门狗中增加重试机制或错误告警。​
  1. 集群环境:在 Redis 集群或主从架构中,需考虑数据同步问题,必要时结合 Redlock 等算法提升可靠性。​

总之,Golang 的协程和定时器特性非常适合实现 Redis 看门狗机制,通过合理的设计可以高效解决分布式锁的自动续期问题,保障分布式系统的并发安全。


网站公告

今日签到

点亮在社区的每一天
去签到