Redis分布式锁实现的三种方式-基于setnx,lua脚本和Redisson

Redis实现分布式锁

分布式锁是解决分布式系统中多节点并发访问共享资源的核心方案,Redis凭借高性能、原子性操作等特性,成为实现分布式锁的主流选择。本文从原理层面拆解Redis分布式锁的核心逻辑,并详细分析三种常见实现方式的代码逻辑、优缺点及生产环境注意事项。

一、Redis分布式锁核心原理

1.1 核心设计目标

一个可靠的分布式锁需满足以下特性:

  • 互斥性:同一时刻只能有一个客户端持有锁,避免并发操作共享资源;
  • 安全性:锁只能由持有者释放,不能被其他客户端误删;
  • 超时释放:避免客户端持有锁后宕机,导致锁永久无法释放(死锁);
  • 原子性:加锁、释放锁的核心操作需原子执行,避免并发场景下的逻辑漏洞;
  • 可重入(可选):同一客户端持有锁后,再次请求锁时无需重新获取(增强易用性)。

1.2 Redis实现锁的核心基础

Redis通过以下核心命令支撑分布式锁实现:

命令/特性 作用
SET key value NX EX t 原子执行"不存在则设置(NX)+ 过期时间(EX)",避免加锁与设超时的拆分操作
DEL key 删除锁(释放锁),需配合校验锁归属,避免误删
Lua脚本 将"校验锁归属+释放锁"封装为原子操作,解决释放锁的并发安全问题
Redisson(客户端) 基于Redis封装了可重入、自动续期、公平锁等高级特性,简化锁的使用

二、三种实现方式详解(逻辑+问题分析)

方式1:基础实现(SetNX + 手动校验释放)

2.1 代码逻辑拆解
java 复制代码
@Resource
private StringRedisTemplate stringRedisTemplate;

/**
 * 示例:扣减库存(基础分布式锁实现)
 */
private void order(){
    // 1. 生成唯一锁值(用于校验锁归属,避免误删)
    String lockValue = UUID.randomUUID().toString();
    // 2. 加锁:SETNX + 过期时间(原子操作),30秒自动释放
    Boolean locked = stringRedisTemplate.opsForValue()
            .setIfAbsent("product:1001:lock", lockValue, 30, TimeUnit.SECONDS);
    try {
        // 3. 加锁成功则执行业务逻辑(扣减库存)
        if (locked) {
            Integer count = (Integer) stringRedisTemplate.opsForHash().get("product:1001","number");
            if (count > 0) {
                stringRedisTemplate.opsForHash().put("product:1001", "number", count - 1);
            }
        }
    } finally {
        // 4. 释放锁:先校验锁归属,再删除(非原子操作)
        if (lockValue.equals(stringRedisTemplate.opsForValue().get("product:1001:lock"))) {
            stringRedisTemplate.delete("product:1001:lock");
        }
    }
}
2.2 核心逻辑
  1. 加锁 :通过setIfAbsent(底层是SET NX EX)实现原子加锁,同时设置30秒超时,避免死锁;
  2. 锁归属校验 :用UUID生成唯一lockValue,释放锁前校验值是否匹配,防止误删其他客户端的锁;
  3. 释放锁:finally块中执行释放逻辑,确保业务执行完(或异常)后释放锁。
2.3 存在的核心问题
  • 释放锁非原子性 :"校验锁归属 + 删除锁"是两步操作,若校验后锁恰好过期,此时其他客户端已加锁,当前客户端执行delete会误删新锁;
  • 无重试机制:加锁失败直接放弃,实际场景中需结合业务设置重试逻辑(如循环重试+休眠);
  • 无锁续期:若业务执行时间超过30秒,锁会自动过期,导致多个客户端同时执行业务,破坏互斥性;
  • Hash操作类型转换风险stringRedisTemplate.opsForHash().get()返回Object,强转Integer可能出现类型异常(需先判空+类型校验)。

方式2:优化版(Lua脚本保证释放锁原子性)

2.1 代码逻辑拆解
java 复制代码
@Resource
private StringRedisTemplate stringRedisTemplate;

private static final String LOCK_KEY = "product:1001:lock";
private static final String STOCK_KEY = "product:1001:number";
private static final long LOCK_TIMEOUT = 30; // 锁超时时间(秒)
private static final long SLEEP_TIME = 100; // 重试间隔(毫秒)

private void order() {
    String lockValue = UUID.randomUUID().toString();

    try {
        // 1. 尝试获取锁(原子加锁)
        Boolean locked = tryAcquireLock(lockValue);
        if (!locked) {
            // 加锁失败可重试/返回失败(示例直接返回,实际可加循环重试)
            return;
        }

        // 2. 执行业务:获取并扣减库存(简化为String结构,避免Hash类型转换问题)
        String stockStr = stringRedisTemplate.opsForValue().get(STOCK_KEY);
        if (stockStr == null || Integer.parseInt(stockStr) <= 0) {
            return;
        }
        stringRedisTemplate.opsForValue().set(STOCK_KEY, String.valueOf(Integer.parseInt(stockStr) - 1));
    } finally {
        // 3. 释放锁:Lua脚本封装"校验+删除",保证原子性
        releaseLock(lockValue);
    }
}

/**
 * 原子加锁:SET NX EX
 */
private Boolean tryAcquireLock(String lockValue) {
    return stringRedisTemplate.opsForValue()
            .setIfAbsent(LOCK_KEY, lockValue, LOCK_TIMEOUT, TimeUnit.SECONDS);
}

/**
 * 原子释放锁:Lua脚本
 */
private void releaseLock(String lockValue) {
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
            "return redis.call('del', KEYS[1]) " +
            "else " +
            "return 0 " +
            "end";

    stringRedisTemplate.execute(
            new DefaultRedisScript<>(script, Long.class),
            Arrays.asList(LOCK_KEY),
            lockValue
    );
}
2.2 核心优化点
  1. 释放锁原子化:将"校验锁归属(get)+ 删除锁(del)"封装为Lua脚本,Redis会原子执行脚本内容,彻底解决方式1的"误删锁"问题;
  2. 简化库存存储:将库存从Hash改为String结构,避免类型转换异常,降低业务复杂度;
  3. 代码分层 :抽离tryAcquireLockreleaseLock方法,提升代码复用性。
2.3 仍存在的问题
  • 无锁续期 :核心问题未解决!若业务执行时间(如扣减库存需40秒)超过LOCK_TIMEOUT(30秒),锁会提前过期,导致并发安全问题;
  • 重试逻辑缺失:示例中加锁失败直接返回,实际场景需增加"循环重试+最大重试次数",避免因瞬时并发导致加锁失败;
  • 无异常处理Integer.parseInt(stockStr)未做异常捕获,若库存值非数字会抛出运行时异常;
  • 单点风险:依赖单个Redis节点,若节点宕机,锁数据丢失,可能导致多个客户端同时加锁。

方式3:生产级实现(Redisson客户端)

Redisson是Redis官方推荐的Java客户端,内置了分布式锁的完整实现,解决了手动实现的诸多痛点。

2.1 代码逻辑拆解
java 复制代码
@Resource
private RedissonClient redissonClient;
@Resource
private StringRedisTemplate stringRedisTemplate;

private void order() {
    // 1. 获取分布式锁对象(可重入锁)
    RLock lock = redissonClient.getLock("product:1001:lock");

    try {
        // 2. 加锁:最多等待10秒,锁30秒后自动释放;获取锁成功则执行业务
        if (lock.tryLock(10, 30, TimeUnit.SECONDS)) {
            try {
                // 3. 扣减库存业务逻辑
                Integer count = (Integer) stringRedisTemplate.opsForHash().get("product:1001","number");
                if (count != null && count > 0) {
                    stringRedisTemplate.opsForHash().put("product:1001", "number", count - 1);
                }
            } finally {
                // 4. 手动释放锁(若业务执行完未超时,主动释放)
                lock.unlock();
            }
        }
    } catch (InterruptedException e) {
        // 5. 中断异常处理,恢复线程中断状态
        Thread.currentThread().interrupt();
    }
}
2.2 核心优势(Redisson解决的痛点)
  1. 自动锁续期(看门狗机制)
    • 若业务执行时间超过锁超时时间,Redisson会启动后台线程(默认每10秒)自动将锁超时时间续期至30秒;
    • 只有当客户端正常释放锁或宕机时,续期才会停止,彻底解决"锁提前过期"问题。
  2. 可重入性 :基于Redis的Hash结构存储锁的持有次数,同一客户端多次tryLock不会导致死锁;
  3. 优雅的重试与等待tryLock(waitTime, leaseTime, unit)支持"最大等待时间",加锁失败时会阻塞等待,直到超时或获取到锁;
  4. 原子性加锁/释放锁:底层封装了Lua脚本,保证加锁、释放锁的原子性;
  5. 集群适配:支持Redis主从、哨兵、集群模式,解决单点风险(需配置Redisson的集群模式)。
2.3 需注意的细节
  • 解锁时机 :必须在finally块中执行unlock(),但需先判断lock.isHeldByCurrentThread(),避免未持有锁时执行解锁抛出异常;
  • 异常处理tryLock会抛出InterruptedException,需捕获并恢复线程中断状态,避免线程状态异常;
  • Redisson配置:生产环境需正确配置RedissonClient(如连接池、超时时间、集群节点),否则会导致锁性能下降或失效;
  • 锁粒度:避免使用过大的锁粒度(如"product:lock"),应细化到具体资源(如"product:1001:lock"),减少锁竞争。

三、三种实现方式对比与生产建议

实现方式 优点 缺点 适用场景
方式1(基础版) 代码简单、无额外依赖 释放锁非原子、无续期、易误删锁 测试环境、低并发非核心业务
方式2(Lua版) 释放锁原子化、代码结构清晰 无续期、重试逻辑需手动实现、单点风险 中小并发、核心逻辑简单场景
方式3(Redisson) 自动续期、可重入、集群适配 引入Redisson依赖、配置稍复杂 生产环境、高并发核心业务

生产环境核心建议

  1. 优先使用Redisson:手动实现分布式锁易遗漏边界条件(如续期、原子性、集群),Redisson封装了成熟的解决方案,是生产首选;
  2. 锁超时时间合理设置:结合业务平均执行时间设置(如业务平均执行5秒,设置超时30秒),避免过短导致续期频繁,过长导致死锁风险;
  3. 避免长时间持有锁:分布式锁应"快进快出",执行业务逻辑时避免耗时操作(如数据库慢查询、远程调用),必要时拆分锁粒度;
  4. 集群模式适配 :若Redis为集群/哨兵模式,Redisson需配置RedissonNodeClusterServersConfig,避免主从切换导致锁丢失;
  5. 兜底方案:分布式锁失效时,需有兜底逻辑(如数据库乐观锁),避免数据一致性问题。

四、总结

Redis分布式锁的核心是原子加锁+安全释放+超时兜底

  1. 基础实现(方式1)仅适用于测试,核心问题是释放锁非原子、无续期;
  2. Lua脚本优化版(方式2)解决了释放锁原子性问题,但仍需手动处理续期、重试等逻辑;
  3. Redisson(方式3)是生产级方案,通过看门狗机制、可重入性、集群适配,解决了手动实现的所有核心痛点。

生产环境中,除非有特殊定制需求,否则优先基于Redisson实现分布式锁,既保证可靠性,又降低开发和维护成本。

相关推荐
苏三说技术3 小时前
Claude Code从失控到起飞,只用了这些技巧
后端
长栎4 小时前
写 for 循环写了十年,你却从没用过迭代器模式最狠的那一面
后端
LiaCode4 小时前
Redis 在生产项目的使用
前端·后端
用户559822481224 小时前
Docker Compose Down 导致容器数据误删——ext4 日志恢复全记录
后端
LiaCode4 小时前
一天学完 redis 的爽翻版核心知识总结
前端·后端
大刚测试开发实战4 小时前
如何内网穿透访问本地私有化部署的TestHub
前端·后端·github
xiaodaoluanzha5 小时前
迄今為止,最簡單的編程語言 Nolang
前端·后端
Csvn5 小时前
Docker 容器管理入门 — 从镜像到容器编排
后端
用户762352425915 小时前
ShardingJDBC
后端
行者全栈架构师5 小时前
IDEA 中 Maven 项目的 15 个红色报错快速解决方法
java·后端