redis分布式锁

Redis 分布式锁是面试和实战中的高频话题。下面我从原理最佳实践系统性地讲解,并附上 Java 代码示例。


一、为什么需要分布式锁?

在单机环境下,可以使用 synchronized 或 ReentrantLock 实现线程安全。但在分布式系统中,多个进程(不同 JVM、不同机器)需要互斥地访问共享资源时,就需要一个跨进程的锁

分布式锁应满足:

  • 互斥性:同一时刻只有一个客户端持有锁。
  • 避免死锁:锁最终能被释放(即使持有者崩溃)。
  • 高可用:锁服务自身需要高可用。
  • 可重入性(可选):同一个客户端可以多次获取同一把锁。
  • 阻塞/非阻塞(可选)。

二、基于 Redis 的简单分布式锁实现

1. 最简版本:SET NX EX

利用 Redis 的 SET key value NX EX seconds 命令(原子性设置,只有 key 不存在时才成功,并同时设置过期时间)。

java

arduino
 体验AI代码助手
 代码解读
复制代码
public boolean tryLock(String lockKey, String requestId, int expireSeconds) {
    String result = jedis.set(lockKey, requestId, "NX", "EX", expireSeconds);
    return "OK".equals(result);
}
  • lockKey:锁的标识。
  • requestId:唯一标识(如 UUID),用于释放时验证,避免误删他人的锁。
  • expireSeconds:锁自动过期时间,防止持有者崩溃导致死锁。

2. 释放锁(必须用 Lua 保证原子性)

如果直接 del lockKey,可能会误删别人持有的锁(比如业务执行超过过期时间,锁自动释放,当前线程又去删除)。因此释放时需要检查 value 是否匹配,且这两步必须原子。

lua

vbnet
 体验AI代码助手
 代码解读
复制代码
-- unlock.lua
if redis.call("get", KEYS[1]) == ARGV[1] then
    return redis.call("del", KEYS[1])
else
    return 0
end

Java 调用:

java

typescript
 体验AI代码助手
 代码解读
复制代码
public boolean unlock(String lockKey, String requestId) {
    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, requestId);
    return Long.valueOf(1).equals(result);
}

这个版本的问题

  • 锁过期时间不好设置:业务可能超时,导致锁提前释放,别的线程获得锁,当前线程业务完成后会误删别人的锁(即使我们用了唯一值校验,但当前线程的 requestId 与新的持有者不同,释放会失败,但业务数据可能已被并发破坏)。
  • 不可重入:同一个线程无法再次获取同一把锁。

三、Redisson:生产级解决方案

Redisson 是 Redis 官方推荐的 Java 客户端,封装了分布式锁的完整实现,支持可重入自动续期(看门狗)公平锁读写锁等。

1. 引入依赖

xml

xml
 体验AI代码助手
 代码解读
复制代码
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.23.0</version>
</dependency>

2. 配置 Redisson

java

ini
 体验AI代码助手
 代码解读
复制代码
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
RedissonClient redisson = Redisson.create(config);
RLock lock = redisson.getLock("myLock");

3. 使用锁(自动续期)

java

csharp
 体验AI代码助手
 代码解读
复制代码
lock.lock(); // 阻塞等待,默认看门狗 30 秒续期,每 1/3 时间自动续期
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

4. 带超时时间的锁(非看门狗)

java

ini
 体验AI代码助手
 代码解读
复制代码
boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 等待最多 10 秒,获取后锁有效期最多 30 秒(不自动续期)

5. Redisson 锁的原理(简要)

  • 可重入:采用 Hash 结构,key 为锁名,field 为线程 ID(或 UUID),value 为重入次数。
  • 看门狗:获取锁成功后,会启动一个定时任务,定期(锁过期时间的 1/3)刷新锁的过期时间,只要持有锁的线程还在运行,锁就不会过期。
  • 释放:解锁时减少重入次数,次数为 0 时才真正删除。

四、RedLock:多节点 Redis 分布式锁

Redis 主从架构存在主从切换时锁丢失的风险:客户端 A 在 master 获取锁,master 还没来得及同步到 slave 就宕机,slave 晋升为 master,此时客户端 B 可能获得同一把锁,导致互斥失效。

为了解决这个问题,Redis 作者提出 RedLock 算法:

  1. 在多个独立的 Redis master 节点(通常 5 个)上获取锁。
  2. 获取锁时使用相同的 key 和随机值,设置一个较短的过期时间(如 10 秒)。
  3. 客户端按顺序向所有节点请求锁,使用相同的超时时间(远小于锁过期时间)。
  4. 大多数节点(N/2 + 1)  成功获取锁,且总耗时小于锁有效时间,则认为获取成功。
  5. 释放锁时,向所有节点发送释放请求。

Redisson 也提供了 RedissonRedLock 实现。

RedLock 的争议

  • 官方推荐,但业界对其正确性有质疑(时钟漂移、网络分区等问题)。
  • 大部分场景下,单节点 + 主从自动故障转移 + 业务幂等 已经足够。如果对锁安全性要求极高,可以考虑 ZooKeeper 或 etcd。

五、最佳实践与常见问题

1. 锁粒度与锁范围

  • 锁应该只覆盖需要互斥的代码块,不要在整个方法上加锁。
  • 锁的 key 设计要包含业务维度,如 lock:order:1001 表示订单 1001 的锁。

2. 避免长时间持有锁

  • 业务应快速执行,如果确实需要长时间操作,应使用看门狗续期或拆分任务。
  • 如果无法预估时间,可考虑使用 Redis 的 SETEX 加轮询,或改用 ZooKeeper 的临时顺序节点。

3. 超时与重试

  • 获取锁时应设置合理的等待时间和超时时间,避免死等。
  • 重试时采用指数退避策略,避免 Redis 压力过大。

4. 锁的监控与报警

  • 监控锁的等待时间、持有时间,异常时报警。

5. 结合业务幂等

  • 即使锁偶尔失效,如果能保证业务幂等(如数据库唯一索引、状态机),可以进一步提升鲁棒性。

六、完整的 Redis 分布式锁 Java 示例(基于 Jedis + Lua)

java

arduino
 体验AI代码助手
 代码解读
复制代码
public class RedisDistributedLock {
    private Jedis jedis;
    private String lockKey;
    private String requestId;
    private int expireSeconds;

    public RedisDistributedLock(Jedis jedis, String lockKey, int expireSeconds) {
        this.jedis = jedis;
        this.lockKey = lockKey;
        this.expireSeconds = expireSeconds;
        this.requestId = UUID.randomUUID().toString();
    }

    public boolean tryLock() {
        String result = jedis.set(lockKey, requestId, "NX", "EX", expireSeconds);
        return "OK".equals(result);
    }

    public void unlock() {
        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, requestId);
    }
}

使用: https://www.gaoding.com/templates/关于北京159.1415.8529北京开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于天津159.1415.8529天津开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于上海159.1415.8529上海开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于重庆159.1415.8529重庆开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于太原159.1415.8529太原开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于石家庄159.1415.8529石家庄开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于呼和浩特159.1415.8529呼和浩特开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于沈阳159.1415.8529沈阳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于吉林159.1415.8529吉林开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于长春159.1415.8529长春开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于哈尔滨159.1415.8529哈尔滨开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南京159.1415.8529南京开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于苏州159.1415.8529苏州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于杭州159.1415.8529杭州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于合肥159.1415.8529合肥开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于厦门159.1415.8529厦门开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于福州159.1415.8529福州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南昌159.1415.8529南昌开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于济南159.1415.8529济南开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于青岛159.1415.8529青岛开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于郑州159.1415.8529郑州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于武汉159.1415.8529武汉开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于长沙159.1415.8529长沙开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于深圳159.1415.8529深圳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于广州159.1415.8529广州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于东莞159.1415.8529东莞开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于南宁159.1415.8529南宁开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于海口159.1415.8529海口开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于三亚159.1415.8529三亚开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于成都159.1415.8529成都开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于贵阳159.1415.8529贵阳开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于昆明159.1415.8529昆明开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于拉萨159.1415.8529拉萨开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于西安159.1415.8529西安开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于兰州159.1415.8529兰州开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于西宁159.1415.8529西宁开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于银川159.1415.8529银川开具仪器设备发票‖第一财经.html https://www.gaoding.com/templates/关于乌鲁木齐159.1415.8529乌鲁木齐开具仪器设备发票‖第一财经.html

java

csharp
 体验AI代码助手
 代码解读
复制代码
RedisDistributedLock lock = new RedisDistributedLock(jedis, "order:1001", 10);
if (lock.tryLock()) {
    try {
        // 业务处理
    } finally {
        lock.unlock();
    }
} else {
    // 获取锁失败,重试或降级
}

七、总结

方案优点缺点适用场景
SET NX EX + Lua实现简单,性能高需手动处理续期、不可重入简单互斥,业务时间可控
Redisson功能完善,自动续期,可重入引入额外依赖绝大多数分布式锁需求
RedLock解决主从切换导致的锁丢失实现复杂,存在争议对安全性要求极高的场景

核心原则

  • 使用原子命令或 Lua 脚本保证锁操作的原子性。
  • 释放锁时验证持有者,避免误删。
  • 设置合理的过期时间,结合看门狗或业务重试。

Redis 分布式锁是解决并发问题的利器,但也要结合业务场景选择合适的方案,并做好容错与监控。

八、其他分布式方案

方案一致性性能(QPS)可靠性实现复杂度典型应用
Redis弱(最终一致)极高(10w+)中(主从切换可能丢锁)简单高并发缓存、短时锁
ZooKeeper中(万级)复杂配置中心、分布式协调
etcd中(万级)中等云原生、服务发现
数据库强(单库)低(千级)依赖数据库简单低并发、已有数据库系统

选择建议

  • 追求性能、允许少量锁失效 → Redis(配合业务幂等)。
  • 要求强一致性、可容忍性能稍低 → ZooKeeper 或 etcd。
  • 系统简单、不想引入新组件 → 数据库唯一索引。
  • 云原生环境 → etcd 是首选。
0
0
0
0
评论
未登录
暂无评论