Go 微服务分布式锁:从 Redis 到 etcd 的一致性保障实践
Go 微服务分布式锁:从 Redis 到 etcd 的一致性保障实践
一、分布式环境下的并发失控:多个实例同时操作共享资源
微服务架构中,同一个服务通常部署多个实例,当多个实例同时访问共享资源(如数据库记录、文件、外部 API 配额)时,缺乏协调机制会导致数据不一致。典型场景包括:库存扣减超卖、定时任务重复执行、配置并发修改覆盖等。某电商系统在大促期间,3 个订单服务实例同时读取到库存为 1,各自扣减后库存变为 -2,直接造成资损。
本地互斥锁(sync.Mutex)只能保护单进程内的并发,跨进程的并发协调必须依赖分布式锁。但分布式锁的实现远比本地锁复杂——网络分区、进程崩溃、时钟不同步都可能破坏锁的正确性。
二、分布式锁的演进:从 Redis 到 etcd
分布式锁的实现经历了从简单到可靠的演进过程。Redis 单节点锁最简单但不可靠,Redlock 算法提高了可靠性但仍有争议,etcd 基于 Raft 共识的锁提供了最强一致性保证。
flowchart LR A[分布式锁需求] --> B{可靠性要求} B -->|低| C[Redis 单节点锁] B -->|中| D[Redis Redlock] B -->|高| E[etcd 分布式锁] C --> F[优点:性能高<br/>缺点:主从切换丢锁] D --> G[优点:多节点投票<br/>缺点:时钟依赖争议] E --> H[优点:Raft 共识强一致<br/>缺点:延迟较高] style C fill:#fff3e0 style D fill:#e8eaf6 style E fill:#e8f5e9三、生产级分布式锁的实现
// redis_lock.go // Redis 分布式锁:带自动续期的可重入锁 import ( "context" "fmt" "time" "github.com/google/uuid" "github.com/redis/go-redis/v9" ) type RedisLock struct { client *redis.Client key string value string // 唯一标识,防止误删他人的锁 ttl time.Duration // 锁的初始 TTL ctx context.Context cancel context.CancelFunc } func NewRedisLock(client *redis.Client, key string, ttl time.Duration) *RedisLock { return &RedisLock{ client: client, key: key, value: uuid.New().String(), // 每个锁实例有唯一标识 ttl: ttl, } } // TryLock 尝试获取锁,非阻塞 func (l *RedisLock) TryLock(ctx context.Context) (bool, error) { // SET key value NX EX ttl — 原子操作,仅当 key 不存在时设置 ok, err := l.client.SetNX(ctx, l.key, l.value, l.ttl).Result() if err != nil { return false, fmt.Errorf("获取锁失败: %w", err) } if ok { // 获取成功,启动后台续期协程 l.ctx, l.cancel = context.WithCancel(context.Background()) go l.keepAlive() } return ok, nil } // keepAlive 后台续期:在持有锁期间持续延长 TTL func (l *RedisLock) keepAlive() { ticker := time.NewTicker(l.ttl / 3) // 每隔 TTL/3 续期一次 defer ticker.Stop() for { select { case <-ticker.C: // 使用 Lua 脚本保证"只有锁的持有者才能续期" script := redis.NewScript(` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("expire", KEYS[1], ARGV[2]) else return 0 end `) script.Run(l.ctx, l.client, []string{l.key}, l.value, int(l.ttl.Seconds())) case <-l.ctx.Done(): return } } } // Unlock 释放锁:使用 Lua 脚本保证"只有锁的持有者才能释放" func (l *RedisLock) Unlock(ctx context.Context) error { if l.cancel != nil { l.cancel() // 停止续期协程 } script := redis.NewScript(` if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end `) _, err := script.Run(ctx, l.client, []string{l.key}, l.value).Result() return err }// etcd_lock.go // etcd 分布式锁:基于 Raft 共识的强一致性锁 import ( "context" "time" clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/concurrency" ) type EtcdLock struct { client *clientv3.Client prefix string // 锁的 key 前缀 timeout time.Duration // 获取锁的超时时间 } func NewEtcdLock(client *clientv3.Client, prefix string, timeout time.Duration) *EtcdLock { return &EtcdLock{ client: client, prefix: prefix, timeout: timeout, } } // Lock 获取分布式锁(阻塞,带超时) func (l *EtcdLock) Lock(ctx context.Context) (*concurrency.Mutex, error) { // 创建带超时的上下文 lockCtx, cancel := context.WithTimeout(ctx, l.timeout) defer cancel() // 创建 etcd 会话(带 TTL,会话断开自动释放锁) session, err := concurrency.NewSession( l.client, concurrency.WithTTL(int(l.timeout.Seconds())), ) if err != nil { return nil, fmt.Errorf("创建 etcd 会话失败: %w", err) } // 创建互斥锁实例 mutex := concurrency.NewMutex(session, l.prefix) // 尝试获取锁 if err := mutex.Lock(lockCtx); err != nil { session.Close() return nil, fmt.Errorf("获取 etcd 锁失败: %w", err) } return mutex, nil }四、Redis 锁与 etcd 锁的对比与选型
一致性级别差异。Redis 单节点锁在主从切换时可能丢失锁信息(主节点宕机前未同步到从节点),导致两个客户端同时持有同一把锁。Redlock 通过多节点投票缓解了这个问题,但依赖各节点时钟同步,在时钟跳变场景下仍可能出现异常。etcd 基于 Raft 协议,所有写操作必须经过多数节点确认,天然避免了单点故障导致的锁丢失。
性能对比。在基准测试中,Redis 单节点锁的获取延迟约 0.5ms,etcd 锁约 5ms(跨 3 节点 Raft 共识)。在高并发场景(1000 QPS)下,Redis 锁的 P99 延迟约 3ms,etcd 锁约 25ms。如果业务对延迟极度敏感,Redis 是更好的选择。
运维复杂度。Redis 集群的运维相对成熟,但 Redlock 需要部署多个独立 Redis 实例(非主从),运维成本较高。etcd 集群本身就需要 3-5 个节点,运维复杂度与 Redlock 相当,但 etcd 提供了更丰富的分布式原语(如 Watch、Lease),适合需要多种协调机制的场景。
选型建议:库存扣减、资金操作等对一致性要求极高的场景,优先选择 etcd 锁;限流、去重等对性能要求高但对短暂不一致可容忍的场景,选择 Redis 锁。
五、总结
分布式锁是微服务架构中共享资源协调的基础设施。核心要点:Redis 锁必须使用 Lua 脚本保证"只有持有者才能释放",并配合后台续期防止业务未完成锁就过期;etcd 锁基于 Raft 共识提供强一致性保证,但延迟是 Redis 的 5-10 倍;选型应根据业务对一致性和性能的需求权衡。落地建议:先明确业务场景的一致性要求,再选择锁方案;无论选择哪种方案,都必须实现锁的超时释放和异常兜底机制,防止死锁。
