在分布式系统中,多个服务实例同时访问共享资源时,如何保证操作的原子性和一致性是个经典难题。想象一下电商系统中的库存扣减场景,如果两个订单同时触发库存变更,不加控制就会导致超卖。单机环境下我们可以用synchronized或ReentrantLock解决,但在分布式环境下这些本地锁就失效了——这就是分布式锁的用武之地。
Redis作为高性能的内存数据库,凭借其单线程执行模型和丰富的原子操作,成为实现分布式锁的热门选择。不过看似简单的锁机制背后藏着不少魔鬼细节:锁的互斥性如何保证?锁过期时间设置多长合适?客户端崩溃后如何避免死锁?这些正是不同Redis锁实现方案要解决的核心问题。
SETNX(SET if Not eXists)是Redis最基础的原子操作,只有key不存在时才会设置成功。这个特性天然适合实现互斥锁:
bash复制SETNX lock_key unique_value
当返回1表示获取锁成功,0则表示锁已被占用。释放锁时直接DEL对应的key即可。看似简单,但实际生产环境需要考虑以下几个关键点:
python复制import redis
import time
import uuid
class RedisLock:
def __init__(self, redis_client, lock_name, expire_time=30):
self.redis = redis_client
self.lock_name = lock_name
self.expire_time = expire_time
self.identifier = str(uuid.uuid4())
def acquire(self, timeout=10):
end = time.time() + timeout
while time.time() < end:
if self.redis.setnx(self.lock_name, self.identifier):
self.redis.expire(self.lock_name, self.expire_time)
return True
time.sleep(0.1)
return False
def release(self):
# Lua脚本保证原子性
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_name, self.identifier)
关键提示:必须使用唯一标识作为value,避免误删其他客户端的锁。实测中遇到过因未设置value导致锁冲突的案例
锁续期策略:
EXPIRE命令PEXPIRE实现毫秒级精度控制集群环境问题:
性能数据:
虽然SETNX方案基本可用,但在复杂场景下仍存在原子性问题。例如:
Redis的Lua脚本执行是原子性的,可以完美解决这些问题。我们来看具体实现。
lua复制-- KEYS[1] 锁key
-- ARGV[1] 锁value
-- ARGV[2] 过期时间(ms)
if redis.call("setnx", KEYS[1], ARGV[1]) == 1 then
return redis.call("pexpire", KEYS[1], ARGV[2])
else
return 0
end
这个脚本将SETNX和EXPIRE合并为一个原子操作,避免了客户端设置过期时间前崩溃导致死锁的问题。
lua复制-- KEYS[1] 锁key
-- ARGV[1] 预期value
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
该脚本确保只有锁的持有者才能释放锁,且操作是原子的。在Spring环境中可以这样使用:
java复制// 获取锁
Boolean success = redisTemplate.execute(new DefaultRedisScript<>(acquireScript, Boolean.class),
Collections.singletonList("order_lock"),
UUID.randomUUID().toString(),
"30000");
// 释放锁
Long result = redisTemplate.execute(new DefaultRedisScript<>(releaseScript, Long.class),
Collections.singletonList("order_lock"),
expectedValue);
| 操作类型 | QPS(单节点) | 平均延迟 | 原子性保证 |
|---|---|---|---|
| 原生SETNX | 120,000 | 0.8ms | 部分 |
| Lua脚本方案 | 90,000 | 1.2ms | 完整 |
| Redisson(下文) | 60,000 | 2.5ms | 完整 |
虽然Lua脚本性能稍低,但换来了更强的可靠性。在实际金融交易系统中,我们更倾向于选择这种方案。
Redisson是Redis官方推荐的Java客户端,其分布式锁实现具有以下特性:
java复制// 获取锁实例
RLock lock = redisson.getLock("orderLock");
try {
// 尝试加锁,最多等待100秒,锁有效期30秒
boolean res = lock.tryLock(100, 30, TimeUnit.SECONDS);
if (res) {
// 业务逻辑
}
} finally {
lock.unlock();
}
框架内部实现了完整的锁续期逻辑:默认每10秒检查一次,如果业务还在执行就延长锁有效期。这个设计解决了业务执行时间不确定的问题。
红锁(RedLock):
java复制RLock lock1 = redisson1.getLock("lock");
RLock lock2 = redisson2.getLock("lock");
RLock lock3 = redisson3.getLock("lock");
RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
lock.lock();
读写锁:
java复制RReadWriteLock rwLock = redisson.getReadWriteLock("anyRWLock");
rwLock.readLock().lock();
rwLock.writeLock().lock();
信号量:
java复制RSemaphore semaphore = redisson.getSemaphore("semaphore");
semaphore.acquire(3);
Redisson的锁实现主要依赖:
核心加锁逻辑的Lua脚本如下(简化版):
lua复制-- 检查锁是否存在
if (redis.call('exists', KEYS[1]) == 0) then
-- 设置hash字段
redis.call('hset', KEYS[1], ARGV[2], 1);
-- 设置过期时间
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 如果是同一线程则重入
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
redis.call('hincrby', KEYS[1], ARGV[2], 1);
redis.call('pexpire', KEYS[1], ARGV[1]);
return nil;
end;
-- 返回剩余过期时间
return redis.call('pttl', KEYS[1]);
| 维度 | SETNX方案 | Lua脚本方案 | Redisson方案 |
|---|---|---|---|
| 实现复杂度 | 高 | 中 | 低 |
| 可靠性 | 低 | 高 | 极高 |
| 功能完整性 | 基础锁 | 基础锁 | 全套分布式同步 |
| 运维成本 | 高 | 中 | 低 |
| 适合场景 | 简单临时锁 | 关键业务锁 | 企业级应用 |
锁粒度控制:
超时时间设置:
获取锁超时 << 锁持有超时监控指标:
网络分区问题:
GC停顿影响:
时钟漂移问题:
即使使用Redisson这样的成熟方案,分布式锁也不是万能的。在某些场景下,可以考虑:
除了Java的Redisson,其他语言也有优秀实现:
在实际架构设计中,我们通常会根据业务特点组合多种同步机制。比如在订单系统中:
这种分层锁策略在保证正确性的同时,也兼顾了系统性能。经过多次618和双11大促的验证,这套方案能够支撑每秒十万级的并发交易。