Redis系列--实现一个简单的redis分布式锁

分布式锁常见的实现方式主要有三种:

  1. 数据库乐观锁
  2. 基于 Redis 的分布式锁
  3. 基于 ZooKeeper 的分布式锁

为了确保分布式锁可用,至少要满足以下四个条件:

  1. 互斥性:任意时刻只有一个客户端能持有锁。
  2. 不会死锁:即使客户端在持有锁期间崩溃而未解锁,其他客户端依然可以继续加锁。
  3. 容错性:只要大部分 Redis 节点正常运行,客户端就可以加锁和解锁。
  4. 解铃还须系铃人:加锁和解锁必须是同一个客户端,不能解掉别人加的锁。

正确的 Redis 分布式锁实现

1. 正确的加锁代码

java 复制代码
public class RedisTool {
    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    /**
     * 尝试获取分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @param expireTime 超期时间(毫秒)
     * @return 是否获取成功
     */
    public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
        return LOCK_SUCCESS.equals(result);
    }
}

2. 错误的加锁示例

arduino 复制代码
public static void wrongGetLock1(Jedis jedis, String lockKey, String requestId, int expireTime) {
    Long result = jedis.setnx(lockKey, requestId);
    if (result == 1) {
        // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
        jedis.expire(lockKey, expireTime);
    }
}

❌ 这个实现的问题在于 setnx 和 expire 不是原子操作,如果程序崩溃,就可能造成锁永远无法释放。


正确的解锁代码

typescript 复制代码
public class RedisTool {
    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * 释放分布式锁
     * @param jedis Redis客户端
     * @param lockKey 锁
     * @param requestId 请求标识
     * @return 是否释放成功
     */
    public static boolean releaseDistributedLock(Jedis jedis, 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, Collections.singletonList(lockKey), Collections.singletonList(requestId));
        return RELEASE_SUCCESS.equals(result);
    }
}

这段 Lua 脚本会先获取锁对应的 value,判断是否和 requestId 一致,如果一致才删除锁(解锁)。

使用 eval() 可以保证操作的原子性,避免误解锁。


错误的解锁示例

1. 不判断是否为同一客户端

typescript 复制代码
public static void wrongReleaseLock1(Jedis jedis, String lockKey) {
    jedis.del(lockKey);
}

❌ 问题:直接删除锁,可能误删别人加的锁。

2. 非原子操作

typescript 复制代码
public static void wrongReleaseLock2(Jedis jedis, String lockKey, String requestId) {
    // 判断加锁与解锁是否是同一个客户端
    if (requestId.equals(jedis.get(lockKey))) {
        // 若在此时,这把锁突然过期,客户端B已经加锁
        // 执行 del() 就可能把客户端B的锁误删
        jedis.del(lockKey);
    }
}

❌ 问题:get + del 分两步执行,可能导致锁被错误释放。


总结

  • 加锁 :一定要保证原子操作,可以直接用 SET key value NX PX expireTime。
  • 解锁:必须保证原子性,并且只允许锁的拥有者解锁,推荐使用 Lua 脚本。
  • 避免错误 :不要拆分加锁和设置过期时间,不要直接 del 锁。

在生产环境中,也可以使用 Redisson 这样的开源库,它封装好了 Redis 分布式锁的各种细节,避免踩坑。

相关推荐
海鸥-w1 小时前
什么是AOP
java
仍然.1 小时前
Redis---缓存
数据库·redis·缓存
Cc.Y1 小时前
Java零基础入门:可变字符串与包装类:StringBuilder、StringBuffer 与 通讯录管理系统实战
java·开发语言·python
paopaokaka_luck2 小时前
基于springboot+vue的智慧家政管理系统(AI问答、 WebSocket实时沟通、ECharts运营分析、订单规则与服务追踪)
java·javascript·spring boot·数据分析·echarts·mybatis
sm_926787052 小时前
RFID 标签打印的技术实现要点与二次开发实践
java·大数据·前端·c++·编辑器
阿森_dev2 小时前
2027 届毕设选题怎么选:30 个 Java 方向选题的实现难度排序
java·开发语言·spring boot·毕业设计·课程设计
Sarvartha2 小时前
Lambda 表达式和函数式接口
java·开发语言
计算机毕业设计杰瑞2 小时前
【计算机毕设精品】基于SpringBoot的一体化教务与学业质量管理系统的设计与实现,附源码_增删改查_java项目_可完美运行部署_网站项目
java·spring boot·课程设计
colinu2 小时前
C++ 异步任务与线程生命周期:`std::async`、`future`、`thread`、`jthread` 与协作式退出
java·jvm·c++
蜗牛互联网3 小时前
Python + JSON Schema 实现工单结构化输出与本地复核
java·人工智能·后端