分布式锁的核心要求与Redis单节点实现
分布式锁需满足互斥性(同一时刻仅一个客户端持有)、可重入性、防死锁(锁自动过期)和容错性(锁服务部分节点宕机仍可用)四个核心要求。Redis基于SET命令的NX+PX参数可实现单节点分布式锁:
package distributed
import (
"context"
"errors"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)
type RedisLock struct {
client *redis.Client
key string
value string
ttl time.Duration
}
func NewRedisLock(client *redis.Client, key string, ttl time.Duration) *RedisLock {
return &RedisLock{
client: client,
key: key,
value: uuid.New().String(),
ttl: ttl,
}
}
// Lock 尝试获取锁
func (l *RedisLock) Lock(ctx context.Context) error {
ok, err := l.client.SetNX(ctx, l.key, l.value, l.ttl).Result()
if err != nil {
return err
}
if !ok {
return errors.New("lock acquired by another client")
}
return nil
}
// Unlock 释放锁(使用Lua脚本保证原子性)
func (l *RedisLock) Unlock(ctx context.Context) error {
// 必须验证value一致,防止误删其他客户端的锁
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
result, err := l.client.Eval(ctx, script, []string{l.key}, l.value).Int()
if err != nil {
return err
}
if result == 0 {
return errors.New("lock not held by this client")
}
return nil
}
// TryLockWithRetry 带重试的锁获取
func (l *RedisLock) TryLockWithRetry(ctx context.Context, retries int, interval time.Duration) error {
for i := 0; i < retries; i++ {
err := l.Lock(ctx)
if err == nil {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(interval):
}
}
return errors.New("failed to acquire lock after retries")
}
// StartAutoRenew 自动续期(watchdog机制)
func (l *RedisLock) StartAutoRenew(ctx context.Context, interval time.Duration) context.CancelFunc {
renewCtx, cancel := context.WithCancel(ctx)
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("PEXPIRE", KEYS[1], ARGV[2])
else
return 0
end
`
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-renewCtx.Done():
return
case <-ticker.C:
l.client.Eval(renewCtx, script, []string{l.key}, l.value, l.ttl.Milliseconds())
}
}
}()
return cancel
}
Unlock操作必须通过Lua脚本实现GET+DEL的原子性执行,避免客户端A的锁过期后客户端B获取锁,客户端A再执行DEL误删B的锁。value使用UUID保证唯一性。
watchdog自动续期机制适用于执行时间不可预估的场景。续期间隔通常设置为TTL的1/3,如TTL=30s则每10s续期一次。但续期存在网络延迟风险,高安全场景需评估续期失败后的处理策略。
Redlock算法:多节点容错锁
Redis作者Antirez提出的Redlock算法通过在多个独立Redis节点上获取锁实现容错。假设N个Redis节点(推荐5个),客户端在多数节点(N/2+1)上成功获取锁即认为锁获取成功:
package distributed
import (
"context"
"errors"
"time"
"github.com/go-redis/redis/v8"
"github.com/google/uuid"
)
type RedLock struct {
clients []*redis.Client
key string
value string
ttl time.Duration
}
func NewRedLock(clients []*redis.Client, key string, ttl time.Duration) *RedLock {
return &RedLock{
clients: clients,
key: key,
value: uuid.New().String(),
ttl: ttl,
}
}
func (rl *RedLock) Lock(ctx context.Context) error {
n := len(rl.clients)
majority := n/2 + 1
successCount := 0
startTime := time.Now()
// 在所有节点上尝试获取锁
for _, client := range rl.clients {
ok, err := client.SetNX(ctx, rl.key, rl.value, rl.ttl).Result()
if err == nil && ok {
successCount++
}
}
// 检查是否获取多数节点锁,且耗时未超过TTL的一半
elapsed := time.Since(startTime)
if successCount >= majority && elapsed < rl.ttl/2 {
// 锁有效时间 = TTL - 获取耗时
return nil
}
// 未获取多数锁,释放已获取的锁
rl.unlockPartial(ctx)
if successCount < majority {
return errors.New("failed to acquire majority locks")
}
return errors.New("lock acquisition too slow")
}
func (rl *RedLock) Unlock(ctx context.Context) error {
return rl.unlockPartial(ctx)
}
func (rl *RedLock) unlockPartial(ctx context.Context) error {
script := `
if redis.call("GET", KEYS[1]) == ARGV[1] then
return redis.call("DEL", KEYS[1])
else
return 0
end
`
var lastErr error
for _, client := range rl.clients {
_, err := client.Eval(ctx, script, []string{rl.key}, rl.value).Result()
if err != nil {
lastErr = err
}
}
return lastErr
}
Redlock的关键设计点:获取锁的耗时必须小于TTL的一半,否则锁的有效期不足以完成业务操作。锁的有效时间计算为effectiveTTL = TTL - acquisitionTime,业务操作必须在此时间内完成。
Redlock的争议主要在于时钟同步问题。如果某节点发生时钟跳跃(NTP校正),锁可能提前过期。对此,Martin Kleppmann建议使用fencing token方案:每次获取锁生成递增token,资源服务端拒绝低token的请求。
etcd分布式锁:Lease机制实现
etcd基于Raft共识算法提供强一致性存储,分布式锁实现依赖Lease(租约)机制。Lease绑定到key,租约到期后key自动删除,天然实现防死锁:
package distributed
import (
"context"
"go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
)
type EtcdLock struct {
client *clientv3.Client
key string
ttl int
session *concurrency.Session
mutex *concurrency.Mutex
}
func NewEtcdLock(client *clientv3.Client, key string, ttl int) (*EtcdLock, error) {
// 创建session,TTL到期后自动释放锁
session, err := concurrency.NewSession(client, concurrency.WithTTL(ttl))
if err != nil {
return nil, err
}
mutex := concurrency.NewMutex(session, key)
return &EtcdLock{
client: client,
key: key,
ttl: ttl,
session: session,
mutex: mutex,
}, nil
}
// Lock 阻塞式获取锁
func (el *EtcdLock) Lock(ctx context.Context) error {
return el.mutex.Lock(ctx)
}
// TryLock 非阻塞式尝试获取锁
func (el *EtcdLock) TryLock(ctx context.Context) error {
return el.mutex.TryLock(ctx)
}
// Unlock 释放锁
func (el *EtcdLock) Unlock(ctx context.Context) error {
return el.mutex.Unlock(ctx)
}
// Close 关闭session,释放锁并清理资源
func (el *EtcdLock) Close() error {
return el.session.Close()
}
etcd的concurrency包封装了完整的锁实现逻辑。与Redis不同,etcd锁的获取是排队式的——当锁被持有时,其他客户端通过Watch机制监听前一个持有者的key删除事件,形成公平锁队列。
实际业务场景的封装使用示例:
package service
import (
"context"
"fmt"
"log"
"time"
"go.etcd.io/etcd/client/v3"
"distributed"
)
type OrderService struct {
etcdClient *clientv3.Client
}
func (s *OrderService) ProcessOrderWithLock(ctx context.Context, orderID string) error {
lockKey := fmt.Sprintf("/locks/order/%s", orderID)
lock, err := distributed.NewEtcdLock(s.etcdClient, lockKey, 30)
if err != nil {
return fmt.Errorf("create lock: %w", err)
}
defer lock.Close()
// 尝试获取锁,最多等待10秒
lockCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := lock.Lock(lockCtx); err != nil {
return fmt.Errorf("acquire lock for order %s: %w", orderID, err)
}
// 执行业务逻辑
if err := s.processOrder(ctx, orderID); err != nil {
// 即使业务失败也必须释放锁
_ = lock.Unlock(context.Background())
return fmt.Errorf("process order: %w", err)
}
// 释放锁
if err := lock.Unlock(ctx); err != nil {
log.Printf("unlock failed for order %s: %v", orderID, err)
return err
}
return nil
}
func (s *OrderService) processOrder(ctx context.Context, orderID string) error {
// 业务处理逻辑
return nil
}
Redis与etcd分布式锁方案对比
选择分布式锁实现方案时,需在性能、一致性、运维复杂度之间权衡:
// 方案选型对比表
+------------------+--------------------------+--------------------------+
| 维度 | Redis分布式锁 | etcd分布式锁 |
+------------------+--------------------------+--------------------------+
| 一致性 | AP(最终一致性) | CP(强一致性,Raft) |
| 性能 | 单节点TPS 10万+ | 集群TPS 1-2万 |
| 锁公平性 | 非公平(抢占式) | 公平(FIFO队列) |
| 防死锁 | TTL过期 | Lease过期 |
| 时钟依赖 | 强(时钟跳跃影响TTL) | 弱(基于Leader租约) |
| 可重入性 | 需自行实现 | concurrency包内置 |
| 运维复杂度 | 低(Redis广泛部署) | 中(需维护etcd集群) |
| 适用场景 | 高并发、短任务、 | 长任务、强一致性要求、 |
| | 最终一致性可接受 | 配置/选主/任务调度 |
+------------------+--------------------------+--------------------------+
Redis锁在高并发短任务场景下性能优势明显,如秒杀库存扣减、限流计数。但存在时钟跳跃导致锁失效的理论风险,不适合金融级强一致性场景。etcd锁基于Raft共识,在网络分区时优先保证一致性而非可用性,适合配置管理、分布式任务调度、选主等场景。
无论选择哪种方案,业务代码都应遵循锁持有时间最小化原则:获取锁后只执行必要的临界区操作,避免在锁内进行网络调用或长时间计算。锁TTL应设置为业务最大执行时间的2-3倍,作为安全边界。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/go-yu-yan-fen-bu-shi-suo-shi-xian-shi-zhan-redisredlock/