面试题答案
一键面试1. 了解脑裂
脑裂是指Redis集群在某些网络异常情况下,集群被分割成多个小的子集群,每个子集群都认为自己是主集群,从而导致数据不一致等问题。在这种情况下,分布式锁可能会在不同子集群中被重复获取,造成锁的误判和丢失。
2. 设计思路
- 多节点获取锁
- 传统的分布式锁通常基于单个Redis节点实现,在脑裂场景下很容易出现问题。可以采用多节点获取锁的方式,例如在多个Redis节点上同时尝试获取锁。
- 比如,在获取锁时,对N个不同的Redis节点都发送SETNX(SET if Not eXists)命令。只有当在大多数(超过半数,假设N为奇数,即
(N + 1)/2
个)节点上成功设置了锁,才认为锁获取成功。 - 代码示例(以Python和Redis - Py为例):
import redis
def multi_node_lock(redis_clients, lock_key, lock_value, expire_time):
success_count = 0
for client in redis_clients:
if client.set(lock_key, lock_value, nx = True, ex = expire_time):
success_count += 1
total_nodes = len(redis_clients)
majority = (total_nodes + 1) // 2
if success_count >= majority:
return True
else:
for client in redis_clients:
client.delete(lock_key)
return False
- 锁的有效期和续租
- 设置合适的锁有效期。如果锁的有效期过长,在脑裂恢复后,旧的锁可能还未释放,导致新的请求无法获取锁;如果过短,可能会出现业务还未执行完,锁就自动释放的情况。
- 可以引入续租机制,在业务执行过程中,定期检查锁的剩余时间,如果剩余时间较短,就对锁进行续租(重新设置有效期)。
- 代码示例(以Java和Jedis为例):
import redis.clients.jedis.Jedis;
import java.util.concurrent.TimeUnit;
public class DistributedLock {
private Jedis jedis;
private String lockKey;
private String lockValue;
private int expireTime;
public DistributedLock(Jedis jedis, String lockKey, String lockValue, int expireTime) {
this.jedis = jedis;
this.lockKey = lockKey;
this.lockValue = lockValue;
this.expireTime = expireTime;
}
public boolean acquireLock() {
return "OK".equals(jedis.set(lockKey, lockValue, "NX", "EX", expireTime));
}
public void renewLock() {
jedis.expire(lockKey, expireTime);
}
}
- 使用Redlock算法
- Redlock算法是Redis官方推荐的分布式锁实现算法。它基于多个独立的Redis节点(至少5个)。
- 具体步骤如下:
- 获取当前时间戳
T1
。 - 依次向N个Redis节点发送获取锁的请求(使用SETNX命令),每个请求设置相同的锁key、value和过期时间。
- 计算获取锁的总耗时
T
,如果获取到锁的节点数大于等于(N + 1)/2
且T < expire_time
,则认为锁获取成功。 - 如果锁获取成功,计算锁的实际有效时间为
expire_time - T
。 - 如果锁获取失败,向所有已经获取到锁的节点发送释放锁的命令(DEL命令)。
- 获取当前时间戳
- 代码示例(以Go语言和Redigo库为例):
package main
import (
"fmt"
"github.com/gomodule/redigo/redis"
"time"
)
const (
numNodes = 5
lockKey = "my_lock"
lockValue = "unique_value"
expireTime = 1000 // milliseconds
)
func redlock() bool {
nodes := make([]redis.Conn, numNodes)
for i := 0; i < numNodes; i++ {
conn, err := redis.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", 6379 + i))
if err != nil {
return false
}
nodes[i] = conn
}
start := time.Now()
successCount := 0
for _, conn := range nodes {
if _, err := conn.Do("SET", lockKey, lockValue, "NX", "EX", expireTime/1000); err == nil {
successCount++
}
}
elapsed := time.Since(start).Milliseconds()
if successCount >= (numNodes+1)/2 && elapsed < int64(expireTime) {
return true
}
for _, conn := range nodes {
conn.Do("DEL", lockKey)
}
return false
}
- 监控和自动修复
- 部署监控系统(如Prometheus + Grafana)来实时监测Redis集群的状态,包括节点数量、网络连接等。
- 当检测到脑裂发生时,自动触发修复机制,例如通过脚本重启或重新配置Redis节点,使其重新组成完整的集群。同时,在修复过程中,暂停分布式锁相关的业务操作,避免在不稳定状态下出现锁的问题。
通过以上设计思路,可以在Redis集群脑裂场景下,尽可能保证基于Redis的分布式锁原子操作的稳定可靠,减少锁的误判和丢失问题。