Redis分布式锁的应用场景与基本原理
分布式锁是微服务架构中保证共享资源互斥访问的核心机制。单机环境下用语言内置锁(如Go的sync.Mutex)即可,分布式环境下多个服务实例跨进程运行,需要借助Redis等中间件实现跨进程锁。Redis分布式锁在后端开发中应用广泛,涵盖库存扣减、订单防重复提交、定时任务防并发执行、资源配额控制等场景。
Redis分布式锁的基本思路是利用SET命令的NX(Not Exists)参数:只有key不存在时才能设置成功,设置成功即获取锁,删除key即释放锁。为保证锁的自动过期防止死锁,同时设置过期时间。value设置为唯一标识(如UUID),释放锁时校验value防止误删他人锁。高并发设计场景下,分布式锁是保障数据一致性的基础组件。
SET NX实现原子加锁与超时自动释放
最基础的Redis分布式锁用SET key value NX PX timeout一条命令完成。NX保证互斥,PX指定过期时间(毫秒)。value设置为唯一标识(如UUID),释放锁时校验value防止误删他人锁。
package main
import (
"context"
"fmt"
"time"
"github.com/redis/go-redis/v9"
"github.com/google/uuid"
)
type RedisLock struct {
client *redis.Client
key string
value string
}
func NewRedisLock(client *redis.Client, key string) *RedisLock {
return &RedisLock{
client: client,
key: key,
value: uuid.New().String(),
}
}
// TryLock 尝试获取锁,非阻塞
func (l *RedisLock) TryLock(ctx context.Context, ttl time.Duration) (bool, error) {
ok, err := l.client.SetNX(ctx, l.key, l.value, ttl).Result()
return ok, err
}
// Unlock 释放锁,使用Lua脚本保证原子性
func (l *RedisLock) Unlock(ctx context.Context) 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(ctx, l.client, []string{l.key}, l.value).Int64()
if err != nil {
return err
}
if result == 0 {
return fmt.Errorf("锁已被其他持有者释放或已过期")
}
return nil
}
释放锁必须用Lua脚本实现原子操作。如果先GET判断value再DEL,两步之间存在时间窗口:判断通过后锁可能过期被其他客户端获取,此时DEL会删除他人的锁。Lua脚本在Redis中原子执行,避免竞态条件。分布式事务场景中,分布式锁配合Saga模式可以保证关键操作的互斥性。
锁续约机制:看门狗自动续期
固定过期时间存在矛盾:设太短,业务未完成锁就过期,其他客户端可能获取锁导致并发问题;设太长,持锁进程崩溃后锁长时间不释放。看门狗(Watchdog)机制通过后台goroutine定期续期解决此问题。
type WatchdogLock struct {
*RedisLock
ctx context.Context
cancel context.CancelFunc
ttl time.Duration
}
// Lock 获取锁并启动看门狗
func (w *WatchdogLock) Lock(ctx context.Context, ttl time.Duration) error {
w.ttl = ttl
w.ctx, w.cancel = context.WithCancel(ctx)
// 尝试获取锁
ok, err := w.TryLock(w.ctx, ttl)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("获取锁失败")
}
// 启动看门狗goroutine
go w.watchdog()
return nil
}
func (w *WatchdogLock) watchdog() {
ticker := time.NewTicker(w.ttl / 3) // 每1/3 TTL续期一次
defer ticker.Stop()
for {
select {
case <-w.ctx.Done():
return
case <-ticker.C:
// Lua脚本续期:校验value后刷新TTL
script := redis.NewScript(`
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
`)
result, err := script.Run(w.ctx, w.client, []string{w.key},
w.value, w.ttl.Milliseconds()).Int64()
if err != nil || result == 0 {
w.cancel()
return
}
}
}
}
func (w *WatchdogLock) Unlock(ctx context.Context) error {
w.cancel() // 停止看门狗
return w.RedisLock.Unlock(ctx)
}
看门狗每TTL/3时间续期一次,续期失败(锁已被释放或网络异常)则停止续约并通知业务层。持锁进程正常退出时调用Unlock释放锁并停止看门狗;进程崩溃时看门狗随之终止,锁在TTL后自动过期。Redisson的Java客户端也采用相同机制。服务治理中需要对分布式锁的使用做监控告警,锁等待超时和续期失败都应该有告警通知。
Redlock算法:多节点容错锁方案
单Redis实例存在单点故障:主从切换时锁可能丢失。Redlock算法由Redis作者Antirez提出,通过多个独立Redis实例实现容错锁。核心思想是在N个(通常5个)独立Redis节点上同时获取锁,超过半数(N/2+1)成功即认为获取锁成功。
type Redlock struct {
clients []*redis.Client
quorum int
}
func NewRedlock(addrs []string) *Redlock {
clients := make([]*redis.Client, len(addrs))
for i, addr := range addrs {
clients[i] = redis.NewClient(&redis.Options{Addr: addr})
}
return &Redlock{
clients: clients,
quorum: len(addrs)/2 + 1,
}
}
func (rl *Redlock) Lock(ctx context.Context, key, value string, ttl time.Duration) (bool, error) {
success := 0
start := time.Now()
for _, client := range rl.clients {
ok, err := client.SetNX(ctx, key, value, ttl).Result()
if err == nil && ok {
success++
}
}
// 检查是否达到法定数量,且耗时未超过TTL
elapsed := time.Since(start)
if success >= rl.quorum && elapsed < ttl {
return true, nil
}
// 获取失败,释放已获取的锁
rl.unlockAll(ctx, key, value)
return false, nil
}
func (rl *Redlock) unlockAll(ctx context.Context, key, value string) {
script := redis.NewScript(`
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
return 0
`)
for _, client := range rl.clients {
script.Run(ctx, client, []string{key}, value)
}
}
Redlock的争议在于时钟漂移和GC暂停可能导致锁失效。Martin Kleppmann在文章中指出,STW垃圾回收暂停期间锁可能过期,恢复后操作基于过期锁执行。实践中对于强一致性要求的场景,应考虑使用etcd或ZooKeeper,其基于lease和fencing token的机制更可靠。Redis分布式锁适合对性能要求高、短暂不一致可容忍的场景。消息中间件场景中,分布式锁常用于保证消息消费的幂等性。
分布式锁的常见陷阱与最佳实践
锁粒度控制:锁粒度越细并发度越高,但管理复杂度也越高。库存扣减按商品ID加锁,订单防重按用户ID+操作类型加锁。避免全局锁导致串行化瓶颈。API接口规范中应明确定义哪些操作需要分布式锁保护。
锁超时与重试策略:获取锁失败时不应无限重试导致雪崩。指数退避重试(间隔1s、2s、4s…上限10s)是合理策略。设置最大重试次数和总超时时间。
可重入锁:同一线程多次获取同一锁不应死锁。用Hash结构存储锁,key为锁名,field为线程标识,value为重入次数。获取时检查field是否匹配,匹配则value+1,释放时value-1,减到0删除key。
监控告警:锁竞争激烈或获取失败率高可能是业务异常信号。记录锁获取耗时、等待时间、失败次数等指标,接入Prometheus监控。业务中台建设中,分布式锁的使用应该有统一封装和治理面板。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/redis-fen-bu-shi-suo-shi-xian-yuan-li-redlock-suan-fa-yu-go/