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);
    }
}

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


正确的解锁代码

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 分布式锁的各种细节,避免踩坑。

相关推荐
SHoM SSER9 小时前
Spring Boot性能提升的核武器,速度提升500%!
java·spring boot·后端
weixin_425023009 小时前
Spring Boot 2.7 + JDK8 集成 Knife4j 4.1.0 教程(仅展示带注解接口)
java·spring boot·后端
追风林9 小时前
arthas 插件 使用中文
java
rleS IONS9 小时前
Spring Boot(七):Swagger 接口文档
java·spring boot·后端
AI浩9 小时前
别卷 Prompt 了,2026 年 AI 工程的新战场是 Harness
java·人工智能·prompt
IT从业者张某某9 小时前
Dockerfile详解
java·开发语言
BPM_宏天低代码9 小时前
【宏天源码】CRM系统的通信中心:邮件/短信/企微消息集成
java·spring boot·freemarker·crm系统
弹简特10 小时前
【JavaEE24-后端部分】 从“手动锁门”到“保安统一站岗”:Spring Boot 拦截器轻松搞定登录校验
java·spring boot·拦截器