跳到主要内容

Redis 中的红锁

基础概念题

1. 什么是分布式锁?Redis 分布式锁存在什么问题?

考察点: 分布式系统基础概念、Redis 主从复制机制理解

参考答案: 分布式锁是在分布式环境中控制多个进程访问共享资源的机制。

Redis 分布式锁的主要问题:

  • 主从复制延迟问题:当客户端在 master 上获取锁成功后,如果 master 在同步给 slave 之前宕机,新的 master(原 slave)上没有锁信息,导致锁丢失
  • 单点故障:依赖单个 Redis 实例
  • 时钟依赖:依赖系统时钟判断锁超时

2. Redlock 算法是为了解决什么问题的?

考察点: 对分布式系统 CAP 理论的理解,Redis 集群架构

参考答案: Redlock 主要解决 Redis 主从架构下的锁安全性问题。在主从复制场景中,主节点获取锁后宕机,从节点提升为主节点时可能丢失锁信息,导致多个客户端同时持有锁。Redlock 通过在多个独立的 Redis 实例上获取锁,只有在大多数实例上都成功获取锁时才认为加锁成功。

算法流程题

3. 请详细描述 Redlock 算法的完整流程,并画出时序图

考察点: 分布式算法理解、并发控制、异常处理

这里的 PX 是 Redis 的一个选项,用于设置键的过期时间(TTL),单位为毫秒。NX 则是另一个选项,表示只有在键不存在时才进行设置操作。

算法详细步骤:

  1. 记录开始时间:获取当前时间戳,用于计算总耗时
  2. 并发获取锁:在所有 Redis 实例上并发执行 SET key value PX ttl NX
  3. 计算耗时:current_time - start_time
  4. 验证成功条件
    • 成功获取锁的节点数 >= N/2 + 1
    • 剩余有效时间 = TTL - 获取锁耗时 > 0
  5. 执行业务逻辑释放锁

4. Redlock 算法中为什么要求在大多数节点上获取锁?

考察点: 分布式系统一致性算法、拜占庭容错

参考答案: 基于多数派原则(Majority Quorum):

  • 在 N 个节点中,任意两个多数派集合必然有交集
  • 例如 5 个节点,任意两个大小为 3 的集合至少有 1 个公共节点
  • 这保证了不可能有两个客户端同时在多数派节点上获取同一把锁
  • 提供了 fault tolerance:可以容忍少于一半的节点故障

编程实现题

5. 请用 Go 实现一个 Redlock 客户端,要求支持锁续期和优雅的错误处理

考察点: Go 并发编程、错误处理、接口设计

package main

import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"sync"
"time"

"github.com/go-redis/redis/v8"
)

type RedlockClient struct {
clients []*redis.Client
quorum int
}

type Lock struct {
key string
value string
ttl time.Duration
clients []*redis.Client
quorum int
acquired []bool
cancelFunc context.CancelFunc
}

// 获取锁的 Lua 脚本,确保原子性
const setLockScript = `
if redis.call("get", KEYS[1]) == false then
return redis.call("set", KEYS[1], ARGV[1], "PX", ARGV[2])
else
return false
end`

// 释放锁的 Lua 脚本,防止误删其他客户端的锁
const releaseLockScript = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end`

func NewRedlockClient(addrs []string) *RedlockClient {
clients := make([]*redis.Client, len(addrs))
for i, addr := range addrs {
clients[i] = redis.NewClient(&redis.Options{
Addr: addr,
})
}

return &RedlockClient{
clients: clients,
quorum: len(addrs)/2 + 1,
}
}

func (r *RedlockClient) Lock(key string, ttl time.Duration) (*Lock, error) {
value := generateUniqueValue()

lock := &Lock{
key: key,
value: value,
ttl: ttl,
clients: r.clients,
quorum: r.quorum,
acquired: make([]bool, len(r.clients)),
}

startTime := time.Now()

// 并发获取锁
var wg sync.WaitGroup
successCount := int32(0)

for i, client := range r.clients {
wg.Add(1)
go func(idx int, c *redis.Client) {
defer wg.Done()

ctx, cancel := context.WithTimeout(context.Background(), ttl/10)
defer cancel()

result := c.Eval(ctx, setLockScript, []string{key}, value, ttl.Milliseconds())
if result.Err() == nil && result.Val() != false {
lock.acquired[idx] = true
atomic.AddInt32(&successCount, 1)
}
}(i, client)
}

wg.Wait()

elapsed := time.Since(startTime)
validTime := ttl - elapsed

// 检查是否满足获取锁的条件
if int(successCount) >= r.quorum && validTime > 0 {
// 启动锁续期
ctx, cancel := context.WithCancel(context.Background())
lock.cancelFunc = cancel
go lock.renewLock(ctx)

return lock, nil
}

// 获取锁失败,清理已获取的锁
lock.Release()
return nil, fmt.Errorf("failed to acquire lock")
}

func (l *Lock) Release() error {
if l.cancelFunc != nil {
l.cancelFunc() // 停止续期
}

var wg sync.WaitGroup
for i, client := range l.clients {
if l.acquired[i] {
wg.Add(1)
go func(c *redis.Client) {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c.Eval(ctx, releaseLockScript, []string{l.key}, l.value)
}(client)
}
}

wg.Wait()
return nil
}

// 锁续期
func (l *Lock) renewLock(ctx context.Context) {
ticker := time.NewTicker(l.ttl / 3) // 每 1/3 TTL 续期一次
defer ticker.Stop()

for {
select {
case <-ctx.Done():
return
case <-ticker.C:
var wg sync.WaitGroup
for i, client := range l.clients {
if l.acquired[i] {
wg.Add(1)
go func(c *redis.Client) {
defer wg.Done()
renewCtx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
c.PExpire(renewCtx, l.key, l.ttl)
}(client)
}
}
wg.Wait()
}
}
}

func generateUniqueValue() string {
bytes := make([]byte, 16)
rand.Read(bytes)
return hex.EncodeToString(bytes)
}

6. 上述实现中为什么要使用 Lua 脚本?请说明其他可能的优化点

考察点: Redis 原子性操作、性能优化思维

参考答案:

使用 Lua 脚本的原因:

  1. 原子性:确保 GET 和 SET/DEL 操作的原子性
  2. 减少网络往返:一次网络请求完成复杂逻辑
  3. 避免竞态条件:防止在检查和设置之间被其他客户端抢占

优化点:

  1. 连接池管理:使用连接池避免频繁建连
  2. 超时策略:设置合理的网络超时时间
  3. 重试机制:实现指数退避重试
  4. 监控指标:添加锁获取成功率、耗时等监控
  5. 故障检测:实现节点健康检查和故障转移

深度分析题

7. Martin Kleppmann 对 Redlock 算法提出了哪些质疑?你如何看待这些问题?

考察点: 对分布式系统理论的深度理解、批判性思维

参考答案:

主要质疑点:

  1. 时钟偏移问题
  1. 进程暂停问题:GC、网络延迟可能导致锁在客户端不知情的情况下过期

个人观点:

  • Redlock 适用于对一致性要求不是特别严格的场景
  • 对于强一致性要求,应该使用基于 Raft/Paxos 的方案(如 etcd、Consul)
  • 实际应用中需要结合业务特点权衡 CAP 特性

8. 在 Go 中实现分布式锁时,还有哪些替代方案?请比较它们的优缺点

考察点: 技术选型能力、对不同中间件的理解

参考答案:

方案对比:

方案优点缺点适用场景
MySQL 行锁强一致性、事务支持性能较低、单点故障对一致性要求极高
Redis SET NX性能高、实现简单主从复制延迟问题性能要求高、一致性要求一般
Redlock解决主从复制问题复杂度高、时钟依赖多活部署、中等一致性要求
etcd强一致性、高可用性能相对较低对一致性要求高的分布式系统

9. 如何测试分布式锁的正确性?请设计测试方案

考察点: 测试思维、并发测试设计

func TestRedlockCorrectness(t *testing.T) {
const (
numClients = 10
lockKey = "test_lock"
duration = time.Second * 5
)

redlock := NewRedlockClient([]string{
"localhost:6379",
"localhost:6380",
"localhost:6381",
"localhost:6382",
"localhost:6383",
})

var (
successCount int64
counter int64
wg sync.WaitGroup
)

// 并发测试:多个客户端同时竞争锁
for i := 0; i < numClients; i++ {
wg.Add(1)
go func(clientID int) {
defer wg.Done()

lock, err := redlock.Lock(lockKey, duration)
if err != nil {
return
}

atomic.AddInt64(&successCount, 1)

// 模拟临界区操作
old := atomic.LoadInt64(&counter)
time.Sleep(100 * time.Millisecond) // 模拟业务耗时
atomic.StoreInt64(&counter, old+1)

lock.Release()
}(i)
}

wg.Wait()

// 验证互斥性:成功获取锁的次数应该等于计数器的值
assert.Equal(t, successCount, counter)

// 验证最终一致性
assert.True(t, successCount > 0, "至少应该有一个客户端获取到锁")
}

// 故障注入测试
func TestRedlockWithNodeFailure(t *testing.T) {
// 模拟节点故障、网络分区等异常情况
// ...
}

10. 在生产环境中使用 Redlock 需要注意哪些监控指标?

考察点: 生产环境经验、监控思维

参考答案:

关键监控指标:

  1. 锁性能指标

    • 锁获取成功率
    • 锁获取平均耗时、P99 耗时
    • 锁持有时间分布
  2. Redis 集群指标

    • 各节点可用性
    • 网络延迟
    • 内存使用率
  3. 业务指标

    • 并发冲突次数
    • 锁超时次数
    • 死锁检测

监控实现示例:

type RedlockMetrics struct {
LockAcquireTotal prometheus.Counter
LockAcquireFailures prometheus.Counter
LockDuration prometheus.Histogram
NodeAvailability prometheus.GaugeVec
}

func (r *RedlockClient) LockWithMetrics(key string, ttl time.Duration) (*Lock, error) {
start := time.Now()
defer func() {
r.metrics.LockDuration.Observe(time.Since(start).Seconds())
}()

lock, err := r.Lock(key, ttl)
if err != nil {
r.metrics.LockAcquireFailures.Inc()
return nil, err
}

r.metrics.LockAcquireTotal.Inc()
return lock, nil
}