Java 高并发场景下 Redis 分布式锁(UUID+Lua)最佳实践

一、核心原理:Redis 分布式锁的设计基石

1.1 分布式锁的核心要求

一款可靠的分布式锁需满足以下 4 点核心要求,否则易引发死锁、锁误删、数据不一致等问题:

  • 互斥性:同一时间只有一个线程能持有锁,杜绝并发竞争;

  • 安全性:仅持有锁的线程能释放锁,防止误删其他线程的锁;

  • 防死锁:锁需设置过期时间,避免线程持有锁后宕机导致锁永久占用;

  • 高可用:加锁、解锁操作高效,适配高并发场景,不成为性能瓶颈。

1.2 UUID+Lua 方案的核心逻辑

本方案通过"Redis 原子加锁 + UUID 唯一标识 + Lua 原子解锁"三者结合,满足上述要求:

  1. UUID 唯一标识:作为锁的 Value 值,绑定加锁线程,确保"锁归属唯一"。UUID 全局唯一,可避免分布式场景下(多服务、多线程)锁归属误判,替代线程 ID(进程内唯一,跨进程易重复);

  2. Redis 原子加锁 :使用 setIfAbsent(SETNX)操作,原子性完成"锁不存在则设置 + 过期时间",避免加锁与设置过期时间分离导致的死锁;

  3. Lua 原子解锁:通过 Lua 脚本原子执行"判断锁归属 + 删除锁",避免"判断"与"删除"两步操作分离导致的锁误删。

二、完整实现:通用 Redis 分布式锁工具类

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.Assert;

import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

/**
 * 通用 Redis 分布式等待锁工具类(UUID+Lua 方案)
 * 核心能力:超时等待加锁、原子解锁,适配高并发场景
 */
@Component
public class GenericRedisWaitLock {

    private final StringRedisTemplate stringRedisTemplate;

    // Lua 原子解锁脚本:验证锁归属(UUID)后删除,避免误删
    private static final String UNLOCK_LUA_SCRIPT = """
            if redis.call('get', KEYS[1]) == ARGV[1] then
                return redis.call('del', KEYS[1])
            else
                return 0
            end
            """;

    // 重试间隔(50ms):平衡重试效率与 CPU 占用
    private static final long DEFAULT_RETRY_INTERVAL = 50;

    // 构造器注入 Redis 模板(Spring 自动装配)
    public GenericRedisWaitLock(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    /**
     * 尝试获取分布式锁(支持自定义等待时间单位,过期时间固定为秒)
     * @param lockKey 锁键(如:lock:device:1001)
     * @param waitTime 最大等待时间(超时后放弃)
     * @param waitTimeUnit 等待时间单位(秒/毫秒等)
     * @param expireTimeSec 锁过期时间(秒,必须>0,防止死锁)
     * @return 锁标识(UUID):成功返回标识,失败返回 null
     */
    public String tryLock(String lockKey,
                          long waitTime,
                          TimeUnit waitTimeUnit,
                          long expireTimeSec) {
        // 入参校验:避免非法参数导致异常
        Assert.hasText(lockKey, "lockKey 不能为空");
        Assert.isTrue(waitTime >= 0, "waitTime 不能为负数");
        Assert.isTrue(expireTimeSec > 0, "expireTimeSec 必须大于 0");
        Assert.notNull(waitTimeUnit, "waitTimeUnit 不能为空");

        // 生成 UUID 作为锁标识,绑定当前线程
        String lockValue = UUID.randomUUID().toString();

        // 转换等待时间为毫秒,计算截止时间
        long waitTimeMs = waitTimeUnit.toMillis(waitTime);
        long deadline = System.currentTimeMillis() + waitTimeMs;

        // 循环尝试加锁,直到超时
        while (System.currentTimeMillis() < deadline) {
            // 原子加锁:不存在则设置值 + 过期时间
            Boolean lockSuccess = stringRedisTemplate.opsForValue()
                    .setIfAbsent(lockKey, lockValue, expireTimeSec, TimeUnit.SECONDS);

            // 加锁成功,返回 UUID 标识(解锁时需传入)
            if (Boolean.TRUE.equals(lockSuccess)) {
                return lockValue;
            }

            // 加锁失败,短暂休眠后重试(避免 CPU 空转)
            try {
                Thread.sleep(DEFAULT_RETRY_INTERVAL);
            } catch (InterruptedException e) {
                // 捕获中断异常,恢复线程状态并退出
                Thread.currentThread().interrupt();
                return null;
            }
        }

        // 等待超时,返回 null 表示加锁失败
        return null;
    }

    /**
     * 重载方法:等待时间与过期时间均为秒(极简调用)
     * @param lockKey 锁键
     * @param waitTimeSec 最大等待时间(秒)
     * @param expireTimeSec 锁过期时间(秒)
     * @return 锁标识(UUID)/null
     */
    public String tryLock(String lockKey, long waitTimeSec, long expireTimeSec) {
        return tryLock(lockKey, waitTimeSec, TimeUnit.SECONDS, expireTimeSec);
    }

    /**
     * 原子释放分布式锁
     * @param lockKey 锁键(与加锁时一致)
     * @param lockValue 锁标识(加锁时返回的 UUID)
     * @return true=解锁成功;false=锁不存在/非当前线程持有
     */
    public boolean unlock(String lockKey, String lockValue) {
        // 空值快速失败:避免空指针与无效解锁
        if (lockKey == null || lockValue == null) {
            return false;
        }

        // 初始化 Lua 脚本
        DefaultRedisScript<Long> unlockScript = new DefaultRedisScript<>();
        unlockScript.setScriptText(UNLOCK_LUA_SCRIPT);
        unlockScript.setResultType(Long.class);

        // 执行 Lua 脚本:原子判断并删除锁
        Long executeResult = stringRedisTemplate.execute(
                unlockScript,
                Collections.singletonList(lockKey),  // KEYS[1] = 锁键
                lockValue                             // ARGV[1] = UUID 标识
        );

        // 结果为 1 表示解锁成功,0 表示锁归属不匹配/已过期
        return executeResult != null && executeResult == 1;
    }
}

三、实战使用:高并发场景示例

以"设备序号递增"和"FDT 巡检记录创建"两个典型高并发场景为例,演示工具类的使用方式,重点体现"加锁-执行业务-解锁"的完整流程。

3.1 场景一:设备序号递增(避免重复/跳号)

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class DeviceSeqService {

    private final GenericRedisWaitLock redisWaitLock;
    private final StringRedisTemplate stringRedisTemplate;

    // 设备序号缓存键
    private static final String DEVICE_SEQ_KEY = "device:seq:current";
    // 设备序号锁键
    private static final String DEVICE_SEQ_LOCK_KEY = "lock:device:seq";
    // 加锁配置:最多等 3 秒,锁过期 10 秒
    private static final long WAIT_TIME_SEC = 3;
    private static final long EXPIRE_TIME_SEC = 10;

    public DeviceSeqService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) {
        this.redisWaitLock = redisWaitLock;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 生成下一个设备序号(高并发安全)
    public Long generateNextSeq() {
        String lockValue = null;
        try {
            // 1. 获取分布式锁
            lockValue = redisWaitLock.tryLock(DEVICE_SEQ_LOCK_KEY, WAIT_TIME_SEC, EXPIRE_TIME_SEC);
            if (lockValue == null) {
                throw new RuntimeException("获取锁超时,序号生成失败");
            }

            // 2. 一查二判三更新(临界区业务)
            String currentSeqStr = stringRedisTemplate.opsForValue().get(DEVICE_SEQ_KEY);
            Long currentSeq = currentSeqStr == null ? 0 : Long.parseLong(currentSeqStr);
            if (currentSeq < 0) {
                throw new RuntimeException("设备序号异常");
            }
            Long newSeq = currentSeq + 1;
            stringRedisTemplate.opsForValue().set(DEVICE_SEQ_KEY, newSeq.toString());

            return newSeq;
        } finally {
            // 3. 最终释放锁(必须在 finally 中,确保锁释放)
            if (lockValue != null) {
                redisWaitLock.unlock(DEVICE_SEQ_LOCK_KEY, lockValue);
            }
        }
    }
}

3.2 场景二:FDT 巡检记录创建(避免重复创建)

java 复制代码
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class FdtInspectService {

    private final GenericRedisWaitLock redisWaitLock;
    private final StringRedisTemplate stringRedisTemplate;

    // 巡检任务缓存键前缀(设备 ID 为后缀)
    private static final String INSPECT_TASK_PREFIX = "fdt:inspect:task:";
    // 巡检锁键前缀(设备 ID 为后缀,细粒度锁)
    private static final String INSPECT_LOCK_PREFIX = "lock:fdt:inspect:";
    // 加锁配置
    private static final long WAIT_TIME_SEC = 3;
    private static final long EXPIRE_TIME_SEC = 10;

    public FdtInspectService(GenericRedisWaitLock redisWaitLock, StringRedisTemplate stringRedisTemplate) {
        this.redisWaitLock = redisWaitLock;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    // 创建设备巡检记录(高并发安全)
    public boolean createInspectTask(String deviceId) {
        String lockKey = INSPECT_LOCK_PREFIX + deviceId;
        String taskKey = INSPECT_TASK_PREFIX + deviceId;
        String lockValue = null;

        try {
            // 1. 获取设备维度的细粒度锁(不影响其他设备并发)
            lockValue = redisWaitLock.tryLock(lockKey, WAIT_TIME_SEC, EXPIRE_TIME_SEC);
            if (lockValue == null) {
                return false; // 加锁失败,说明已有采集器处理该设备
            }

            // 2. 一查二判三更新(临界区业务)
            String taskStatus = stringRedisTemplate.opsForValue().get(taskKey);
            if ("RUNNING".equals(taskStatus)) {
                return false; // 已存在巡检任务,不重复创建
            }
            // 创建设备任务(设置为运行中状态,有效期 30 分钟)
            stringRedisTemplate.opsForValue().set(taskKey, "RUNNING", 30, java.util.concurrent.TimeUnit.MINUTES);
            // 模拟巡检记录入库逻辑
            saveInspectTaskToDb(deviceId);
            return true;
        } finally {
            // 3. 释放锁
            if (lockValue != null) {
                redisWaitLock.unlock(lockKey, lockValue);
            }
        }
    }

    // 模拟巡检记录入库
    private void saveInspectTaskToDb(String deviceId) {
        // 实际业务中替换为数据库写入逻辑
    }
}

四、关键注意事项与最佳实践

4.1 锁的粒度设计

优先使用细粒度锁(如场景二中按设备 ID 加锁),而非全局锁。细粒度锁可减少锁竞争,提升系统并发能力;全局锁会导致所有线程排队,成为性能瓶颈。

4.2 过期时间设置

锁过期时间(expireTimeSec)需满足:过期时间 > 业务最大耗时,建议设置为业务最大耗时的 1.5-2 倍。例如业务最多执行 5 秒,过期时间可设为 10 秒,避免锁提前过期导致并发问题。

4.3 解锁操作规范

  • 解锁必须传入加锁时返回的 UUID 标识,否则 Lua 脚本会拒绝解锁,防止误删;

  • 解锁操作必须放在 finally 块中,确保无论业务执行成功还是异常,锁都能最终释放;

  • 解锁失败仅需日志记录,无需抛异常(锁已自动过期,不影响数据一致性)。

4.4 加锁失败处理

加锁失败(返回 null)时,可根据业务场景选择处理方式:

  • 抛异常:适用于必须获取锁才能执行的业务(如序号生成);

  • 返回失败:适用于非核心业务(如巡检记录创建);

  • 有限重试:通过循环重试获取锁(需控制重试次数,避免无限循环)。

4.5 避免常见坑

  • 不直接使用 delete 解锁:直接删除锁会导致误删其他线程的锁,必须用 Lua 脚本原子解锁;

  • 不忽略中断异常:捕获 InterruptedException 后需恢复线程中断状态,避免线程状态错乱;

  • 不使用线程 ID 作为锁标识:线程 ID 跨进程易重复,无法满足分布式场景需求。

五、总结

Redis 分布式锁(UUID+Lua)方案,通过 UUID 保证锁归属唯一,通过 Lua 脚本保证解锁原子性,结合 Redis 高性能原子操作,可完美解决分布式高并发场景下的数据一致性问题。本文提供的工具类轻量化、无业务耦合,可直接复用;实战示例覆盖了常见高并发场景,给出了细粒度锁、过期时间配置等最佳实践。

在实际开发中,需根据业务特点调整加锁等待时间、过期时间,合理设计锁粒度,避开常见坑点,才能充分发挥该方案的优势,构建高可用、高并发的分布式系统。

相关推荐
落子君2 小时前
设计模式之【 断路器模式】
java
添砖java。。。2 小时前
java实现mqtt链接并控制门锁设备
java·开发语言
Jul1en_2 小时前
【Redis】Set类型、命令及应用场景
数据库·redis·缓存
xier_ran2 小时前
【C++】static 关键字与 const 关键字的作用
java·数据库·microsoft
凭君语未可2 小时前
为什么需要代理?从一个基础问题理解 JDK 静态代理
java·开发语言
橙露2 小时前
Redis 缓存穿透、击穿、雪崩解决方案
数据库·redis·缓存
Makoto_Kimur2 小时前
Agent 面试速成清单
java·agent
程序员雷欧2 小时前
Redis基础知识全解析:从数据结构到生产实战
数据结构·数据库·redis
人道领域3 小时前
【黑马点评日记02】Redis缓存优化:商户查询性能提升百倍
java·spring boot·spring·servlet·tomcat·intellij-idea