在分布式系统中,多个服务实例同时访问共享资源是常见场景。比如秒杀活动中扣减库存、定时任务的执行、订单状态的更新等,都需要一种机制来确保同一时刻只有一个实例能操作。这就是分布式锁要解决的问题。

结合我的实际项目经验,这篇文章来聊聊Go语言中实现分布式锁的几种主流方案,选对工具,少走弯路。


为什么需要分布式锁?

单机环境下,我们可以用sync.Mutex轻松实现互斥锁。但在分布式系统中,多个服务实例运行在不同的机器上,内存不共享,本地锁就失效了。

举个例子:用户A在节点1上修改订单状态,用户B同时在节点2上也想修改同一订单。如果没有分布式锁,两个节点同时操作,数据就乱套了。

分布式锁的核心要求:

  • 互斥性:同一时刻只有一个客户端能持有锁
  • 防死锁:锁必须有过期机制,避免客户端崩溃后锁无法释放
  • 高可用:锁服务本身要稳定可靠

方案一:Redis分布式锁

Redis是最常用的分布式锁实现方案,核心原理是利用SETNX命令(Set if Not eXists)的原子性。

工作原理

  1. 客户端尝试设置一个key,如果key不存在则设置成功,获得锁
  2. 设置过期时间,防止客户端崩溃后锁无法释放
  3. 释放锁时,需要验证锁的持有者,避免误删其他客户端的锁

优点:性能高,实现简单,Redis部署维护方便

缺点:在Redis主从切换时可能出现锁丢失问题;需要手动处理锁续期

适用场景:高并发、对可靠性要求不是极端苛刻的场景

推荐开源库github.com/go-redsync/redsync/v4

client := redis.NewClient(&redis.Options{Addr: "localhost:6379"})
rs := redsync.New(goredis.NewPool(client))
mutex := rs.NewMutex("my-lock", redsync.WithExpiry(10*time.Second))

if err := mutex.Lock(); err != nil { return }
defer mutex.Unlock()

方案二:ZooKeeper分布式锁

ZooKeeper是Apache旗下的分布式协调服务,天生适合做分布式锁。

工作原理

  1. 创建一个持久节点作为锁的根目录
  2. 每个客户端在根目录下创建临时有序节点
  3. 节点序号最小的客户端获得锁
  4. 其他客户端监听比自己序号小的前一个节点
  5. 前一个节点删除(锁释放)时,当前客户端被唤醒

优点:可靠性极高,支持公平锁(按请求顺序获取),会话断开自动释放锁

缺点:性能相对较低,ZooKeeper集群部署和维护较复杂

适用场景:对可靠性要求高、并发度适中的场景,如配置中心、选主场景

推荐开源库github.com/go-zookeeper/zk

conn, _, _ := zk.Connect([]string{"127.0.0.1:2181"}, time.Second*5)
lock := zk.NewLock(conn, "/my-lock", zk.WorldACL(zk.PermAll))
if err := lock.Lock(); err != nil { return }
defer lock.Unlock()

方案三:etcd分布式锁

etcd是CoreOS开发的分布式键值存储,Kubernetes就是基于etcd构建的。

工作原理: etcd提供了concurrency包,开箱即用的分布式锁实现:

  1. 创建一个租约(Lease),设置TTL
  2. 创建锁对应的key,绑定租约
  3. 如果key不存在则创建成功,获得锁
  4. 租约到期自动释放锁,也支持手动续期

优点:性能好、可靠性高、支持租约机制、部署相对简单

缺点:相比Redis实现稍重,需要依赖etcd集群

适用场景:云原生环境,特别是已经使用etcd的项目(如Kubernetes生态)

推荐开源库go.etcd.io/etcd/client/v3/concurrency

cli, _ := clientv3.New(clientv3.Config{Endpoints: []string{"localhost:2379"}})
session, _ := concurrency.NewSession(cli, concurrency.WithTTL(10))
mutex := concurrency.NewMutex(session, "/my-lock/")

mutex.Lock(context.Background())
defer mutex.Unlock(context.Background())
// 执行业务逻辑

方案四:数据库分布式锁

如果不想引入额外组件,用现有数据库也能实现分布式锁。

工作原理

  1. 创建一张锁表,设置唯一索引
  2. 获取锁时插入一条记录,插入成功则获得锁
  3. 释放锁时删除该记录
  4. 可以增加过期时间字段,定期清理过期锁

优点:实现简单,无需额外组件,适合已有数据库的场景

缺点:性能较低,数据库压力大,需要处理死锁和过期清理

适用场景:并发度低、对性能要求不高的场景

推荐开源库:无(实现简单,通常自行实现)

// 获取锁:插入记录,唯一索引冲突则失败
_, err := db.Exec("INSERT INTO locks(lock_key, owner) VALUES(?, ?)", key, owner)

// 释放锁:删除记录
db.Exec("DELETE FROM locks WHERE lock_key = ? AND owner = ?", key, owner)

如何选择?

方案 性能 可靠性 部署难度 推荐场景
Redis ⭐⭐⭐⭐⭐ ⭐⭐⭐ 高并发、秒杀、抢红包
ZooKeeper ⭐⭐⭐ ⭐⭐⭐⭐⭐ 选主、配置中心、元数据管理
etcd ⭐⭐⭐⭐ ⭐⭐⭐⭐ 云原生、K8s生态
数据库 ⭐⭐ ⭐⭐⭐ 低并发、快速验证

实践建议

1. 设置合理的过期时间

过期时间太短,任务没执行完锁就释放了;太长,客户端崩溃后其他客户端要等很久。建议根据业务最长执行时间设置,并实现锁续期机制。

2. 释放锁要验证持有者

释放锁时,务必确认锁是自己持有的,否则可能误删其他客户端的锁。Redis可以用Lua脚本保证原子性。

3. 做好降级和容错

分布式锁服务可能不可用,业务代码要有降级策略,比如本地锁兜底、返回错误让用户重试等。


写在最后

分布式锁没有银弹,选型时需要权衡性能、可靠性、部署成本。对于大多数互联网应用,Redis分布式锁是首选;对可靠性要求极高的场景,ZooKeeper或etcd更合适;快速验证或低并发场景,数据库锁也能胜任。

记住:技术选型的本质是权衡,没有最好的方案,只有最适合的方案。