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

相关推荐
微尘寒风6 小时前
【Git】的安装和使用
java·git
y = xⁿ6 小时前
DeepSeek Harness 学习日记:关于Agent接口,工具调用的底层实现
android·java·学习
侧耳倾听1117 小时前
jwt使用简介
java·jwt
顶点多余7 小时前
那些在算法中适合巩固的知识点---1
java·前端·算法
AI人工智能+电脑小能手7 小时前
大白话说Java设计模式-46-责任链模式(业务实战篇)
java·设计模式·责任链模式·风控校验·servlet filter·spring interceptor·链式处理
jay神8 小时前
【计算机毕业设计】基于SpringBoot的程序教学辅助系统
java·前端·vue.js·spring boot·后端·毕业设计·课程设计
2601_962066498 小时前
【Sql Server】Update中的From语句,以及常见更新操作方式
android·java·数据库
yaoxin5211239 小时前
507. Java 反射 - 在 BeanFactory 中实现依赖注入
java·开发语言
OPEN-F9 小时前
C++模板教程:变参模板、折叠表达式与SFINAE
java·开发语言·c++
2601_9620722110 小时前
SpringMVC进阶(自定义拦截器以及异常处理)
java