87.Go Redis实现可重入、自动续期分布式锁

本文详细介绍了如何使用Go语言和Redis实现分布式锁,包括基本的Lock和Unlock功能,以及添加了可重入性和自动续期的改进版本。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

86.分布式锁理论分析 中我们介绍了分布式锁的原理、"坑"以及解决办法。本文就给一下代码示例:

一、Redis实现分布式锁

package main

import (
  "fmt"
  "github.com/go-redis/redis"
  "time"
)

var client = redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", 
  DB:       0,  
})

func Lock(key string, value string, expiration time.Duration) (bool, error) {
  return client.SetNX(key, value, expiration).Result()
}

func Unlock(key string, value string) error {
  val, err := client.Get(key).Result()
  if err != nil {
    return err
  }
  // 避免释放它人的锁
  if val == value {
    return client.Del(key).Err()
  }
  return fmt.Errorf("key %s is locked by another value %s", key, val)
}

func main() {
  // my-value应该保证全局唯一,如uuid,雪花算法产生的ID等
  ok, err := Lock("my-key", "my-value", 10*time.Second)
  if err != nil {
    // handle error
  }
  if !ok {
    fmt.Println("Lock failed")
  } else {
    fmt.Println("Lock success")
    defer Unlock("my-key", "my-value")
    // do your job
   
  }
}

在上面的代码中,我们定义了两个函数:LockUnlockLock函数会尝试在Redis中设置一个键值对,并设置一个过期时间。如果这个键值对已经被其他地方设置了,那么SetNX函数会返回false,否则返回true

Unlock函数则尝试删除这个键值对,但是在删除之前,会检查这个键值对的值是否符合输入的值,如果不符合,那么认为这个锁已经被其他地方获取,这时就不应该删除这个键值对。

二、可重入与自动续期

上面例子,锁缺失两个重要的性质:一个是可重入,一个是如何实现到期自动续期逻辑。

package main

import (
  "sync"
  "time"

  "github.com/go-redis/redis"
  "github.com/satori/go.uuid"
)

var client = redis.NewClient(&redis.Options{
  Addr:     "localhost:6379",
  Password: "", // no password set
  DB:       0,  // use default DB
})

type Lock struct {
  key        string
  value      string
  expiration time.Duration
  mu         sync.Mutex
  isLocked   bool
  count      int
}

func NewLock(key string, expiration time.Duration) *Lock {
  return &Lock{
    key:        key,
    value:      uuid.NewV4().String(), // 这里使用uuid作为value
    expiration: expiration,
  }
}

func (l *Lock) Lock() (bool, error) {
  l.mu.Lock()
  defer l.mu.Unlock()

  if l.isLocked {
    l.count++
    return true, nil
  }

  ok, err := client.SetNX(l.key, l.value, l.expiration).Result()
  if err != nil || !ok {
    return false, err
  }

  l.isLocked = true
  l.count++

  go l.renew()
  return true, nil
}

func (l *Lock) Unlock() error {
  l.mu.Lock()
  defer l.mu.Unlock()

  if !l.isLocked {
    return nil
  }

  l.count--

  if l.count > 0 {
    return nil
  }

  val, err := client.Get(l.key).Result()
  if err != nil {
    return err
  }
  // 保证释放的是自己的锁
  if val != l.value {
    return nil
  }

  l.isLocked = false
  return client.Del(l.key).Err()
}

func (l *Lock) renew() {
  ticker := time.NewTicker(l.expiration / 2)
  for range ticker.C {
    l.mu.Lock()

    if !l.isLocked {
      ticker.Stop()
      l.mu.Unlock()
      break
    }

    client.Expire(l.key, l.expiration)
    l.mu.Unlock()
  }
}

func main() {
  lock := NewLock("my-key", 10*time.Second)
  locked, err := lock.Lock()
  if err != nil {
    panic(err)
  }
  if !locked {
    return
  }
  defer lock.Unlock()
  // do something
}

这段代码中,Lock结构体中新加入了muisLockedcount字段,分别表示互斥锁、是否已经锁定还有重入次数。当再次获取锁的时候,已经锁定则重入次数增加,否则尝试获取锁。在unlock时,如果重入次数大于零,则直接减少重入次数而不释放锁。

同时加入了renew函数,这个函数会每过一段时间检查这个锁是否已经被释放,未被释放则续期,并在锁释放后停止续期。

<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 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值