Golang 基于 Redis 的分布式锁实现与安全释放
在 Go 语言并发编程中,解决协程资源竞争可使用 sync.Mutex。但在微服务或分布式架构下,需借助 Redis 实现分布式锁。本文展示如何利用 Redigo 客户端结合 Lua 脚本,安全地处理锁的获取、持有、自动过期以及防止误删的逻辑。
分布式场景下的锁需求
在单机应用中,我们通常使用 sync.Mutex 或信道(Channel)来保护共享资源。例如,当多个协程同时自增一个全局变量时,不加锁会导致竞态条件:
numberFlag := 0
wg := new(sync.WaitGroup)
for i := 0; i < 200; i++ {
wg.Add(1)
go func() {
defer wg.Done()
numberFlag++ // 非原子操作,存在竞态
}()
}
wg.Wait()
fmt.Println(numberFlag) // 结果不确定
虽然可以使用 Channel 模拟信号量作为简单锁:
myLock := make(chan struct{}, 1)
// ... myLock<- struct{}{} (抢锁)
// ... <-myLock (释放),但这种方式仅适用于同一进程内的通信。
在分布式系统中,不同节点的 Golang 进程无法共享内存。此时需要使用 Redis 或 ZooKeeper 等中间件。ZooKeeper 虽可通过临时节点保证强一致性,但维护成本高;而 Redis 因广泛使用且性能好,是分布式锁的首选方案。
基础 Redis 锁的缺陷
一种常见的思路是使用 Redis 的 SETNX(Set if Not Exists)加过期时间来实现锁。以下是基础版本:
// 尝试获取锁
reply, err := c.Do("SET", lockName, 2, "EX", ExpTime, "NX")
// 业务处理 ...
// 释放锁
_, err = c.Do("DEL", lockName)
此方法在实际生产中存在两个致命问题:
- 锁误释放(AOP问题):如果业务执行时间超过了锁的过期时间,锁自动释放后,其他协程可能会抢到此锁。当原协程业务结束去释放锁时,会错误地删除当前持有者的锁。
- 非原子操作:先判断 KEY 是否存在,再 SET,最后 DEL,这些步骤分步执行中间若有异常或网络延迟,会导致逻辑状态不一致。
使用 Lua 脚本保证原子性
为了解决上述问题,必须使用 Redis 执行 Lua 脚本。Redis 保证脚本执行的原子性,且可以通过传入唯一的标识值(如 UUID+时间戳),确保只有加锁者才能解锁。
第1步:生成唯一锁标识
每个请求获取锁时,需要生成一个全局唯一的 Value。格式建议包含服务名、时间戳及随机字符串,以保证唯一性。
func getLockOnlyValue(nowGroName string) string {
nano := strconv.FormatInt(time.Now().UnixNano(), 10)
return nowGroName + "_" + nano + "_" + RandStringRunes(6)
}
第2步:原子获取锁
将设置 Key 和过期时间的操作封装在 Lua 脚本中。脚本成功返回 OK 表示获取成功。
var lockCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
redis.call("SET", KEYS[1], ARGV[1], "PX", ARGV[2] , "NX")
return "OK"
else
return redis.call("SET", KEYS[1], ARGV[1], "NX", "PX", ARGV[2])
end`
在 Go 中调用:
lua := redis.NewScript(1, lockCommand)
resp, err := lua.Do(conn, LockName, myLockValue, expireTimeMs)
第3步:原子释放锁
这是最关键的一步。释放前必须先 GET 当前值,判断是否等于自己的 Value,相等才执行 DEL。这行得通的原因是 Lua 脚本在执行期间不会被其他命令插入,保证了“检查-删除”的原子性。
var delCommand = `if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEKS[1])
else
return 0
end`
func delLock(LockName, LockeValue string, conn redis.Conn) (bool, error) {
lua := redis.NewScript(1, delCommand)
resp, err := lua.Do(conn, LockName, LockeValue)
if err != nil {
return false, err
}
reply, ok := resp.(int64)
if !ok { return false, nil }
return reply == 1, nil
}
完整测试案例与输出
通过启动两个协程模拟分布式服务 A 和 B,B 在等待时会持续打印未抢到锁的信息,直到 A 释放。由于使用了 Lua 校验,即使 A 的处理时间接近过期时间,也不会误删 B 的锁。
在上述流程中,我们不仅实现了基本的互斥,还引入了防重入和安全性考量。需要注意的是,这种基于 Redis 的实现并非强一致性锁(不同于 ZK),如果 Redis 主从切换期间发生主库宕机而未同步锁信息,可能会引发短暂的重入。对于要求绝对安全的场景,应选用 Redisson 等成熟的第三方库(内置看门狗机制)。但对于大多数高并发读写场景,掌握 Lua 脚本实现原理足以应对。
以上就是基于 Redis 和 Lua 实现 Golang 分布式锁的详细内容,更多关于 Go 语言分布式并发控制与 Redigo 优化的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













