在分布式锁的设计中,"互斥性" 保证了同一时间只有一个节点能操作资源,而 "安全性" 则确保了锁不会被无关节点误删。本文将聚焦于 Redis 分布式锁的安全性问题,深入解析如何通过巧妙设计 value 值和原子操作,防止锁被其他协程或节点误删除。
一、为什么会出现锁被误删的情况?
你的笔记中提到 "过期时间可能导致锁被误删",这是分布式锁实现中一个典型的安全隐患。让我们通过一个具体场景理解这个问题:
- 客户端 A 获取锁,设置 value 为 "lock-value-A",过期时间 8 秒
- 客户端 A 的业务逻辑执行缓慢,8 秒后锁自动过期释放
- 客户端 B 此时成功获取锁,设置 value 为 "lock-value-B"
- 客户端 A 的业务逻辑终于执行完成,执行DEL命令释放锁
- 结果:客户端 A 删除了客户端 B 持有的锁,导致新的客户端 C 也能获取到锁,引发并发问题
问题的核心在于:释放锁时没有验证锁的归属权。如果只是简单执行DEL命令,任何知道锁 key 的客户端都能删除锁,这显然不符合分布式锁的安全要求。
二、解决方案:用唯一 value 标识锁的归属
解决锁误删问题的关键思路是:给每个锁设置唯一的 value 值,释放锁时先验证 value 是否匹配,只有匹配时才删除锁。
1. 唯一 value 的生成策略
value 需要满足 "全局唯一性",确保不同客户端、不同时间获取的锁具有不同标识。常见的生成方式有:
- UUID/GUID:通用唯一识别码,如uuid.New().String()
- 客户端 ID + 时间戳:如"client-123:" + time.Now().UnixNano()
- 随机数 + 进程 ID:如"pid-456:" + rand.Intn(1000000)
在 Go 语言中生成 UUID 的示例:
import (
"github.com/google/uuid"
)
// 生成唯一锁值
func generateLockValue() string {
return uuid.New().String()
}
这个 value 会在获取锁时作为SET命令的参数存入 Redis,成为锁的 "身份证"。
2. 带 value 验证的锁释放流程
释放锁时需要执行两个操作:
- 获取锁的当前 value
- 比较 value 是否与自己持有的一致,一致则删除
但这两个操作如果分开执行,在高并发场景下仍然可能出现问题(如刚获取到 value,锁就过期被其他客户端获取)。因此必须保证这两个操作的原子性。
Redis 的 Lua 脚本功能正好满足这一需求 ——Redis 会将整个 Lua 脚本作为一个原子操作执行,中间不会被其他命令打断。
释放锁的 Lua 脚本如下:
-- 释放锁的Lua脚本
-- KEYS[1]:锁的key
-- ARGV[1]:当前客户端持有的value
if redis.call('GET', KEYS[1]) == ARGV[1] then
-- value匹配,删除锁
return redis.call('DEL', KEYS[1])
else
-- value不匹配,不做操作
return 0
end
这个脚本的逻辑很清晰:只有当 Redis 中锁的当前值与客户端持有的值完全一致时,才会执行删除操作,否则直接返回 0(不做任何处理)。
三、完整实现:从获取到释放的全流程
结合前面的知识,我们可以实现一个安全的分布式锁,包含完整的 value 验证机制。
1. Go 语言实现示例
package main
import (
"context"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)
type RedisLock struct {
client *redis.Client
lockKey string // 锁的key
lockValue string // 锁的唯一value
expireTime time.Duration // 过期时间
}
// 创建新锁实例
func NewRedisLock(client *redis.Client, key string, expireTime time.Duration) *RedisLock {
return &RedisLock{
client: client,
lockKey: key,
expireTime: expireTime,
}
}
// 获取锁
func (rl *RedisLock) Lock(ctx context.Context) (bool, error) {
// 生成唯一value
rl.lockValue = uuid.New().String()
// 执行SET NX命令,同时设置过期时间
result, err := rl.client.SetNX(
ctx,
rl.lockKey,
rl.lockValue,
rl.expireTime,
).Result()
return result, err
}
// 释放锁(带value验证)
func (rl *RedisLock) Unlock(ctx context.Context) (bool, error) {
// 释放锁的Lua脚本
script := `
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end
`
// 执行Lua脚本
result, err := rl.client.Eval(
ctx,
script,
[]string{rl.lockKey}, // KEYS参数
rl.lockValue // ARGV参数
).Int64()
if err != nil {
return false, err
}
// 返回是否成功释放锁
return result == 1, nil
}
// 获取当前锁的value(用于调试)
func (rl *RedisLock) GetCurrentValue(ctx context.Context) (string, error) {
return rl.client.Get(ctx, rl.lockKey).Result()
}
2. 关键实现解析
- Lock 方法:获取锁时生成唯一 value,并通过SetNX命令原子性地设置 key、value 和过期时间
- Unlock 方法:使用 Lua 脚本实现 "验证 value + 删除锁" 的原子操作,确保只有锁的持有者才能释放锁
- value 存储:将 value 保存在 RedisLock 实例中,用于释放锁时的验证
这种实现彻底解决了 "锁被误删" 的问题:即使锁过期后被其他客户端获取,原客户端在释放时会发现 value 不匹配,从而放弃删除操作。
四、进阶思考:value 设计的额外用途
除了防止误删,锁的 value 还可以承载更多信息,提升分布式锁的可观测性和功能性:
1. 携带持有者信息
将客户端 ID、进程 ID 等信息编码到 value 中,便于问题排查:
func generateLockValue(clientID string) string {
// 格式:客户端ID:进程ID:UUID
return fmt.Sprintf("%s:%d:%s", clientID, os.Getpid(), uuid.New().String())
}
当出现锁异常时,可以通过GET lockKey命令查看当前持有者信息,快速定位问题节点。
2. 支持可重入锁
可重入锁允许同一客户端多次获取同一把锁(类似 Java 的 ReentrantLock)。实现方式是在 value 中记录获取次数:
// 可重入锁的value格式:"clientID:count"
func (rl *ReentrantLock) Lock(ctx context.Context) (bool, error) {
// 尝试获取锁
currentValue, err := rl.client.Get(ctx, rl.lockKey).Result()
if err == redis.Nil {
// 锁不存在,第一次获取
return rl.client.SetNX(ctx, rl.lockKey, rl.baseValue+":1", rl.expireTime).Result()
}
// 检查是否是当前客户端持有锁
if strings.HasPrefix(currentValue, rl.baseValue) {
// 解析当前计数并加1
count, _ := strconv.Atoi(strings.Split(currentValue, ":")[1])
newCount := count + 1
// 更新计数(需要保证原子性,可使用Lua脚本)
return true, rl.client.Set(ctx, rl.lockKey, fmt.Sprintf("%s:%d", rl.baseValue, newCount), rl.expireTime).Err()
}
// 其他客户端持有锁,获取失败
return false, nil
}
释放时则递减计数,当计数为 0 时才真正删除锁。
五、总结:分布式锁安全的核心原则
防止锁被其他协程误删,本质上是解决 "锁的归属权验证" 问题。总结几个关键原则:
- 唯一标识原则:每个锁必须有唯一的 value,作为持有者的身份凭证
- 原子操作原则:释放锁时的 "验证 + 删除" 必须是原子操作,Lua 脚本是最佳选择
- 最小权限原则:只有锁的持有者才能释放锁,任何情况下都不允许越权操作
在实际开发中,建议直接使用经过验证的开源库(如 Redisson、go-redsync),它们已经妥善处理了这些安全细节。理解这些原理的价值在于:当遇到锁相关的异常时,能够快速定位问题根源,而不是停留在 "API 调用" 的层面。
分布式锁的安全性是一个细节决定成败的领域,一个小小的疏漏就可能导致整个分布式系统的并发控制失效。通过本文的学习,希望你能深刻理解 value 验证机制的重要性,在设计和使用分布式锁时始终保持对 "安全性" 的敬畏。
作者:Code季风
链接:https://juejin.cn/post/7536319934542381119
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

被折叠的 条评论
为什么被折叠?



