Go分布式锁实现方案从Redis到etcd的完整对比
文章导语
在分布式系统中,分布式锁是保证数据一致性的核心组件。Go生态中,基于Redis、etcd、ZooKeeper的分布式锁实现各有所长。本文将深入对比三种主流方案,给出选型建议和生产级实现代码。
一、分布式锁的核心要求
一个合格的分布式锁必须满足:
- 互斥性:同一时刻只有一个客户端持有锁
- 防死锁:锁超时自动释放,避免客户端崩溃导致死锁
- 解铃还须系铃人:只有加锁的客户端才能释放锁
- 容错性:部分节点故障不影响锁服务
二、基于Redis的分布式锁
2.1 基础实现(有缺陷)
go
// 简陋版------有多个严重问题
func BadLock(rdb *redis.Client, key string, value string, ttl time.Duration) bool {
return rdb.SetNX(ctx, key, value, ttl).Val()
}
func BadUnlock(rdb *redis.Client, key string) {
rdb.Del(ctx, key) // 可能释放别人的锁!
}
2.2 正确的Redlock实现
go
type RedLock struct {
clients []*redis.Client
}
func (r *RedLock) Lock(ctx context.Context, key string, ttl time.Duration) (string, error) {
value := uuid.New().String()
deadline := time.Now().Add(ttl)
quorum := len(r.clients)/2 + 1
acquired := 0
for _, client := range r.clients {
start := time.Now()
ok, err := client.SetNX(ctx, key, value, ttl).Result()
if err != nil || !ok {
continue
}
elapsed := time.Since(start)
if elapsed > ttl/2 {
client.Del(ctx, key)
continue
}
acquired++
}
if acquired < quorum {
r.Unlock(ctx, key, value)
return "", ErrLockFailed
}
remaining := time.Until(deadline)
if remaining < 0 {
r.Unlock(ctx, key, value)
return "", ErrLockTimeout
}
return value, nil
}
// 安全释放------使用Lua脚本保证原子性
const unlockScript = `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
func (r *RedLock) Unlock(ctx context.Context, key string, value string) error {
for _, client := range r.clients {
client.Eval(ctx, unlockScript, []string{key}, value)
}
return nil
}
三、基于etcd的分布式锁
go
func EtcdLock(ctx context.Context, client *clientv3.Client, key string, ttl int64) (*clientv3.LeaseGrantResponse, error) {
// etcd通过lease(租约)实现自动过期
lease, err := client.Grant(ctx, ttl)
if err != nil {
return nil, err
}
// 使用事务实现原子性
txn := client.Txn(ctx)
txn.If(clientv3.Compare(clientv3.CreateRevision(key), "=", 0)).
Then(clientv3.OpPut(key, "locked", clientv3.WithLease(lease.ID))).
Else(clientv3.OpGet(key))
resp, err := txn.Commit()
if err != nil {
client.Revoke(ctx, lease.ID)
return nil, err
}
if !resp.Succeeded {
client.Revoke(ctx, lease.ID)
return nil, ErrLockHeld
}
// 自动续租
keepAliveCh, err := client.KeepAlive(ctx, lease.ID)
if err != nil {
return nil, err
}
go func() {
for range keepAliveCh {
// 续租成功
}
}()
return lease, nil
}
func EtcdUnlock(ctx context.Context, client *clientv3.Client, lease *clientv3.LeaseGrantResponse) error {
_, err := client.Revoke(ctx, lease.ID)
return err
}
四、Redis vs etcd 对比
| 维度 | Redis | etcd |
|---|---|---|
| 性能 | 极高(内存操作) | 中等(Raft共识) |
| 一致性 | 最终一致性(单机强一致) | 强一致性(Raft) |
| 可用性 | 主从/哨兵/集群 | 内置Raft集群 |
| 自动续期 | 需要自行实现 | 内置Lease + KeepAlive |
| Watch机制 | pub/sub(不可靠) | Watch(可靠) |
| 运维成本 | 低(广泛使用) | 中(Raft集群) |
五、选型建议
go
// 1. 性能敏感且可容忍偶发冲突 → Redis
// 2. 一致性要求极高 → etcd
// 3. 已经使用Redis → Redis(减少基础设施)
// 4. K8S环境 → etcd(K8S自带)
// 生产推荐:golang-redis自带的Redlock
import "github.com/redis/go-redis/v9"
// 使用redsync实现
import goredis "github.com/go-redsync/redsync/v4"
import goredislib "github.com/go-redsync/redsync/v4/redis/goredis/v9"
六、全文总结
- Redis分布式锁性能高但需要Redlock算法保证可靠性
- etcd分布式锁基于Raft强一致性,适合高一致性要求场景
- 锁必须设置超时防止死锁
- 释放锁必须验证持有者(Lua脚本或etcd的Transaction)
- 根据业务场景选择合适方案
七、技术进阶展望
- Chubby和Paxos在分布式锁中的应用
- 分布式锁的公平性实现
- K8S中ConfigMap/Endpoint实现分布式锁
参考文献
- Redis分布式锁官方文档: https://redis.io/docs/manual/patterns/distributed-locks/
- Martin Kleppmann - How to do distributed locking
- etcd官方文档 - Concurrency API
- redsync: https://github.com/go-redsync/redsync
- Apache Curator实现文档