Redis 分布式锁:从原理到实战的完整指南

发布于:2025-09-11 ⋅ 阅读:(17) ⋅ 点赞:(0)

🔒 Redis 分布式锁:从原理到实战的完整指南

🧠 一、分布式锁基础概念

💡 为什么需要分布式锁?

在分布式系统中,​​跨进程/跨服务的资源同步​​是常见需求:

微服务A
共享资源
微服务B
微服务C

典型应用场景​​:

  • 🛒 ​​防止重复下单​​:同一用户同时发起多个订单请求
  • ⚡ ​​秒杀库存控制​​:高并发下的库存扣减
  • 🔄 ​​定时任务防重​​:确保分布式环境下任务只执行一次
  • 📝 ​​数据一致性保证​​:避免并发写导致的数据错误

⚠️ 分布式锁的挑战

​​实现分布式锁必须解决的四大问题​​:

  1. 互斥性​​:同一时刻只有一个客户端能持有锁 ​​
  2. 死锁预防​​:锁必须能自动释放,防止死锁
  3. 容错性​​:即使部分节点故障,锁机制仍然可用
  4. 性能​​:高并发场景下的低延迟要求

📊 分布式锁方案对比

方案 实现复杂度 性能 可靠性 适用场景
Redis 单节点 中小规模应用
Redis 集群 大规模应用
ZooKeeper 强一致性场景
数据库锁 简单低频场景

⚡ 二、基于 SET NX 的实现

💡 基础实现原理

Redis 分布式锁的核心命令是 SET resource_name random_value NX PX timeout:

Client Redis SET lock:order:1234 uuid1234 NX PX 30000 OK 执行业务逻辑 DEL lock:order:1234 (nil) 等待或重试 alt [加锁成功] [加锁失败] Client Redis

🛠️ 完整加锁流程

客户端请求加锁
生成唯一标识UUID
执行SET NX PX命令
是否返回OK?
加锁成功
执行业务逻辑
加锁失败
等待或重试
业务完成
释放锁
结束

📝 基础实现代码

​​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 锁的问题​​:

  1. 单点故障​​:Redis 节点宕机导致锁服务不可用
  2. 主从延迟​​:主节点宕机时,从节点可能未同步锁信息
  3. ​​锁误删​​:过期时间估算不准确可能导致误删其他客户端锁

🔐 三、RedLock 算法深度解析

💡 RedLock 算法原理

RedLock 是 Redis 官方推荐的分布式锁算法,通过在​​多个独立 Redis 节点​​上获取锁来提高可靠性:

客户端
Redis节点1
Redis节点2
Redis节点3
Redis节点4
Redis节点5

🧮 RedLock 算法流程

​​加锁过程​​:

  1. 获取当前时间戳 T1
  2. 依次向 N 个 Redis 节点发送加锁命令
  3. 计算加锁耗时,确认锁的有效时间
  4. 当在多数节点(N/2+1)上加锁成功,且总耗时小于锁超时时间时,加锁成功
Client Node1 Node2 Node3 Node4 Node5 开始时间T1 SET lock_key uuid NX PX time SET lock_key uuid NX PX time SET lock_key uuid NX PX time SET lock_key uuid NX PX time SET lock_key uuid NX PX time OK OK OK (nil) (nil) 结束时间T2 计算耗时 = T2 - T1 检查: 1. 成功节点≥3个 2. 耗时 < 锁超时时间 Client Node1 Node2 Node3 Node4 Node5

⚙️ 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 的批评​​:

  1. ​​时钟跳跃问题​​:系统时钟不同步可能导致锁异常
  2. GC 停顿问题​​:长时间的 GC 停顿可能导致锁失效
  3. 网络延迟问题​​:网络分区可能导致锁状态不一致

​​应对策略​​:

// 使用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();
    }
}

网站公告

今日签到

点亮在社区的每一天
去签到