Go 分布式锁实战:Redis Redlock 和 etcd 的实现差异与坑

📅 发布时间:2026/7/17 17:42:57
Go 分布式锁实战:Redis Redlock 和 etcd 的实现差异与坑 Go 分布式锁实战Redis Redlock 和 etcd 的实现差异与坑一、一个库存超卖事故引发的思考电商大促期间库存扣减的并发请求同时打到两台服务节点。业务方使用的是单节点 Redis 实现的分布式锁主从切换的那一秒锁失效了。结果1000 件库存卖了 1003 件客服电话打爆。分布式锁看起来简单上锁、干活、解锁。但在真实的生产环境中网络抖动、进程 GC、时钟偏移每个环节都可能让锁变成摆设。在选择方案之前先明确分布式锁需要满足的条件互斥性同一时刻只有一个客户端持有锁防死锁持有锁的客户端崩溃后锁能自动释放容错性锁服务部分节点故障时系统仍能工作二、Redlock 和 etcd 的架构原理两种方案的本质差异Redlock是多 Redis 实例上的主动加锁过程。客户端依次向 N 个独立的 Redis 节点申请锁如果在大多数节点N/21上获取成功且总耗时小于锁的有效期才算加锁成功。etcd基于 Raft 共识协议。写操作由 Leader 发起、多数派确认后才返回成功。本质是借助共识算法的线性一致性来保证锁的互斥性。三、两种方案的 Go 实现Redlock 实现package lock import ( context crypto/rand encoding/hex fmt time github.com/go-redsync/redsync/v4 github.com/go-redsync/redsync/v4/redis/goredis/v9 goredislib github.com/redis/go-redis/v9 ) // RedlockManager Redlock 分布式锁管理器 type RedlockManager struct { rs *redsync.Redsync } // NewRedlockManager 创建 Redlock 管理器传入多个 Redis 实例 func NewRedlockManager(addrs []string) *RedlockManager { var pools []redsync.Pool for _, addr : range addrs { client : goredislib.NewClient(goredislib.Options{ Addr: addr, DialTimeout: 200 * time.Millisecond, // 连接超时不能太长 ReadTimeout: 100 * time.Millisecond, WriteTimeout: 100 * time.Millisecond, }) pools append(pools, goredis.NewPool(client)) } return RedlockManager{rs: redsync.New(pools...)} } // Lock 获取分布式锁返回锁句柄和错误 // name: 锁名称 // ttl: 锁失效时间建议 8-30 秒 func (rm *RedlockManager) Lock(ctx context.Context, name string, ttl time.Duration) (*LockHandle, error) { // 生成随机值作为锁持有者标识 value : make([]byte, 16) if _, err : rand.Read(value); err ! nil { return nil, fmt.Errorf(生成随机值失败: %w, err) } mutex : rm.rs.NewMutex( name, redsync.WithExpiry(ttl), redsync.WithTries(3), // 重试 3 次 redsync.WithRetryDelay(50*time.Millisecond), redsync.WithValue(hex.EncodeToString(value)), ) if err : mutex.LockContext(ctx); err ! nil { return nil, fmt.Errorf(获取锁失败: %w, err) } return LockHandle{mutex: mutex, name: name, ttl: ttl}, nil } // LockHandle 锁句柄 type LockHandle struct { mutex *redsync.Mutex name string ttl time.Duration } // Unlock 释放锁 func (lh *LockHandle) Unlock(ctx context.Context) error { if _, err : lh.mutex.UnlockContext(ctx); err ! nil { return fmt.Errorf(释放锁失败: %w, err) } return nil } // Extend 续约锁的有效期长任务需要 func (lh *LockHandle) Extend(ctx context.Context) error { if _, err : lh.mutex.ExtendContext(ctx); err ! nil { return fmt.Errorf(续约锁失败: %w, err) } return nil }etcd 实现package lock import ( context fmt time clientv3 go.etcd.io/etcd/client/v3 go.etcd.io/etcd/client/v3/concurrency ) // EtcdLockManager etcd 分布式锁管理器 type EtcdLockManager struct { client *clientv3.Client } // NewEtcdLockManager 创建 etcd 锁管理器 func NewEtcdLockManager(endpoints []string) (*EtcdLockManager, error) { client, err : clientv3.New(clientv3.Config{ Endpoints: endpoints, DialTimeout: 5 * time.Second, }) if err ! nil { return nil, fmt.Errorf(连接 etcd 失败: %w, err) } return EtcdLockManager{client: client}, nil } // Lock 获取 etcd 分布式锁通过 Session 机制自动续约 func (elm *EtcdLockManager) Lock(ctx context.Context, name string, ttl int) (*EtcdLockHandle, error) { // 创建 Sessionttl 内未续约则自动释放 session, err : concurrency.NewSession(elm.client, concurrency.WithTTL(ttl)) if err ! nil { return nil, fmt.Errorf(创建 Session 失败: %w, err) } // 在 Session 上创建 Mutex mutex : concurrency.NewMutex(session, fmt.Sprintf(/locks/%s, name)) if err : mutex.Lock(ctx); err ! nil { session.Close() // 加锁失败时清理 Session return nil, fmt.Errorf(获取锁失败: %w, err) } return EtcdLockHandle{ mutex: mutex, session: session, }, nil } // EtcdLockHandle etcd 锁句柄 type EtcdLockHandle struct { mutex *concurrency.Mutex session *concurrency.Session } // Unlock 释放锁 func (elh *EtcdLockHandle) Unlock(ctx context.Context) error { if err : elh.mutex.Unlock(ctx); err ! nil { return fmt.Errorf(释放锁失败: %w, err) } return elh.session.Close() }四、边界分析与选型 Trade-offsRedlock 的已知问题时钟偏移风险如果某个 Redis 节点的时钟出现跳跃锁的过期时间可能缩短GC 暂停Go 程序的 GC STW 时间如果超过锁 TTL会导致锁误释放主从切换哨兵模式下主节点宕机已获取的锁数据可能丢失规避策略TTL 设置为预期执行时间的 2~3 倍单次操作超过锁有效期时主动续约业务层做乐观锁兜底数据库 version 字段etcd 的适用边界etcd 的可靠性和一致性优于 Redlock但吞吐量远低于 Redisetcd 适合锁竞争低、一致性要求高的场景如配置变更、Leader 选举不适合高并发的业务锁如库存扣减选型速查表场景推荐方案理由库存扣减等高并发锁Redlock高吞吐分布式任务调度etcd强一致 自动续约配置变更互斥etcd可 Watch 变更事件短时业务锁1s单 Redis够用避免过设计五、总结Redlock 和 etcd 解决的是不同维度的问题Redlock追求性能和吞吐代价是边界条件下的安全性etcd追求一致性和可靠性代价是写入性能实际选型时不要迷信高可用方案。一个库存扣减场景用 Redlock 就够了加上数据库乐观锁做兜底。只有涉及数据正确性不可逆的场景资金转账、配置变更才需要 etcd 的强一致保证。补充一个生产教训Go 里使用 Redlock 时确保GOMEMLIMIT设置合理。如果节点 OOM 导致 GC 频繁STW 时间超过锁 TTL 的概率会大幅增加。