1. 分布式锁的核心需求与挑战
在分布式系统中,当多个服务实例需要互斥访问共享资源时,传统的单机锁机制就失效了。比如电商系统中的库存扣减、秒杀活动中的商品抢购,都需要确保同一时间只有一个请求能执行业务逻辑。这就是分布式锁要解决的核心问题——跨进程的互斥访问控制。
Redis实现分布式锁的优势在于:
- 高性能:基于内存操作,响应时间在毫秒级
- 原子性:通过Lua脚本或SETNX命令保证操作的不可分割性
- 可重入:通过线程标识支持同一线程多次获取锁
- 自动过期:避免死锁,通过TTL自动释放
但实现一个健壮的Redis分布式锁需要解决几个关键问题:
- 原子性获取锁与设置过期时间
- 避免误删其他客户端的锁
- 锁续期机制处理长耗时任务
- 集群环境下的可靠性保证
2. 基础实现方案与原理
2.1 SETNX + EXPIRE 基础版
最基础的实现方式是组合使用SETNX和EXPIRE命令:
java复制// 获取锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent("lock_key", "1");
if(locked) {
redisTemplate.expire("lock_key", 30, TimeUnit.SECONDS);
try {
// 执行业务逻辑
} finally {
redisTemplate.delete("lock_key");
}
}
但这个方案存在严重缺陷——SETNX和EXPIRE不是原子操作,如果在设置过期时间前进程崩溃,会导致锁永远无法释放。生产环境绝对不能用这种实现。
2.2 Redis 2.6+ 的原子性方案
Redis 2.6版本后扩展了SET命令的参数,支持原子化操作:
java复制String result = redisTemplate.opsForValue().set(
"lock_key",
"client1",
Duration.ofSeconds(30),
RedisStringCommands.SetOption.SET_IF_ABSENT
);
这个命令等价于:
code复制SET lock_key client1 NX EX 30
NX表示只有key不存在时才设置,EX设置过期时间,整个操作是原子的。这才是正确的实现基础。
3. 生产级实现方案
3.1 防止误删锁机制
基础方案存在锁被其他客户端误删的风险。比如:
- 客户端A获取锁,设置30秒过期
- 客户端A执行业务逻辑耗时35秒,锁已自动释放
- 客户端B获取到锁
- 客户端A执行完删除锁,此时删除的是B的锁
解决方案是在删除前验证锁的值是否是自己设置的:
java复制String clientId = UUID.randomUUID().toString();
try {
// 获取锁
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
"lock_key",
clientId,
Duration.ofSeconds(30)
);
if(locked) {
// 执行业务逻辑
}
} finally {
// 只有锁的值是自己设置的才删除
String value = redisTemplate.opsForValue().get("lock_key");
if(clientId.equals(value)) {
redisTemplate.delete("lock_key");
}
}
3.2 Lua脚本保证原子性
上面的检查+删除操作仍然不是原子的,在并发下可能出问题。Redis支持用Lua脚本保证多个操作的原子性:
lua复制if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
Java中调用:
java复制String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList("lock_key"),
clientId
);
4. 高级特性实现
4.1 锁续期机制
对于执行时间不确定的长任务,需要实现锁续期(看门狗机制)。可以启动一个后台线程定期延长锁的过期时间:
java复制private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
// 获取锁后启动续期任务
scheduler.scheduleAtFixedRate(() -> {
String value = redisTemplate.opsForValue().get("lock_key");
if(clientId.equals(value)) {
redisTemplate.expire("lock_key", 30, TimeUnit.SECONDS);
}
}, 10, 10, TimeUnit.SECONDS); // 每10秒续期一次
注意要在释放锁时取消这个定时任务。
4.2 可重入锁实现
Java中的ReentrantLock支持同一个线程多次获取锁,Redis分布式锁也可以实现类似功能:
java复制// 使用ThreadLocal记录重入次数
private ThreadLocal<Integer> lockCount = ThreadLocal.withInitial(() -> 0);
public boolean tryLock() {
Integer count = lockCount.get();
if(count > 0) {
lockCount.set(count + 1);
return true;
}
// 正常获取锁逻辑...
if(success) {
lockCount.set(1);
return true;
}
return false;
}
public void unlock() {
Integer count = lockCount.get();
if(count == null || count <= 0) {
throw new IllegalStateException();
}
if(count > 1) {
lockCount.set(count - 1);
return;
}
// 真正释放锁
lockCount.remove();
// 执行删除锁的逻辑...
}
5. 集群环境下的特殊处理
5.1 Redlock算法
在Redis集群环境下,简单的单节点锁可能失效。Redis作者提出了Redlock算法:
- 获取当前时间(毫秒)
- 依次向N个Redis节点请求锁(使用相同的key和随机值)
- 计算获取锁消耗的时间(当前时间减去步骤1的时间),当且仅当大多数节点(N/2+1)获取成功,并且总耗时小于锁的过期时间,才认为获取成功
- 如果获取失败,要向所有节点发起释放锁请求
Java实现示例:
java复制public boolean tryRedLock(List<RedisTemplate> redisTemplates, String lockKey, String clientId, long expireTime) {
long startTime = System.currentTimeMillis();
int successCount = 0;
for(RedisTemplate redis : redisTemplates) {
if(redis.opsForValue().setIfAbsent(lockKey, clientId, expireTime, TimeUnit.MILLISECONDS)) {
successCount++;
}
}
long costTime = System.currentTimeMillis() - startTime;
return successCount >= redisTemplates.size()/2 + 1
&& costTime < expireTime;
}
5.2 集群脑裂问题
在Redis集群发生网络分区时可能出现脑裂,导致多个客户端同时获取锁。Redlock算法也不能完全解决这个问题,需要根据业务场景权衡:
- 对一致性要求极高的场景,可能需要使用Zookeeper等强一致性系统
- 对性能要求高的场景,可以接受极低概率的锁失效,通过业务层做幂等处理
6. 生产环境注意事项
6.1 性能优化建议
- 锁的粒度要尽可能细,比如按用户ID或订单ID加锁,而不是全局锁
- 合理设置锁超时时间,过长会影响系统可用性,过短可能导致任务未完成锁就释放
- 获取锁失败后要有退避策略,比如指数退避,避免大量重试导致Redis压力过大
java复制// 指数退避示例
long baseDelay = 100;
long maxDelay = 10000;
long delay = baseDelay;
while(!tryGetLock()) {
Thread.sleep(delay);
delay = Math.min(delay * 2, maxDelay);
}
6.2 常见问题排查
- 锁永远不释放:检查是否在所有代码路径(包括异常)都正确释放了锁
- 锁被其他客户端释放:确保删除锁时验证了客户端标识
- 获取锁耗时过长:可能是Redis负载过高或网络问题,需要监控Redis性能指标
- 锁竞争激烈:考虑优化业务逻辑减少锁持有时间,或使用分段锁
7. 与Spring框架集成
7.1 基于AOP的注解式锁
可以定义一个分布式锁注解:
java复制@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DistributedLock {
String key();
long expire() default 30000;
TimeUnit timeUnit() default TimeUnit.MILLISECONDS;
}
然后通过AOP实现锁的自动获取和释放:
java复制@Aspect
@Component
public class DistributedLockAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(distributedLock)")
public Object around(ProceedingJoinPoint joinPoint, DistributedLock distributedLock) throws Throwable {
String lockKey = distributedLock.key();
String clientId = UUID.randomUUID().toString();
try {
Boolean locked = redisTemplate.opsForValue().setIfAbsent(
lockKey,
clientId,
distributedLock.expire(),
distributedLock.timeUnit()
);
if(!locked) {
throw new RuntimeException("获取锁失败");
}
return joinPoint.proceed();
} finally {
// Lua脚本释放锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(lockKey),
clientId
);
}
}
}
7.2 Spring Boot Starter实现
可以进一步封装成Spring Boot Starter,通过配置文件控制锁的行为:
yaml复制distributed-lock:
redis:
enable: true
default-expire: 30000
prefix: "lock:"
然后实现自动配置类:
java复制@Configuration
@ConditionalOnClass(RedisTemplate.class)
@EnableConfigurationProperties(DistributedLockProperties.class)
public class DistributedLockAutoConfiguration {
@Bean
public DistributedLockAspect distributedLockAspect(RedisTemplate<String, String> redisTemplate) {
return new DistributedLockAspect(redisTemplate);
}
}
8. 替代方案对比
虽然Redis是实现分布式锁的常用方案,但也有其他选择:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Redis | 性能高,实现简单 | 强一致性保证较弱 | 高性能要求的AP系统 |
| Zookeeper | 强一致性,可靠性高 | 性能较低,实现复杂 | 对一致性要求高的CP系统 |
| 数据库 | 无需额外组件 | 性能差,有死锁风险 | 简单的低频场景 |
| etcd | 一致性保证强 | 社区资源相对较少 | Kubernetes环境 |
选择时需要考虑:
- 系统的CAP需求(一致性vs可用性)
- 锁的获取频率和性能要求
- 已有技术栈和运维能力
9. 实战案例:秒杀系统库存扣减
假设我们要实现一个秒杀系统的库存扣减,使用Redis分布式锁确保不超卖:
java复制@DistributedLock(key = "'seckill:' + #itemId", expire = 5000)
public boolean deductStock(Long itemId, Integer quantity) {
// 查询当前库存
Integer stock = redisTemplate.opsForValue().get("stock:" + itemId);
if(stock == null) {
// 从数据库加载库存到Redis
stock = loadStockFromDB(itemId);
}
// 检查库存是否充足
if(stock < quantity) {
return false;
}
// 扣减库存
redisTemplate.opsForValue().decrement("stock:" + itemId, quantity);
return true;
}
关键点:
- 锁的key包含商品ID,实现细粒度锁
- 锁的过期时间设置为5秒,避免长时间阻塞
- 库存数据预热到Redis,减少数据库访问
- 使用Redis的原子操作decrement保证库存扣减的原子性
10. 监控与运维建议
生产环境使用Redis分布式锁需要完善的监控:
- 锁等待时间:记录获取锁的耗时,超过阈值报警
- 锁竞争情况:监控锁的获取失败率,评估系统压力
- 锁泄漏检测:定期扫描长时间持有的锁,可能是程序bug导致未释放
- Redis性能指标:监控Redis的内存、CPU、网络等指标
可以使用Prometheus + Grafana搭建监控看板,关键指标包括:
- distributed_lock_acquire_time
- distributed_lock_failure_count
- distributed_lock_hold_time
运维方面需要注意:
- Redis的持久化配置,避免重启导致锁信息丢失
- 合理的连接池配置,避免锁竞争时连接耗尽
- 集群部署时确保节点间的网络延迟在可接受范围内