【Redis实战篇】初步基于Redis实现的分布式锁---基于黑马点评

🔥个人主页:北极的代码(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!

前言:

上一章节我们解决了一人一单的问题(单机版),也就是多线程的并发的问题,我们利用的是java中自带的锁机制synchronized,然而这个锁机制还是有缺陷的,下面我们具体看看,以及解决的方案。

摘要:

本文探讨了分布式系统中单机锁的局限性及解决方案。在集群环境下,传统synchronized锁失效,因为不同JVM的锁监视器相互独立。

提出使用Redis实现分布式锁,通过SETNX命令确保多服务器间互斥访问共享资源。

文章详细分析了锁自动拆箱的NPE风险、动态锁名的实现方式,以及try-finally确保锁释放的重要性。

最后展示了如何在订单业务中应用Redis分布式锁,强调正确释放锁避免死锁的关键性。该方案有效解决了集群环境下的并发安全问题,为分布式系统开发提供了实用指导。

分析问题:

什么是集群

集群 就是把同一套代码 ,同时运行在多台服务器(或多台虚拟机/容器)上。

  • 单机:只有一台服务器,比如一个Tomcat。

  • 集群:有多台服务器(Tomcat1、Tomcat2......),前面用Nginx等做负载均衡,把用户请求分发给不同服务器。

为什么要用集群
  • 高并发:一台服务器撑不住,多台分担压力。

  • 高可用:一台挂了,其他还能服务。

关键点:集群中的每台服务器都有自己独立的内存(JVM堆、方法区等)。

集群模式下的新问题

现在把上面这段代码部署到集群:

  • 服务器A:处理用户1的请求1

  • 服务器B:处理用户1的请求2

问题出在哪里
synchronized(obj) 只对同一个JVM进程有效。服务器A的锁和服务器B的锁毫无关系。

单体中一个JVM只有一个锁监视器,所以只会有一个线程获取锁,可以实现线程间的互斥。

而多台服务器有多个JVM。有多个锁监视器,所以就有多个线程同时进行,线程就不互斥了

两个请求同时通过"查询订单"这一步,各自都认为没有订单,然后各自去插入------一人多单又出现了。

这就是分布式/集群环境下的并发安全问题单机的本地锁(JVM锁)失效了

解决方案:

我们的目的就是尽管在多个服务器中,我们也要保持只有一个锁监视器,这样才能避免并发。

核心思路 :让所有服务器竞争同一个外部资源(Redis),而不是各自用自己JVM里的锁。

分布式锁:

首先:不管什么锁(单机锁、分布式锁),核心目的都一样:控制多个执行者,对同一共享资源的互斥访问

分布式锁的核心思想 :需要一个所有服务器都能看到、都能访问的"公共停车场",把锁放在那里。

三个关键特征

  1. 互斥:同一时刻,只有一台服务器的线程能拿到锁。

  2. 可见:所有服务器都能访问到这个锁的状态。

  3. 可靠:锁要稳定,不会自己消失(或者有合理的过期)。

常见实现方式
实现方式 类比
Redis(最常用) 一个所有人都能看到的公告板,谁在上面写了自己的名字,谁就拥有锁
Zookeeper 一个自动排队的叫号系统
数据库唯一索引 一个特殊的花名册,只能有一个人登记成功
特性 MySQL Redis ZooKeeper
互斥 利用MySQL本身的互斥锁机制 利用SETNX等互斥命令 利用节点的唯一性和有序性实现互斥
高可用
高性能 一般 一般
安全性 断开连接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开连接自动释放

基于Redis的分布式锁:

整体思路:


首先要先获取锁:

这里还是有很多细节的,我们的这个锁的key不能直接固定 ,不同的业务要有不同的锁,然后就是锁的值,要是当前线程

关于获取锁之后的返回值,我们图上的是返回ok和nil,这是因为Spring在帮我们封装函数的时候帮我们对结果做了判断,关于我们返回的结果,也有值得注意的。

注意点1:关于自动拆箱

stringRedisTemplate.opsForValue().setIfAbsent() 的返回值类型是 Boolean包装类 ,不是基本类型 boolean)。

在 Spring Data Redis 中,这个方法的返回值可能是:

  • true - 设置成功(获取到锁)

  • false - 设置失败(锁已被占用)

  • null - 操作异常或超时

如果直接 return success 会怎样

java 复制代码
java

// 错误写法
public boolean tryLock(Long timeoutSec) {
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(...);
    return success;  // ⚠️ 如果 success = null,自动拆箱会报 NPE
}

自动拆箱过程

  • successBoolean 对象类型

  • 方法返回值是 boolean 基本类型

  • Java 会自动执行 success.booleanValue() 来拆箱

  • 如果 success == null,调用 null.booleanValue()NullPointerException

正确写法

java 复制代码
java

return Boolean.TRUE.equals(success);

这行代码做了什么:

  1. Boolean.TRUE 是常量,永远不会为 null

  2. equals() 方法参数允许为 null,会安全返回 false

  3. 最后返回的是 boolean 基本类型(不会拆箱)

各种情况下的返回值

success 的值 Boolean.TRUE.equals(success) 结果
true true ✅ 获取到锁
false false ❌ 没获取到锁
null false ❌ 没获取到锁(安全)
注意点2:为什么不用注入方式

原因1:name 是动态变化的

java 复制代码
java

// 如果使用注入,name 无法动态设置
@Component
public class SimpleRedisLock {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    @Value("${lock.name}")  // ❌ name 是固定的配置值,无法改变
    private String name;
}

实际使用场景

java 复制代码
java

// 每个锁对象需要不同的业务名称
SimpleRedisLock orderLock = new SimpleRedisLock("order:user:123", stringRedisTemplate);
SimpleRedisLock stockLock = new SimpleRedisLock("stock:goods:456", stringRedisTemplate);
SimpleRedisLock couponLock = new SimpleRedisLock("coupon:100", stringRedisTemplate);

每次创建锁,name 都不同,无法通过注入方式预先设置。

原因2:不是所有对象都需要Spring管理

java 复制代码
SimpleRedisLock 的使用方式:

java

// 使用方(Service中)
@Service
public class OrderService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    public void createOrder(Long userId) {
        // 临时创建锁对象,用完即扔
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        try {
            if (lock.tryLock(10L)) {
                // 执行下单逻辑
            }
        } finally {
            lock.unlock();
        }
    }
}

这个锁对象是短生命周期的,每次请求都可能创建新对象,不需要交给Spring容器管理。

拓展:什么时候用注入,什么时候用构造方法
场景 使用方式 示例
固定依赖,对象需要Spring管理 依赖注入(@Autowired) Service、Repository、Configuration
动态参数,每次创建都不同 构造方法传参 锁的key、分页参数、临时对象
既有固定依赖,又有动态参数 混合:固定依赖用注入,动态用构造 看下面的优化方案
java 复制代码
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }
    private static final String KEY_PREFIX = "lock:";
    public boolean tryLock(Long timeoutSec) {
        //设置value时,我们要知道是哪个线程的值
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);

        return Boolean.TRUE.equals(success);
    }
释放锁:
java 复制代码
    /**
     * 释放锁
     */
    public void unlock() {
       stringRedisTemplate.delete(KEY_PREFIX+name);
    }

实际业务代码实现:

在这里我们修改了前面用的synchronized锁机制,用了Redis的分布式锁,我们在这里尝试创建锁对象,那这里的锁对象是什么呢:

1.创建锁对象

java 复制代码
java

SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);

白话翻译创建一个锁工具对象,用来操作Redis中的锁

不是 :去Redis里创建锁
而是:在Java内存中new一个对象,这个对象知道怎么去操作Redis里的锁

类比理解

想象你要用智能门锁

java 复制代码
java

// 这行代码相当于:
SmartLock lock = new SmartLock("房间A", redis连接器);

// 并不是:
// 1. 不是在房间A上安装锁(Redis里还没有锁)
// 2. 只是创建了一个"遥控器"对象
  • 这个对象本身 = 遥控器

  • Redis里的锁 = 真正的门锁

创建完对象后,你还要按按钮才能锁门:

java 复制代码
java

lock.tryLock(10L);  // 这才是真正去Redis里创建锁
代码执行步骤分解
java 复制代码
java

// 第1步:new一个锁对象(只在Java内存中)
SimpleRedisLock lock = new SimpleRedisLock("order:123", stringRedisTemplate);

// 此时:
// - lock对象.name = "order:123"
// - lock对象.stringRedisTemplate = 已配置好的模板
// - Redis服务器:完全没变化,没有任何锁

// 第2步:调用tryLock,才真正去Redis操作
boolean isLock = lock.tryLock(10L);

// 此时Redis里才会出现:
// key: "lock:order:123"
// value: "线程ID"
// 过期时间: 10秒
设计思想:对象构建行为执行 分离
步骤 做什么 类比
new SimpleRedisLock(...) 准备工具,设置参数 拿到遥控器,设置好房间号
lock.tryLock() 真正执行操作 按遥控器的"锁门"按钮

好处

  1. 可以多次使用同一个锁对象
java 复制代码
java

SimpleRedisLock lock = new SimpleRedisLock("order:123", redisTemplate);

lock.tryLock(10L);   // 第一次加锁
// 业务操作...
lock.unlock();       // 释放锁

// 同一把锁可以重复使用
lock.tryLock(5L);    // 第二次加锁
  1. 可以临时创建,用完就扔
java 复制代码
java

// 更常见的使用方式:用一次就扔掉
new SimpleRedisLock("order:" + userId, stringRedisTemplate).tryLock(10L);

2.关于这里的if判断

我们总是优先考虑错误,去排除,减少嵌套的层级。

先把失败情况(没拿到锁)处理掉并返回,后面的主逻辑就不用再嵌套在 if 块里了,代码更扁平、更清晰。

复制代码
开始
  ↓
尝试获取锁 → isLock = false 还是 true?
  ↓
isLock = false? 
  ↓ 是
  └─→ 返回"不允许重复下单" 【结束】
  ↓ 否(isLock = true)
继续执行主逻辑...
  ↓
返回结果 【结束】

3.try-finally 的核心作用

java 复制代码
java

try {
    // 需要保护的业务代码
    return proxy.createVoucherOrder(voucherId);
} finally {
    // 无论上面是否发生异常,这里的代码都会执行
    lock.unlock();  // 释放锁
}

一句话总结try-finally 确保 unlock() 一定会被执行,防止死锁。

如果不写 try-finally 会怎样
错误写法1:不释放锁
java 复制代码
java

// ❌ 错误
boolean isLock = lock.tryLock(1200L);
if (!isLock) return Result.fail("...");

// 执行业务
return proxy.createVoucherOrder(voucherId);
// 忘记调用 unlock()

后果:锁永远不会被释放 → 其他请求永远拿不到锁 → 死锁

错误写法2:在 finally 外面释放
java 复制代码
java

// ❌ 错误
boolean isLock = lock.tryLock(1200L);
try {
    return proxy.createVoucherOrder(voucherId);
} catch (Exception e) {
    throw e;
}
lock.unlock();  // 如果上面 return 了,这行永远不会执行!

后果 :如果 createVoucherOrder 正常返回,unlock() 不会执行 → 锁不会被释放

java 复制代码
  //创建订单的逻辑
        Long userId=UserHolder.getUser().getId();
        //尝试创建锁对象
        SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        //获取锁
        boolean isLock = lock.tryLock(1200L);
        //判断锁获取是否成功
        if (!isLock){
            //获取失败
            return Result.fail("不允许重复下单");
        }


        try {
            //获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy. createVoucherOrder(voucherId);
        }  finally {
            //释放锁
            lock.unlock();
        }

结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!

相关推荐
健康平安的活着2 小时前
mysql中left join 不一定比 in效率高案例
数据库·mysql
呱牛do it6 小时前
企业级门户网站设计与实现:基于SpringBoot + Vue3的全栈解决方案(Day 3)
java·vue
神の愛7 小时前
左连接查询数据 left join
java·服务器·前端
南境十里·墨染春水8 小时前
linux学习进展 线程同步——互斥锁
java·linux·学习
雨奔8 小时前
Kubernetes 联邦 Deployment 指南:跨集群统一管理 Pod
java·容器·kubernetes
杨凯凡8 小时前
【021】反射与注解:Spring 里背后的影子
java·后端·spring
IT摆渡者8 小时前
MySQL性能巡检脚本分析报告
数据库·mysql
lulu12165440788 小时前
Claude Code项目大了响应慢怎么办?Subagents、Agent Teams、Git Worktree、工作流编排四种方案深度解析
java·人工智能·python·ai编程
buhuimaren_8 小时前
FastDFS分布式存储
分布式