

🔥个人主页:北极的代码(欢迎来访)
🎬作者简介:java后端学习者
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
上一章节我们解决了一人一单的问题(单机版),也就是多线程的并发的问题,我们利用的是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里的锁。

分布式锁:
首先:不管什么锁(单机锁、分布式锁),核心目的都一样:控制多个执行者,对同一共享资源的互斥访问。
分布式锁的核心思想 :需要一个所有服务器都能看到、都能访问的"公共停车场",把锁放在那里。
三个关键特征:
互斥:同一时刻,只有一台服务器的线程能拿到锁。
可见:所有服务器都能访问到这个锁的状态。
可靠:锁要稳定,不会自己消失(或者有合理的过期)。

常见实现方式:
| 实现方式 | 类比 |
|---|---|
| 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会怎样
javajava // 错误写法 public boolean tryLock(Long timeoutSec) { Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(...); return success; // ⚠️ 如果 success = null,自动拆箱会报 NPE }自动拆箱过程:
success是Boolean对象类型方法返回值是
boolean基本类型Java 会自动执行
success.booleanValue()来拆箱如果
success == null,调用null.booleanValue()→ NullPointerException正确写法
javajava return Boolean.TRUE.equals(success);这行代码做了什么:
Boolean.TRUE 是常量,永远不会为 null
equals() 方法参数允许为 null,会安全返回 false
最后返回的是 boolean 基本类型(不会拆箱)
各种情况下的返回值:
success 的值 Boolean.TRUE.equals(success) 结果 truetrue✅ 获取到锁 falsefalse❌ 没获取到锁 nullfalse❌ 没获取到锁(安全)
注意点2:为什么不用注入方式
原因1:name 是动态变化的
javajava // 如果使用注入,name 无法动态设置 @Component public class SimpleRedisLock { @Autowired private StringRedisTemplate stringRedisTemplate; @Value("${lock.name}") // ❌ name 是固定的配置值,无法改变 private String name; }实际使用场景:
javajava // 每个锁对象需要不同的业务名称 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管理
javaSimpleRedisLock 的使用方式: 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里的锁类比理解
想象你要用智能门锁:
javajava // 这行代码相当于: SmartLock lock = new SmartLock("房间A", redis连接器); // 并不是: // 1. 不是在房间A上安装锁(Redis里还没有锁) // 2. 只是创建了一个"遥控器"对象
这个对象本身 = 遥控器
Redis里的锁 = 真正的门锁
创建完对象后,你还要按按钮才能锁门:
javajava lock.tryLock(10L); // 这才是真正去Redis里创建锁代码执行步骤分解
javajava // 第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()真正执行操作 按遥控器的"锁门"按钮 好处:
- 可以多次使用同一个锁对象
javajava SimpleRedisLock lock = new SimpleRedisLock("order:123", redisTemplate); lock.tryLock(10L); // 第一次加锁 // 业务操作... lock.unlock(); // 释放锁 // 同一把锁可以重复使用 lock.tryLock(5L); // 第二次加锁
- 可以临时创建,用完就扔
javajava // 更常见的使用方式:用一次就扔掉 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:不释放锁
javajava // ❌ 错误 boolean isLock = lock.tryLock(1200L); if (!isLock) return Result.fail("..."); // 执行业务 return proxy.createVoucherOrder(voucherId); // 忘记调用 unlock()后果:锁永远不会被释放 → 其他请求永远拿不到锁 → 死锁
错误写法2:在 finally 外面释放
javajava // ❌ 错误 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();
}
结语:如果对你有帮助,请点赞,关注,收藏,你的支持就是我最大的鼓励!