一、RedLock机制解决的问题
核心场景:解决传统Redis单节点/主从架构下分布式锁的不可靠问题。当主节点故障时,若从节点未同步锁信息,可能导致多个客户端同时持有锁,破坏互斥性。
典型问题案例:
- 主从切换锁丢失:主节点宕机后未同步锁到从节点,导致多客户端同时持有锁
- 网络分区误判:客户端与Redis集群出现网络隔离时产生的错误锁状态判断
- 单点故障风险:传统单节点Redis锁的可用性缺陷
二、RedLock核心机制
1. 前提条件
- 独立部署:至少部署5个(建议奇数个)完全独立的Redis实例(无主从/集群关系)
- 时钟同步:各节点时钟偏差需远小于锁的过期时间,即,各节点时钟偏差需 << 锁过期时间(建议使用NTP同步)
- 网络可靠:客户端与Redis实例间的网络延迟需可控(建议同机房部署)
2. 算法流程
-
获取锁:
- 获取当前时间(T1)
- 依次向所有实例请求加锁
SET lock_key $client_id NX PX $expire_time
- 计算总耗时:必须 < 锁有效时间(TTL - 时钟容差)
- 成功条件:若在多数节点(N/2+1)加锁成功,且总耗时(T2-T1)小于锁有效时间,则成功
-
持有锁:
- 业务处理时间必须 < 锁有效时间
- 自动续期机制维持锁活性
-
释放锁:
- 向所有实例发送Lua脚本删除锁(需验证client_id)
三、注意事项
注意事项 | 说明 | 解决方案 |
---|---|---|
GC暂停风险 | 客户端长时间GC可能导致锁过期后被其他客户端获取 | 使用独立锁续期线程(与业务逻辑分离) |
时钟漂移 | 极端情况下时钟跳跃可能导致锁提前失效 | 业务处理时间 + 时钟漂移 < 原TTL,建议设置TTL=业务预估时间*3 |
网络延迟控制 | 必须设置合理的锁超时时间(通常10-30ms级网络) | 加锁阶段若未获得多数成功,需立即回滚已获得的锁 |
客户端唯一标识 | 释放锁时必须验证client_id,防止误删他人锁 | 用UUID+客户端标识生成唯一锁值 |
四、哨兵模式实践方案
实现架构
Go代码示例(使用go-redis
库)
import (
"github.com/go-redis/redis/v8"
"context"
"time"
)
func acquireRedLock(ctx context.Context, clients []*redis.Client, lockKey string, clientID string, expireTime time.Duration) bool {
success := 0
start := time.Now()
for _, client := range clients {
ok, err := client.SetNX(ctx, lockKey, clientID, expireTime).Result()
if err == nil && ok {
success++
}
}
totalTime := time.Since(start)
return success > len(clients)/2 && totalTime < expireTime
}
func releaseRedLock(ctx context.Context, clients []*redis.Client, lockKey string, clientID string) {
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`
for _, client := range clients {
client.Eval(ctx, script, []string{lockKey}, clientID)
}
}
实现要点:
- 每个哨兵组使用独立的主节点作为RedLock实例
- 客户端监听哨兵的主切换通知,及时更新连接
- 主节点变更时需重新检查锁状态
五、Cluster模式实践方案
原生Cluster不适用RedLock机制原因
- 数据分片导致锁无法保证在多数节点存储
- 迁移槽位时可能导致锁丢失
变通方案对比
方案 | 优点 | 缺点 |
---|---|---|
固定哈希标签锁 | 实现简单 | 退化为单点锁,失去高可用性 |
集群模式+自定义分片 | 保持集群特性 | 需修改Redis内核,成本高 |
外挂RedLock节点组 | 不破坏现有集群 | 增加运维复杂度 |
推荐方案:部署独立的RedLock节点组,与业务Cluster分离
推荐替代方案:
- 使用ZooKeeper:基于ZAB协议的临时顺序节点
- 使用etcd:基于Raft协议实现强一致性锁
六、Golang完整实现(含自动续期)
package main
import (
"context"
"fmt"
"math/rand"
"sync"
"time"
"github.com/go-redis/redis/v8"
)
type RedLock struct {
clients []*redis.Client
key string
value string
ttl time.Duration
stopRenew chan struct{}
mu sync.Mutex
locked bool
}
func NewRedLock(clients []*redis.Client, key string, ttl time.Duration) *RedLock {
return &RedLock{
clients: clients,
key: key,
value: generateLockValue(),
ttl: ttl,
stopRenew: make(chan struct{}),
}
}
// 加锁(含自动续期)
func (rl *RedLock) Lock() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
if rl.locked {
return true
}
success := 0
start := time.Now()
ctx, cancel := context.WithTimeout(context.Background(), rl.ttl/2)
defer cancel()
for _, client := range rl.clients {
if ok, err := client.SetNX(ctx, rl.key, rl.value, rl.ttl).Result(); err == nil && ok {
success++
}
}
if success > len(rl.clients)/2 && time.Since(start) < rl.ttl/2 {
rl.locked = true
go rl.renewLock()
return true
}
rl.unlockInternal() // 清理未完成的锁
return false
}
// 自动续期
func (rl *RedLock) renewLock() {
ticker := time.NewTicker(rl.ttl / 3)
defer ticker.Stop()
for {
select {
case <-ticker.C:
success := 0
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end`
for _, client := range rl.clients {
res, err := client.Eval(ctx, script, []string{rl.key}, rl.value, rl.ttl.Milliseconds()).Result()
if err == nil && res.(int64) == 1 {
success++
}
}
cancel()
if success <= len(rl.clients)/2 {
rl.Unlock()
return
}
case <-rl.stopRenew:
return
}
}
}
// 解锁
func (rl *RedLock) Unlock() {
rl.mu.Lock()
defer rl.mu.Unlock()
if !rl.locked {
return
}
close(rl.stopRenew)
rl.unlockInternal()
}
// 内部解锁方法
func (rl *RedLock) unlockInternal() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
script := `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`
for _, client := range rl.clients {
client.Eval(ctx, script, []string{rl.key}, rl.value)
}
rl.locked = false
}
func generateLockValue() string {
return fmt.Sprintf("%d-%d", time.Now().UnixNano(), rand.Int())
}
// 使用示例
func main() {
clients := []*redis.Client{
redis.NewClient(&redis.Options{Addr: "redis1:6379"}),
redis.NewClient(&redis.Options{Addr: "redis2:6379"}),
redis.NewClient(&redis.Options{Addr: "redis3:6379"}),
}
lock := NewRedLock(clients, "order_lock", 10*time.Second)
if lock.Lock() {
defer lock.Unlock()
fmt.Println("执行关键业务逻辑...")
time.Sleep(15 * time.Second) // 测试自动续期
} else {
fmt.Println("获取锁失败")
}
}
七、关键实现说明
- 唯一锁标识:结合时间戳和随机数生成唯一value
- 续期机制:
- 独立goroutine定时续期(间隔=TTL/3)
- 续期时验证锁所有权
- 续期失败自动释放锁
- 超时控制:
- 加锁操作总超时=TTL/2
- 每次Redis操作单独设置超时
- 异常处理:
- 网络错误自动忽略(符合RedLock设计)
- 最终一致性释放锁
八、各场景推荐方案
场景 | 推荐方案 | 锁有效性保障 |
---|---|---|
跨机房部署 | 独立RedLock节点组+专线 | 网络延迟<30ms |
云环境 | 同可用区多实例 | 结合云商SLA |
混合部署 | RedLock节点组+ZooKeeper备份 | 双锁机制 |
超高并发 | RedLock+本地排队 | 减少Redis压力 |
九、RedLock适用性总结
场景 | 推荐方案 | 注意事项 |
---|---|---|
独立多实例环境 | 原生RedLock | 确保节点独立性和网络低延迟 |
哨兵模式 | 多哨兵组+RedLock | 需监控主节点切换 |
Cluster模式 | 禁用,改用ZooKeeper | 避免破坏集群分片逻辑 |
高时钟精度要求场景 | 结合NTP服务同步 | 时钟漂移需小于锁有效期1/3 |
十、性能优化建议
- 批量执行:使用pipeline批量发送加锁命令
- 渐进式回退:加锁失败时采用指数退避重试
- 监控指标:
- 锁获取平均耗时
- 续期成功率
- 锁冲突次数
- 连接池优化:为锁操作配置专用连接池
该实现完整覆盖了RedLock核心需求,通过自动续期机制解决了长时间操作可能导致的锁过期问题,同时保持了与Redis官方规范的一致性。在实际生产环境中,建议结合监控系统和熔断机制增强稳定性。