🔒 Redis 分布式锁:从原理到实战的完整指南
文章目录
🧠 一、分布式锁基础概念
💡 为什么需要分布式锁?
在分布式系统中,跨进程/跨服务的资源同步是常见需求:
典型应用场景:
- 🛒 防止重复下单:同一用户同时发起多个订单请求
- ⚡ 秒杀库存控制:高并发下的库存扣减
- 🔄 定时任务防重:确保分布式环境下任务只执行一次
- 📝 数据一致性保证:避免并发写导致的数据错误
⚠️ 分布式锁的挑战
实现分布式锁必须解决的四大问题:
- 互斥性:同一时刻只有一个客户端能持有锁
- 死锁预防:锁必须能自动释放,防止死锁
- 容错性:即使部分节点故障,锁机制仍然可用
- 性能:高并发场景下的低延迟要求
📊 分布式锁方案对比
方案 | 实现复杂度 | 性能 | 可靠性 | 适用场景 |
---|---|---|---|---|
Redis 单节点 | 低 | 高 | 中 | 中小规模应用 |
Redis 集群 | 中 | 高 | 高 | 大规模应用 |
ZooKeeper | 高 | 中 | 高 | 强一致性场景 |
数据库锁 | 低 | 低 | 高 | 简单低频场景 |
⚡ 二、基于 SET NX 的实现
💡 基础实现原理
Redis 分布式锁的核心命令是 SET resource_name random_value NX PX timeout:
🛠️ 完整加锁流程
📝 基础实现代码
Java 实现示例:
public class SimpleDistributedLock {
private Jedis jedis;
private String lockKey;
private String lockValue;
private int expireTime;
public boolean tryLock() {
// 生成唯一标识
lockValue = UUID.randomUUID().toString();
// 尝试加锁
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
return "OK".equals(result);
}
public boolean unlock() {
// 使用Lua脚本保证原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) " +
"else return 0 end";
Object result = jedis.eval(script, 1, lockKey, lockValue);
return Long.valueOf(1).equals(result);
}
}
⚠️ 基础实现的缺陷
单节点 Redis 锁的问题:
- 单点故障:Redis 节点宕机导致锁服务不可用
- 主从延迟:主节点宕机时,从节点可能未同步锁信息
- 锁误删:过期时间估算不准确可能导致误删其他客户端锁
🔐 三、RedLock 算法深度解析
💡 RedLock 算法原理
RedLock 是 Redis 官方推荐的分布式锁算法,通过在多个独立 Redis 节点上获取锁来提高可靠性:
🧮 RedLock 算法流程
加锁过程:
- 获取当前时间戳 T1
- 依次向 N 个 Redis 节点发送加锁命令
- 计算加锁耗时,确认锁的有效时间
- 当在多数节点(N/2+1)上加锁成功,且总耗时小于锁超时时间时,加锁成功
⚙️ RedLock 实现细节
Java RedLock 实现:
public class RedLock {
private List<Jedis> jedisList;
private String lockKey;
private String lockValue;
private int expireTime;
public boolean tryLock() {
int successCount = 0;
long startTime = System.currentTimeMillis();
// 尝试在所有节点上加锁
for (Jedis jedis : jedisList) {
try {
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
if ("OK".equals(result)) {
successCount++;
}
} catch (Exception e) {
// 记录日志,继续尝试其他节点
}
}
// 计算加锁耗时
long endTime = System.currentTimeMillis();
long costTime = endTime - startTime;
// 检查是否在多数节点上加锁成功且耗时合理
return successCount >= jedisList.size() / 2 + 1 &&
costTime < expireTime;
}
public void unlock() {
// 在所有节点上释放锁
for (Jedis jedis : jedisList) {
try {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script, 1, lockKey, lockValue);
} catch (Exception e) {
// 记录日志,继续释放其他节点
}
}
}
}
⚠️ RedLock 的争议与注意事项
Martin Kleppmann 的批评:
- 时钟跳跃问题:系统时钟不同步可能导致锁异常
- GC 停顿问题:长时间的 GC 停顿可能导致锁失效
- 网络延迟问题:网络分区可能导致锁状态不一致
应对策略:
// 使用fencing token保证操作的顺序性
public class FencingTokenLock {
private AtomicLong token = new AtomicLong(0);
public long acquireLock() {
// 获取锁的同时获取递增的token
if (tryLock()) {
return token.incrementAndGet();
}
return -1;
}
public void performOperation(long requiredToken) {
// 检查token有效性
if (token.get() > requiredToken) {
throw new IllegalStateException("操作基于过期的锁状态");
}
// 执行操作
}
}
🚀 四、实战应用案例
🛒 案例1:防止重复下单
业务场景:同一用户短时间内多次提交订单请求
解决方案:
public class OrderService {
private static final String ORDER_LOCK_PREFIX = "lock:order:";
private static final int LOCK_EXPIRE = 3000; // 3秒
public CreateOrderResult createOrder(String userId, OrderRequest request) {
String lockKey = ORDER_LOCK_PREFIX + userId;
String lockValue = UUID.randomUUID().toString();
try {
// 尝试获取锁
boolean locked = tryLock(lockKey, lockValue, LOCK_EXPIRE);
if (!locked) {
return CreateOrderResult.error("操作过于频繁,请稍后重试");
}
// 执行业务逻辑
return doCreateOrder(userId, request);
} finally {
// 释放锁
unlock(lockKey, lockValue);
}
}
private boolean tryLock(String key, String value, int expireMs) {
String result = jedis.set(key, value, "NX", "PX", expireMs);
return "OK".equals(result);
}
}
⚡ 案例2:秒杀库存控制
高并发场景下的库存扣减:
public class SeckillService {
private static final String STOCK_LOCK_PREFIX = "lock:seckill:";
private static final int LOCK_TIMEOUT = 100; // 100毫秒
public SeckillResult seckill(String productId, String userId) {
String lockKey = STOCK_LOCK_PREFIX + productId;
String lockValue = UUID.randomUUID().toString();
try {
// 非阻塞锁,快速失败
boolean locked = tryLockWithRetry(lockKey, lockValue, LOCK_TIMEOUT, 3);
if (!locked) {
return SeckillResult.error("秒杀太火爆了,请重试");
}
// 检查库存
int stock = getStock(productId);
if (stock <= 0) {
return SeckillResult.error("库存不足");
}
// 扣减库存
decreaseStock(productId);
createOrder(productId, userId);
return SeckillResult.success("秒杀成功");
} finally {
unlock(lockKey, lockValue);
}
}
private boolean tryLockWithRetry(String key, String value, int timeout, int maxRetries) {
for (int i = 0; i < maxRetries; i++) {
if (tryLock(key, value, timeout)) {
return true;
}
try {
Thread.sleep(10); // 短暂等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
return false;
}
}
🔄 案例3:分布式定时任务
确保分布式环境下任务只执行一次:
public class DistributedScheduler {
private static final String TASK_LOCK_PREFIX = "lock:task:";
private static final int TASK_LOCK_EXPIRE = 30000; // 30秒
public void executeScheduledTask(String taskId) {
String lockKey = TASK_LOCK_PREFIX + taskId;
String lockValue = UUID.randomUUID().toString();
try {
// 获取锁,如果获取失败说明其他节点正在执行
boolean locked = tryLock(lockKey, lockValue, TASK_LOCK_EXPIRE);
if (!locked) {
log.info("任务{}正在其他节点执行", taskId);
return;
}
// 执行任务
executeTask(taskId);
} finally {
// 注意:定时任务锁通常让它们自动过期,避免跨节点时间差问题
try {
unlock(lockKey, lockValue);
} catch (Exception e) {
log.warn("释放任务锁异常", e);
}
}
}
}
💡 五、总结与最佳实践
📊 方案选择指南
场景 | 推荐方案 | 理由 | 注意事项 |
---|---|---|---|
中小应用 | SET NX + Lua | 简单高效 | 需要单点Redis高可用 |
大型应用 | RedLock | 高可用性 | 需要5个以上独立节点 |
金融场景 | 数据库锁+Redis | 强一致性 | 性能较低 |
高并发 | 分段锁+Redis | 高性能 | 实现复杂度高 |
🔧 最佳实践总结
1. 锁设计原则:
- 🔑 唯一标识:每个锁使用唯一value,避免误删
- ⏰ 合理超时:根据业务操作时间设置合适的超时时间
- 🔄 自动释放:确保锁最终能被释放,防止死锁
- ❌ 避免嵌套:分布式锁不支持可重入性
2. 性能优化:
// 使用连接池减少网络开销
JedisPoolConfig poolConfig = new JedisPoolConfig();
poolConfig.setMaxTotal(100);
poolConfig.setMaxIdle(50);
JedisPool jedisPool = new JedisPool(poolConfig, "redis-host", 6379);
// 使用Pipeline批量操作
Pipeline pipeline = jedis.pipelined();
for (String lockKey : lockKeys) {
pipeline.set(lockKey, lockValue, "NX", "PX", expireTime);
}
List<Object> results = pipeline.syncAndReturnAll();
3. 监控告警:
# 监控锁等待时间
redis-cli --latency
# 监控锁竞争情况
redis-cli info stats | grep rejected
# 设置锁等待超时告警
# 当平均锁等待时间 > 100ms时告警
🚀 Redisson 高级特性
Redisson 分布式锁特性:
// 1. 可重入锁
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 可重入操作
lock.lock(); // 内部计数器+1
// ...
lock.unlock(); // 内部计数器-1
} finally {
lock.unlock();
}
// 2. 公平锁
RLock fairLock = redisson.getFairLock("fairLock");
// 3. 联锁(多个锁同时加锁)
RLock lock1 = redisson.getLock("lock1");
RLock lock2 = redisson.getLock("lock2");
RLock multiLock = redisson.getMultiLock(lock1, lock2);
// 4. 红锁(RedLock实现)
RLock redLock = redisson.getRedLock(lock1, lock2, lock3);
⚠️ 常见陷阱与解决方案
1. 锁过期时间问题:
// 错误:业务操作可能超过锁超时时间
jedis.set(lockKey, value, "NX", "PX", 30000);
// 长时间业务操作...
jedis.del(lockKey); // 锁可能已自动释放
// 解决方案:使用看门狗自动续期
private void startWatchdog(final String key, final String value, final int expireMs) {
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
if (isLockHeld(key, value)) {
jedis.expire(key, expireMs / 1000);
}
}, expireMs / 3, expireMs / 3, TimeUnit.MILLISECONDS);
}
2. 网络分区问题:
// 网络分区时可能产生脑裂
// 解决方案:使用fencing token
public class FencingTokenManager {
private AtomicLong token = new AtomicLong(0);
public long getNextToken() {
return token.incrementAndGet();
}
public boolean validateToken(long clientToken) {
return clientToken >= token.get();
}
}
3. 客户端崩溃问题:
// 确保锁最终能被释放
public class SafeDistributedLock {
public boolean tryLockWithLease(String key, String value, int expireMs) {
// 设置锁的同时启动守护线程
boolean locked = tryLock(key, value, expireMs);
if (locked) {
startLeaseMonitor(key, value, expireMs);
}
return locked;
}
private void startLeaseMonitor(String key, String value, int expireMs) {
Thread monitorThread = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(expireMs / 2);
if (!isLockHeld(key, value)) {
break;
}
renewLock(key, value, expireMs);
} catch (InterruptedException e) {
break;
}
}
});
monitorThread.setDaemon(true);
monitorThread.start();
}
}