欢迎光临
我们一直在努力

Redis分布式锁灵活运用:原理与误删问题解决

Redis 分布式锁灵活运用:从原理到解决误删问题

摘要:在集群部署或分布式系统中,传统的 synchronized 和 ReentrantLock 因局限于单 JVM 进程而失效。本文深入探讨基于 Redis 的分布式锁实现方案,从 setnx 命令的原子性操作入手,解决死锁隐患。通过手写 ILock 接口与 SimpleRedisLock 实现类,展示如何在 Spring Boot 项目中落地分布式锁,并重点剖析“锁误删”问题的成因与解决方案(唯一标识校验),为高并发场景下的数据安全提供可靠保障。


一、为什么需要分布式锁?

在单体架构中,我们习惯使用 synchronized 或 ReentrantLock 来保证线程安全。然而,当项目演进为微服务架构或集群部署模式时,JVM 层面的锁无法跨进程生效。此时,我们需要一种多进程可见且互斥的锁机制,即分布式锁。

一个合格的分布式锁必须满足以下三个核心条件:

  • 互斥性:任意时刻,只能有一个客户端持有锁。
  • 高可用性:锁服务本身必须稳定,不能成为单点故障。
  • 安全性:具备防死锁机制(如超时自动释放),防止因服务宕机导致锁无法释放。
  • 主流方案对比

    特性MySQLRedisZookeeper
    互斥实现 利用数据库唯一索引或行锁 利用 setnx 命令的原子性 利用临时有序节点的唯一性
    高可用 好(主从复制) 好(哨兵/集群模式) 好(ZAB 协议)
    高性能 一般(依赖磁盘 I/O) 极好(纯内存操作) 一般(需多次网络交互)
    安全性 断开连接自动释放 需手动设置过期时间 临时节点,断开自动删除

    结论:在对性能要求极高且业务允许短暂不一致的场景下(如秒杀、库存扣减),Redis 是实现分布式锁的首选方案。


    二、Redis 分布式锁的核心原理

    1. 基础命令:setnx 与原子性

    Redis 提供了 SETNX (SET if Not Exists) 命令,只有当 Key 不存在时才会设置成功,天然具备互斥性。

    # 尝试获取锁,NX 表示互斥,EX 表示设置过期时间(秒)
    SET lock_key thread_id NX EX 10

    为什么要原子性? 早期实现常分两步走:先 setnx 加锁,再 expire 设置超时。若在执行完第一步后服务宕机,锁将永远存在(死锁)。因此,必须将“设置值”与“设置过期时间”合并为一条原子命令。

    • **返回 1 **:获取锁成功。
    • 返回 0(nil):获取锁失败(锁已被占用)。

    2. 释放锁

    释放锁相对简单,直接删除 Key 即可:

    DEL lock_key

    注意:生产环境中需校验锁的持有者是否为当前线程,防止误删其他线程的锁(下文将详细展开)。


    三、项目实战:手写分布式锁组件

    在之前的 Redis操作中的项目中,我们处理并发通常依赖数据库乐观锁或简单的同步块。但在高并发秒杀场景下(项目中有可以通过积分兑换话费并且限量),我们需要更轻量级的锁。

    下面我将展示如何在 Spring Boot 项目中,封装一个标准的分布式锁组件。

    1. 定义标准接口 ILock

    为了规范锁的行为,我们定义统一的接口,包含“尝试加锁”和“释放锁”两个核心方法。

    package com.hmdp.utils;

    public interface ILock {
    /**
    * 尝试获取锁
    * @param timeoutSec 锁持有的超时时间(秒)
    * @return true-获取成功,false-获取失败
    */

    boolean tryLock(long timeoutSec);

    /**
    * 释放锁
    */

    void unlock();
    }

    2. 实现 SimpleRedisLock (初级版)

    基于 StringRedisTemplate 实现上述接口。核心逻辑利用 setIfAbsent 方法对应 Redis 的 SET … NX EX 命令。

    package com.hmdp.utils;

    import org.springframework.data.redis.core.StringRedisTemplate;
    import java.util.concurrent.TimeUnit;

    public class SimpleRedisLock implements ILock {

    private final String name; // 锁的名称(业务标识)
    private final StringRedisTemplate stringRedisTemplate;

    // 锁的前缀,避免与其他 Key 冲突
    private static final String LOCK_PREFIX = "lock:";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    this.name = name;
    this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
    // 获取当前线程 ID 作为锁的值,方便排查问题
    long threadId = Thread.currentThread().getId();

    // 核心:原子性设置锁,并指定过期时间
    Boolean success = stringRedisTemplate.opsForValue()
    .setIfAbsent(LOCK_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);

    // 防止 Boolean 对象为 null 导致空指针
    return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
    // 直接删除 Key 释放锁
    stringRedisTemplate.delete(LOCK_PREFIX + name);
    }
    }

    3. 业务场景集成

    假设我们有一个一人一单的秒杀场景,传统做法是在 Service 层加 synchronized(userId)。但在集群环境下,这无效。现在使用我们封装的 SimpleRedisLock 进行改造:

    // 1. 构建锁实例,锁的粒度为"order:userId",确保同一用户只能持有一把锁
    Long userId = UserHolder.getUser().getId();
    SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);

    // 2. 尝试获取锁,设置超时时间为 10 秒
    boolean isLock = lock.tryLock(10);

    if (!isLock) {
    // 获取锁失败,说明有重复请求或正在处理中
    return Result.fail("请勿重复下单");
    }

    try {
    // 3. 执行业务逻辑
    // 注意:此处需获取代理对象以支持事务 (@Transactional) 生效
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
    return proxy.createVoucherOrder(voucherId);
    } catch (IllegalStateException e) {
    throw new RuntimeException(e);
    } finally {
    // 4. 无论成功与否,必须在 finally 中释放锁
    lock.unlock();
    }


    四、核心痛点:锁的误删问题与解决方案

    1. 问题复现

    上述初级版本存在一个严重隐患:锁误删。

    场景推演:

  • 线程 A 获取锁,设置超时时间 10s。
  • 线程 A 业务执行耗时 15s(超过锁超时时间)。
  • 第 10s 时,锁自动释放。
  • 线程 B 立即获取到同一把锁。
  • 第 15s 时,线程 A 执行完毕,调用 unlock() 删除了锁。
  • 结果:线程 A 误删了线程 B 持有的锁,导致并发安全问题。
  • 2. 解决方案:唯一标识校验

    为了解决这个问题,我们需要在释放锁之前,判断当前锁的持有者是否是自己。

    改进思路:

    • 加锁时:Value 不再仅存线程 ID,而是存 UUID + 线程ID,确保全局唯一性。
    • 解锁时:先查询 Redis 中的 Value,与当前线程的标识比对,一致才删除。 在这里插入图片描述

    3. 代码升级

    package com.hmdp.utils;

    import org.springframework.data.redis.core.StringRedisTemplate;
    import java.util.UUID;
    import java.util.concurrent.TimeUnit;

    public class SimpleRedisLock implements ILock {

    private final String name;
    // 使用 UUID + 线程ID 作为唯一标识,防止不同 JVM 中线程 ID 重复
    private static final String ID_PREFIX = UUID.randomUUID().toString() + "-";
    private static final String KEY_PREFIX = "lock:";
    private final StringRedisTemplate stringRedisTemplate;

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
    this.name = name;
    this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
    // 生成唯一线程标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();

    Boolean success = stringRedisTemplate.opsForValue()
    .setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);

    return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
    // 1. 获取当前线程的唯一标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();

    // 2. 获取 Redis 中锁的标识
    String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);

    // 3. 比对标识,一致则删除
    if (threadId.equals(id)) {
    stringRedisTemplate.delete(KEY_PREFIX + name);
    }
    }
    }

    注意:上述“查询 + 删除”的操作并非原子性。在高并发极端场景下,仍可能出现判断成功后、删除前锁过期的情况。生产级解决方案通常需要使用 Lua 脚本 将判断与删除合并为原子操作,这也是接下来进阶学习的方向。


    五、总结与展望

    通过上述实现,我们成功将 JVM 锁升级为Redis 分布式锁,并解决了基础的误删问题。

    核心收获:

    • 原子性加锁:利用 SET … NX EX 杜绝死锁。
    • 唯一标识:引入 UUID 防止误删他人锁。
    • 灵活封装:通过接口抽象,业务代码无侵入。

    待优化点(进阶思考): 目前的实现仍存在非原子性删除的隐患,且不支持锁的可重入。在后续的博文中,我将进一步:

  • 引入 Lua 脚本 保证解锁的原子性。
  • 探索 Redisson 框架源码,看它如何实现可重入锁与看门狗机制(WatchDog)。
  • 分析 RedLock 算法在 Redis 集群模式下的可靠性。
  • 技术之路无止境,从 CRUD 到中间件原理,每一步都算数。


    作者简介:大二在读,目前正通过自研项目(非遗文化+AI)与开源社区积累经验。专注技术使用与深度。欢迎关注我,一起用代码打破边界。

    相关专栏:

    • Spring Boot 系列总结
    • Redis 实战进阶

    (如果觉得文章对你有帮助,欢迎点赞👍、收藏⭐️,你的支持是我持续更新的动力!)

    赞(0)
    未经允许不得转载:171主机测评 » Redis分布式锁灵活运用:原理与误删问题解决
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址