在分布式系统中,多个服务实例同时访问共享资源时,如何保证操作的互斥性成为一个关键问题。Redis分布式锁正是为解决这一问题而生的经典方案。要理解Redis分布式锁,我们需要从最基础的设计目标开始。
一个健壮的分布式锁必须满足以下五个关键特性:
互斥性:这是锁最基础的功能。在任何时刻,只能有一个客户端持有锁。想象一下银行转账场景,如果两个操作同时修改同一个账户余额而没有互斥保护,必然会导致数据不一致。
安全性:锁必须只能由持有它的客户端释放。假设客户端A获取了锁,但在释放前锁过期了,此时客户端B获取了锁,如果客户端A仍然尝试释放锁,就会错误地释放客户端B的锁。这种"误删"问题在实际项目中经常导致难以排查的bug。
超时释放:为了防止客户端崩溃或网络分区导致锁永远无法释放,锁必须设置合理的过期时间。这就像我们现实生活中的租约,到期后自动失效,避免资源被无限期占用。
原子性:加锁和释放锁的操作必须是原子的。例如,检查锁是否存在和设置锁这两个操作如果不能原子执行,就可能出现多个客户端同时认为自己获取了锁的情况。
可重入性(可选):同一个客户端在持有锁的情况下,可以再次成功获取该锁。这个特性主要是为了方便锁的嵌套调用,避免死锁。比如递归函数中需要多次获取同一个锁的场景。
Redis之所以能成为实现分布式锁的首选,主要得益于它提供的几个关键特性:
单线程模型:Redis的单线程特性保证了命令执行的原子性,这对于实现锁的互斥性至关重要。
丰富的原子命令:SET key value NX EX seconds命令可以原子性地完成"不存在则设置"和"设置过期时间"两个操作,这是实现锁的基础。
Lua脚本支持:通过Lua脚本,我们可以将多个操作封装成一个原子操作。这在实现复杂的锁释放逻辑时特别有用。
高性能:Redis的高吞吐量和低延迟特性,使得基于它实现的分布式锁对系统性能影响很小。
提示:虽然Redis单节点就能实现分布式锁,但在生产环境中建议使用Redis集群模式,以提高可用性。当使用集群时,需要注意Redlock算法等分布式锁的特殊实现方式。
让我们从一个最基本的实现开始,这是很多开发者最初接触Redis分布式锁的方式:
java复制public class BasicRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
public void doBusiness(String lockKey) {
String lockValue = UUID.randomUUID().toString();
boolean locked = false;
try {
// 尝试获取锁
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (locked) {
// 执行业务逻辑
executeBusiness();
}
} finally {
// 释放锁
if (locked) {
String currentValue = redisTemplate.opsForValue().get(lockKey);
if (lockValue.equals(currentValue)) {
redisTemplate.delete(lockKey);
}
}
}
}
private void executeBusiness() {
// 具体的业务逻辑
}
}
这个实现有几个值得注意的地方:
锁值生成:使用UUID作为锁的值,这是为了确保每个锁请求都有唯一标识,避免不同客户端的锁值冲突。
原子加锁:setIfAbsent方法对应Redis的SET NX EX命令,原子性地完成"不存在则设置"和"设置过期时间"两个操作。
锁释放机制:在finally块中释放锁,确保即使业务逻辑抛出异常,锁也能被释放。释放前会检查锁的值是否匹配,防止误删其他客户端的锁。
虽然这个实现看起来简单直接,但它存在几个严重的问题:
释放锁的非原子性:检查锁值和删除锁是两个独立操作,这中间可能有其他操作插入。比如:
缺乏锁续期机制:如果业务执行时间超过锁的过期时间,锁会自动释放,可能导致多个客户端同时进入临界区。
不具备可重入性:同一个客户端无法多次获取同一个锁,这在某些场景下会带来不便。
注意:这种基础实现只适合用于测试环境或对一致性要求不高的场景。在生产环境中使用这种实现可能会导致严重的数据不一致问题。
为了解决基础实现中锁释放的非原子性问题,我们可以使用Lua脚本将检查锁值和删除锁的操作封装成一个原子操作:
java复制public class LuaRedisLock {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String UNLOCK_SCRIPT =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end";
public void doBusiness(String lockKey) {
String lockValue = UUID.randomUUID().toString();
boolean locked = false;
try {
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (locked) {
executeBusiness();
}
} finally {
if (locked) {
// 使用Lua脚本原子性地释放锁
redisTemplate.execute(
new DefaultRedisScript<>(UNLOCK_SCRIPT, Long.class),
Collections.singletonList(lockKey),
lockValue
);
}
}
}
}
这种实现方式解决了基础版本最严重的"误删锁"问题:
原子性释放:Lua脚本在Redis中会作为一个整体执行,中间不会被其他命令打断,完美解决了检查和删除之间的竞态条件。
性能影响小:Lua脚本在Redis中执行,减少了网络往返开销。
代码更健壮:消除了锁释放过程中的竞态条件,提高了系统的可靠性。
虽然Lua脚本解决了原子释放的问题,但这种实现仍然有一些不足:
锁续期问题未解决:业务执行时间超过锁过期时间的问题依然存在。
缺乏重试机制:当获取锁失败时,没有自动重试的机制。
单点故障风险:如果使用单Redis节点,当该节点宕机时,锁服务将完全不可用。
不具备可重入性:和基础实现一样,这种方案也不支持锁的重入。
Redisson是Redis官方推荐的Java客户端,它提供了完整的分布式锁实现,解决了我们前面遇到的所有问题:
自动锁续期(看门狗机制):如果业务执行时间较长,Redisson会自动延长锁的持有时间。
可重入性:同一个线程可以多次获取同一个锁。
丰富的锁类型:除了基本的可重入锁,还支持公平锁、联锁、红锁等高级特性。
完善的超时和重试机制:提供了灵活的锁获取等待时间配置。
集群支持:适配Redis的各种集群模式,提高了可用性。
下面是使用Redisson实现分布式锁的典型代码:
java复制public class RedissonLockService {
@Autowired
private RedissonClient redissonClient;
public void doBusiness(String lockKey) {
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试获取锁,最多等待100ms,锁持有时间30s
boolean locked = lock.tryLock(100, 30000, TimeUnit.MILLISECONDS);
if (locked) {
try {
executeBusiness();
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("Lock acquisition interrupted", e);
}
}
}
看门狗机制:
可重入性实现:
丰富的锁类型:
在使用Redisson分布式锁时,有几个重要的实践建议:
合理设置锁超时时间:
避免锁粒度过大:
正确处理异常情况:
集群环境配置:
| 特性 | 基础实现 | Lua脚本优化 | Redisson |
|---|---|---|---|
| 互斥性 | ✓ | ✓ | ✓ |
| 安全释放 | ✗ | ✓ | ✓ |
| 自动续期 | ✗ | ✗ | ✓ |
| 可重入性 | ✗ | ✗ | ✓ |
| 集群支持 | ✗ | ✗ | ✓ |
| 实现复杂度 | 简单 | 中等 | 复杂 |
| 生产环境适用性 | 不推荐 | 有限场景 | 推荐 |
开发测试环境:
生产环境:
特殊场景:
减少锁持有时间:
锁分段:
适当设置超时:
监控与告警:
症状:
解决方案:
症状:
解决方案:
症状:
解决方案:
症状:
解决方案:
RedLock是Redis作者提出的分布式锁算法,用于在多个独立Redis节点上实现更可靠的分布式锁。核心思想是:
虽然RedLock提供了更高的可靠性,但它也带来了更复杂的实现和性能开销,需要根据业务需求权衡使用。
在某些场景下,可以考虑不使用分布式锁:
对于高并发场景下的锁优化:
在实际项目中,我遇到过因为不合理使用分布式锁导致的性能问题。一个典型的案例是:在用户注册流程中,对用户名检查使用了分布式锁,导致注册接口的TPS非常低。后来我们通过预检查+最终数据库唯一约束的方式,完全移除了这个分布式锁,性能提升了数十倍。