Golang 基于 Redis 的分布式锁实现与安全释放

听雨眠
听雨眠
发布时间:2026-10-15| 6人浏览过
原创

在 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)

此方法在实际生产中存在两个致命问题:

  1. 锁误释放(AOP问题):如果业务执行时间超过了锁的过期时间,锁自动释放后,其他协程可能会抢到此锁。当原协程业务结束去释放锁时,会错误地删除当前持有者的锁。
  2. 非原子操作:先判断 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 优化的资料请关注本站其它相关文章!

相关标签:

免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。

Python 相关产品和应用

全部下载
Selenium

Selenium

4.51.0 · Windows

用于浏览器自动化和 Web 测试的 Python 工具。

scikit-learn

scikit-learn

1.9.1 · Windows

常用 Python 机器学习算法与模型评估工具库。

Black

Black

26.10.1 · Windows

强调一致性的 Python 自动代码格式化工具。

Ruff

Ruff

0.17.1 · macOS

由 Rust 编写的高速 Python 代码检查和格式化工具。

pytest

pytest

9.1.1 · Windows

简洁而强大的 Python 自动化测试框架。

FastAPI

FastAPI

0.146.0 · Windows

高性能 Python API 框架,支持类型提示和自动接口文档。

Flask

Flask

3.1.3 · Windows

轻量灵活的 Python Web 应用框架。

Django

Django

6.1.2 · Windows

成熟的 Python Web 框架,适合构建内容站点和业务系统。

Spyder

Spyder

6.1.7 · Windows

面向科学计算与数据分析的 Python 集成开发环境。

SciPy

SciPy

1.18.1 · Windows

面向科学、工程和技术计算的 Python 算法库。