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

相关推荐
2301_780789664 小时前
跨平台适配!SDK 游戏盾兼容全终端游戏场景
java·网络·ddos
m0_639906945 小时前
Redis入门|五种数据结构、使用场景、持久化RDB&AOF
数据结构·数据库·redis
学长毕设5 小时前
基于springboot的食品仓库管理系统
java·spring boot·后端
步行cgn6 小时前
FormHttpMessageConverter 详解:Spring MVC 中表单数据的读写转换器
java·spring·mvc
龙亘川6 小时前
构建城市公共安全数字底座:亘川智城智慧安防系统全维度解读
java·大数据·开发语言·人工智能·信息可视化
金銀銅鐵6 小时前
[Java] 用 CLI 展示 EnumMap 增删键值对时的内部变化
java·源码·ai编程
秋天的怀念_David6 小时前
第二十二夜 · 数字迷雾
java·程序人生·算法
Raas1007 小时前
AI网关部署方案?从部署到上线全流程
java·开发语言·人工智能·gateway·ai网关·企业级ai网关·企业级ai产品
niucloud-admin7 小时前
NIUCLOUD PHP SAAS上门家政使用文档:城市管理
java·前端·php
霸道流氓气质8 小时前
智能客服组件:分层意图识别、RAG Advisor 与人工转接状态机的 Vue3 + Spring AI 生产级实战
java·人工智能·spring