在分布式系统中,多个进程或服务同时访问共享资源时,如何保证操作的原子性和一致性是个经典难题。想象一下电商系统中的库存扣减场景:如果两个订单同时触发库存变更,不加控制就会导致超卖。单机环境下我们用线程锁就能解决,但分布式环境下各服务位于不同物理节点,传统锁机制完全失效。
分布式锁需要满足三个基本要求:
Redis因其单线程特性、高性能和丰富的原子命令,成为实现分布式锁的热门选择。但实践中我们遇到过各种意外情况:网络延迟导致锁超时失效、客户端阻塞引发锁误释放、主从切换产生多锁等。接下来我会分享经过生产验证的Redis分布式锁实现方案。
早期我们使用SETNX key value命令尝试获取锁,但这种方式存在明显缺陷:
bash复制# 问题代码示例
SETNX lock_key 1 # 获取锁
DEL lock_key # 释放锁
改进方案是给锁设置过期时间:
bash复制SETNX lock_key 1
EXPIRE lock_key 10
但这两条命令非原子操作,可能在SETNX成功后EXPIRE执行前客户端崩溃。Redis 2.6.12后我们有了更好的解决方案。
Redis 2.6.12开始SET命令支持NX和EX选项,可以原子性地实现锁获取和过期设置:
bash复制SET lock_key unique_value NX EX 10
参数说明:
释放锁时需要验证value匹配,避免误删其他客户端的锁:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
当需要更高可靠性时,可以采用Redis作者提出的Redlock算法。该算法需要至少5个独立的Redis主节点(非集群模式),基本流程:
注意:实际部署时需要根据网络延迟调整过期时间,建议设置自动续期机制
对于执行时间不确定的长任务,需要实现锁续期(看门狗机制)。以Java为例:
java复制private ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
public boolean renewLock(String lockKey, String value, int expireTime) {
executor.scheduleAtFixedRate(() -> {
if (getLockValue(lockKey).equals(value)) {
redisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
}
}, 0, expireTime / 3, TimeUnit.SECONDS); // 每1/3过期时间续期一次
}
在Redlock算法中,如果某台Redis服务器时间发生跳跃:
解决方案:
当客户端获取锁后发生长时间GC或网络阻塞,锁可能在此期间过期。其他客户端获取锁后,原客户端恢复执行,导致临界区代码被多个客户端同时执行。
解决方案:
在Redis主从架构下,如果主节点获取锁后未同步到从节点就崩溃,可能导致多个客户端同时获取锁。
解决方案:
根据业务场景选择合适锁粒度:
推荐方案:
java复制// 按业务ID哈希分片
int shard = orderId.hashCode() % 16;
String lockKey = "order_lock:" + shard;
当锁被占用时,常见的处理方式:
退避算法示例:
java复制int maxRetries = 3;
long baseDelay = 100; // 初始延迟ms
long maxDelay = 1000; // 最大延迟ms
for (int i = 0; i < maxRetries; i++) {
if (tryLock()) return true;
long delay = Math.min(baseDelay * (1 << i), maxDelay);
Thread.sleep(delay + random.nextInt(50));
}
python复制import redis
import time
import uuid
class RedisLock:
def __init__(self, redis_client, lock_key, expire=30):
self.redis = redis_client
self.lock_key = lock_key
self.expire = expire
self.identifier = str(uuid.uuid4())
def acquire(self):
end = time.time() + 10 # 超时时间
while time.time() < end:
if self.redis.set(self.lock_key, self.identifier, nx=True, ex=self.expire):
return True
time.sleep(0.001)
return False
def release(self):
script = """
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end"""
self.redis.eval(script, 1, self.lock_key, self.identifier)
go复制package main
import (
"context"
"fmt"
"time"
"github.com/go-redis/redis/v8"
)
type RedisLock struct {
client *redis.Client
key string
value string
expire time.Duration
}
func NewRedisLock(client *redis.Client, key string, expire time.Duration) *RedisLock {
return &RedisLock{
client: client,
key: key,
value: fmt.Sprintf("%d", time.Now().UnixNano()),
expire: expire,
}
}
func (l *RedisLock) Acquire(ctx context.Context) (bool, error) {
return l.client.SetNX(ctx, l.key, l.value, l.expire).Result()
}
func (l *RedisLock) Release(ctx context.Context) error {
script := `
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end`
return l.client.Eval(ctx, script, []string{l.key}, l.value).Err()
}
conf复制# 防止内存不足
maxmemory 4gb
maxmemory-policy allkeys-lru
# 提高网络性能
tcp-keepalive 60
bash复制# 开发环境
dev:order:lock:123
# 生产环境
prod:order:lock:123
bash复制# 查找长期存在的锁
redis-cli --scan --pattern '*lock*' | while read key; do
ttl=$(redis-cli ttl "$key")
if [ $ttl -eq -1 ]; then
echo "锁泄漏: $key"
fi
done
在实际项目中,我们曾遇到因未设置合适过期时间导致系统死锁的情况。后来通过添加监控告警,当锁持有时间超过阈值时立即通知运维人员。同时建议在获取锁时记录堆栈信息,便于排查锁未释放的问题根源。