概述
- 在分布式系统中,并发控制和数据一致性是至关重要的问题
- 当多个服务或进程需要访问和修改共享资源时,我们必须确保在同一时间只有一个服务或进程能够执行操作,以防止数据竞争和不一致,这就是分布式锁要解决的问题
- Redis 作为一个高性能的键值存储系统,经常被用作实现分布式锁的工具。
- Redis 的 SETNX、EXPIRE、DEL 等命令可以组合起来实现一个简单的分布式锁,但是,直接使用这些命令可能会引入一些复杂的逻辑和潜在的错误
- 因此,许多开发者选择使用现成的库来简化分布式锁的实现
- 在 Go 语言中,github.com/go-redsync/redsync 是一个流行的库,它基于 Redis 提供了分布式锁的抽象和实现
Redis 分布式锁的基本原理
Redis 分布式锁通常基于以下原理
- 加锁:使用 SETNX 命令尝试设置一个键值对,如果键不存在则设置成功(返回 1),否则设置失败(返回 0)。设置成功的进程获得了锁
- 设置过期时间:为了防止死锁,通常会为锁设置一个过期时间,使用 EXPIRE 命令
- 解锁:当进程完成操作后,需要删除之前设置的键值对来释放锁,使用 DEL 命令
- 处理锁过期:如果持有锁的进程在锁过期之前未能完成操作并释放锁,其他进程可以重新获取锁
示例程序
go
package main
import (
"fmt"
"time"
"github.com/go-redsync/redsync/v4"
"github.com/go-redsync/redsync/v4/redis/goredis/v9"
goredislib "github.com/redis/go-redis/v9"
"sync"
)
func RedisLock(wg *sync.WaitGroup) {
// 初始化锁客户端
client := goredislib.NewClient(&goredislib.Options{
Addr: "127.0.0.1:6380",
Password: "123456_redis",
Username: "root",
DB: 0,
})
pool := goredis.NewPool(client) // or, pool := redigo.NewPool(...)
rs := redsync.New(pool)
// 初始化锁
mutexname := "my-global-mutex"
mutex := rs.NewMutex(mutexname, redsync.WithExpiry(30*time.Second))
// 开始锁定
fmt.Println("Lock()....")
if err := mutex.Lock(); err != nil {
panic(err)
}
// 自己的一些业务逻辑
fmt.Println("Get Lock!!!")
time.Sleep(time.Second * 1)
// 开始解锁
fmt.Println("UnLock()")
if ok, err := mutex.Unlock(); !ok || err != nil {
panic("unlock failed")
}
fmt.Println("Released Lock!!!")
}
func main() {
// 测试程序
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go RedisLock(&wg)
}
wg.Wait()
}
-
程序运行,输出
Lock().... Lock().... Lock().... Get Lock!!! UnLock() Released Lock!!! Get Lock!!! UnLock() Released Lock!!! Get Lock!!! UnLock() Released Lock!!!
-
前三个Lock() 代表程序在排队阶段
-
后面接着三次输出,每次都有: Get Lock!!!, UnLock(), Released Lock!!!
-
每个过程都是 获取锁,解锁,释放锁
-
所以在高并发的时候,使用redis的分布式锁可以解决资源数据的竞争
-
特别注意的是:
mutexname := "my-global-mutex"
这个锁的名称在 redis 中使用的时候会生成,当锁被全部释放后,就会自动释放,而且这个锁的名称最好也自行规范下go// 以下是 docker 内的redis容器cli 127.0.0.1:6379> keys * 1) "my-global-mutex" // 稍后再次查询后,锁的key和value消失 127.0.0.1:6379> keys * (empty array)
-
另外,如果锁在规定的时间内没有完成工作,那么在设定的过期时间后就会自动结束,如果不设置过期时间,走系统内的过期时间
使用 redsync 实现 Redis 分布式锁
- redsync 库提供了一个高级接口,使得在 Go 语言中使用 Redis 分布式锁变得更加简单
- 下面是如何使用 redsync 实现分布式锁的步骤:
- 初始化 Redis 客户端:首先,你需要初始化一个 Redis 客户端。在上述代码中,使用了 github.com/redis/go-redis/v9 库来创建 Redis 客户端,并为其设置了地址、密码、用户名和数据库编号
- 创建锁池:redsync 需要一个锁池(Pool)来管理锁,使用
goredis.NewPool
方法来创建一个基于 go-redis 客户端的锁池 - 创建分布式锁:使用
redsync.New
方法来创建一个Redsync
实例,并使用NewMutex
方法来创建一个分布式锁,可以为锁指定一个名称,并设置锁的过期时间。 - 加锁:调用 Lock 方法来尝试获取锁,如果加锁失败(例如,其他进程已经持有锁),则该方法会返回一个错误
- 执行业务逻辑:在成功获取锁之后,执行您的业务逻辑
- 解锁:完成业务逻辑后,调用 Unlock 方法来释放锁,请注意,Unlock 方法会返回一个布尔值和一个错误,布尔值表示是否成功释放了锁,而错误则表示在解锁过程中是否发生了错误
注意事项
- 死锁:务必为锁设置合理的过期时间,以防止死锁
- 重试机制:当加锁失败时,您可能需要实现一个重试机制来等待一段时间后再次尝试加锁
- 解锁失败:虽然 Unlock 方法会尝试释放锁,但在某些情况下(例如,Redis 实例崩溃或网络问题),解锁可能会失败。因此,您应该始终检查 Unlock 方法的返回值,并在必要时处理解锁失败的情况
- 并发控制:在分布式系统中,除了使用分布式锁之外,您还需要考虑其他并发控制策略,例如消息队列、事件驱动架构等
- 安全性:确保 Redis 实例的安全性,包括使用强密码、限制访问权限等。此外,如果您的系统对安全性有更高的要求,您可能需要考虑使用更复杂的分布式锁实现,例如基于 ZooKeeper 或 etcd 的分布式锁