当前位置: 首页 > news >正文

Spring Boot 实战 Redis 分布式锁:从原理到高并发落地

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

在单体应用中,我们用 synchronizedReentrantLock 就能解决并发问题(比如库存扣减、订单幂等创建)。但当应用部署在多台服务器(分布式架构)时,本地锁只能控制单台机器的线程,无法跨服务同步,会导致超卖、重复操作等严重问题。

比如库存扣减场景,3台服务器同时处理请求,本地锁失效,最终库存变成负数

Redis 分布式锁凭借高性能、高可用、易实现的特点,成为分布式场景下的主流选择,核心优势:

  • 基于内存操作,响应速度快(毫秒级)
  • 支持分布式部署,可横向扩展
  • 自带过期机制,避免死锁

二、Redis 分布式锁核心原理

2.1 核心命令:SET NX EX

Redis 分布式锁的核心是通过 原子命令 保证“加锁”操作的唯一性,推荐使用 SET 命令的扩展参数:

SET lock_key unique_value NX EX 30

各参数含义:

  • lock_key:锁的唯一标识(如 stock:lock:1001,1001 为商品ID)
  • unique_value:锁的唯一值(必须全局唯一,用于释放锁时判断“是否是自己的锁”,避免误删)
  • NX(Not Exist):仅当 lock_key 不存在时才创建,保证同一时间只有一个线程加锁
  • EX(Expire):设置锁的过期时间(如 30 秒),避免业务异常导致锁无法释放(死锁)

2.2 关键注意点

  1. 原子性:加锁必须是“判断不存在 + 设置值 + 设过期”三步合一的原子操作,否则会出现并发安全问题(比如先判断再设置,中间被其他线程插队)。
  2. 唯一标识:释放锁时必须先判断“当前锁的 value 是否是自己加锁时的 value”,再删除,这两步也需要原子性(用 Lua 脚本实现)。
  3. 过期时间:过期时间要大于业务执行时间,避免业务没跑完锁就过期,导致并发问题;若业务时间不确定,需引入“看门狗”机制自动续期。

三、Spring Boot 实战 Redis 分布式锁

3.1 环境准备

1. 引入依赖

创建 Spring Boot 项目(推荐 2.7.x 版本),在 pom.xml 中添加 Redis 依赖:

<!-- Spring Data Redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Commons Pool2(Lettuce 连接池) -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
<!-- Lombok(简化代码) -->
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional>
</dependency>
2. 配置 Redis

application.yml 中配置 Redis 连接信息(Lettuce 是 Spring Boot 默认客户端,性能优于 Jedis):

spring:redis:# Redis 地址host: localhost# 端口port: 6379# 密码(若未设置则省略)password: 123456# 数据库索引(默认 0)database: 0# 连接超时时间timeout: 3000ms# Lettuce 连接池配置lettuce:pool:# 最大活跃连接数max-active: 8# 最大空闲连接数max-idle: 8# 最小空闲连接数min-idle: 2# 连接池最大阻塞等待时间max-wait: -1ms
3. 配置 RedisTemplate

Spring Boot 自动配置的 RedisTemplate 默认用 JDK 序列化,可读性差,我们自定义配置,使用 JSON 序列化:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(factory);// 字符串序列化器(key 用 String 序列化)StringRedisSerializer stringSerializer = new StringRedisSerializer();template.setKeySerializer(stringSerializer);template.setHashKeySerializer(stringSerializer);// JSON 序列化器(value 用 JSON 序列化)GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();template.setValueSerializer(jsonSerializer);template.setHashValueSerializer(jsonSerializer);template.afterPropertiesSet();return template;}
}

3.2 手写 Redis 分布式锁工具类

核心实现“加锁”“释放锁”“续期”三个核心方法,重点保证原子性:

import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Component;import java.util.Collections;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;/*** Redis 分布式锁工具类(实现 Lock 接口,符合 Java 锁规范)*/
@Component
@RequiredArgsConstructor
public class RedisDistributedLock implements Lock {private final RedisTemplate<String, Object> redisTemplate;// 锁的默认过期时间(30秒)private static final long DEFAULT_EXPIRE = 30;// 锁的唯一标识前缀(UUID 保证全局唯一)private final ThreadLocal<String> lockValue = new ThreadLocal<>();/*** 加锁(阻塞式,直到获取锁成功)*/@Overridepublic void lock() {// 循环加锁,直到成功while (!tryLock()) {// 避免频繁自旋,让出 CPUtry {TimeUnit.MILLISECONDS.sleep(50);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}/*** 尝试加锁(非阻塞式,获取成功返回 true,失败返回 false)* @return 是否加锁成功*/@Overridepublic boolean tryLock() {return tryLock(DEFAULT_EXPIRE, TimeUnit.SECONDS);}/*** 尝试加锁(带过期时间)* @param time 过期时间* @param unit 时间单位* @return 是否加锁成功*/@Overridepublic boolean tryLock(long time, TimeUnit unit) {// 1. 生成全局唯一的锁值(UUID + 线程ID,避免同一JVM内线程冲突)String value = UUID.randomUUID().toString() + ":" + Thread.currentThread().getId();// 2. 转换过期时间为秒(Redis EX 命令单位是秒)long expireSeconds = unit.toSeconds(time);// 3. 执行 Redis SET NX EX 命令(原子操作)Boolean success = redisTemplate.opsForValue().setIfAbsent(getLockKey(),  // 锁的 keyvalue,         // 锁的 valueexpireSeconds, // 过期时间TimeUnit.SECONDS);// 4. 加锁成功,保存锁值到 ThreadLocalif (Boolean.TRUE.equals(success)) {lockValue.set(value);return true;}return false;}/*** 释放锁(必须保证原子性:判断 value + 删除 key)*/@Overridepublic void unlock() {// 1. Lua 脚本:判断锁的 value 是否是当前线程的,若是则删除(原子操作)String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('del', KEYS[1]) " +"else " +"return 0 " +"end";// 2. 执行 Lua 脚本DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);Long result = redisTemplate.execute(script,Collections.singletonList(getLockKey()), // KEYS[1]:锁的 keylockValue.get()                          // ARGV[1]:当前线程的锁值);// 3. 释放 ThreadLocal 资源lockValue.remove();// 4. 若 result == 0,说明锁已过期或被其他线程占用,可打印警告if (result == 0) {System.err.println("释放锁失败:锁已过期或被其他线程占用");}}/*** 续期锁(看门狗机制核心方法,业务执行超时前调用)* @param expireSeconds 续期时间(秒)* @return 是否续期成功*/public boolean renewLock(long expireSeconds) {// Lua 脚本:判断锁的 value 是当前线程的,才续期(原子操作)String luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then " +"return redis.call('expire', KEYS[1], ARGV[2]) " +"else " +"return 0 " +"end";DefaultRedisScript<Long> script = new DefaultRedisScript<>(luaScript, Long.class);Long result = redisTemplate.execute(script,Collections.singletonList(getLockKey()),lockValue.get(),String.valueOf(expireSeconds));return result == 1;}// ------------------------------ 以下方法暂不实现(按需扩展) ------------------------------@Overridepublic void lockInterruptibly() throws InterruptedException {throw new UnsupportedOperationException("暂不支持可中断锁");}@Overridepublic Condition newCondition() {throw new UnsupportedOperationException("暂不支持 Condition");}/*** 生成锁的 key(可根据业务自定义,比如加商品ID、用户ID)* 示例:stock:lock:1001(1001 为商品ID)*/private String getLockKey() {// 实际项目中可通过参数传入,这里简化为固定值,需根据业务调整return "stock:lock:1001";}
}

3.3 实战场景:库存扣减(高并发测试)

我们用“商品库存扣减”这个经典场景,测试分布式锁的有效性。

1. 库存服务实现
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
@Slf4j
@RequiredArgsConstructor
public class StockService {private final RedisTemplate<String, Object> redisTemplate;private final RedisDistributedLock redisLock;// 库存 key(商品ID:1001)private static final String STOCK_KEY = "stock:1001";// 初始库存(100件)private static final int INIT_STOCK = 100;/*** 初始化库存(项目启动时执行)*/public void initStock() {redisTemplate.opsForValue().set(STOCK_KEY, INIT_STOCK);log.info("初始化库存成功,初始库存:{}", INIT_STOCK);}/*** 扣减库存(无锁版本,高并发下会超卖)*/public boolean deductStockWithoutLock() {// 1. 获取当前库存Integer stock = (Integer) redisTemplate.opsForValue().get(STOCK_KEY);if (stock == null || stock <= 0) {log.error("库存不足,当前库存:{}", stock);return false;}// 2. 扣减库存(非原子操作,高并发下会超卖)int newStock = stock - 1;redisTemplate.opsForValue().set(STOCK_KEY, newStock);log.info("扣减库存成功,当前库存:{}", newStock);return true;}/*** 扣减库存(有锁版本,避免超卖)*/public boolean deductStockWithLock() {try {// 1. 加锁(阻塞式,直到获取锁)redisLock.lock();// 2. 业务逻辑:扣减库存Integer stock = (Integer) redisTemplate.opsForValue().get(STOCK_KEY);if (stock == null || stock <= 0) {log.error("库存不足,当前库存:{}", stock);return false;}// 3. 扣减库存(此时只有一个线程执行,安全)int newStock = stock - 1;redisTemplate.opsForValue().set(STOCK_KEY, newStock);log.info("扣减库存成功,当前库存:{}", newStock);return true;} finally {// 4. 释放锁(必须在 finally 中,保证业务异常时也能释放)redisLock.unlock();}}
}
2. 接口暴露(用于测试)
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/stock")
@RequiredArgsConstructor
public class StockController {private final StockService stockService;/*** 初始化库存*/@GetMapping("/init")public String initStock() {stockService.initStock();return "库存初始化成功";}/*** 无锁扣减库存*/@GetMapping("/deduct/without-lock")public String deductWithoutLock() {boolean success = stockService.deductStockWithoutLock();return success ? "库存扣减成功" : "库存不足";}/*** 有锁扣减库存*/@GetMapping("/deduct/with-lock")public String deductWithLock() {boolean success = stockService.deductStockWithLock();return success ? "库存扣减成功" : "库存不足";}
}
3. 高并发测试(JMeter)
  1. 测试准备

    • 调用 http://localhost:8080/stock/init 初始化库存(100件)。
    • 用 JMeter 创建线程组:1000 个线程,循环 1 次(模拟 1000 个并发请求)。
  2. 测试无锁版本

    • 请求地址:http://localhost:8080/stock/deduct/without-lock
    • 结果:库存会变成负数(比如 -23),出现超卖,因为 1000 个请求同时扣减 100 个库存。
  3. 测试有锁版本

    • 请求地址:http://localhost:8080/stock/deduct/with-lock
    • 结果:库存最终为 0,无超卖,所有请求有序执行,分布式锁生效。

四、进阶优化:解决锁的“过期”与“集群”问题

4.1 问题1:锁过期导致业务未完成

若业务执行时间超过锁的过期时间(比如锁设 30 秒,但业务需要 60 秒),锁会自动释放,其他线程会加锁,导致并发问题。

解决方案:看门狗机制
在加锁成功后,启动一个后台线程,每隔 expire/3 秒(比如 10 秒)调用 renewLock() 续期锁,直到业务执行完成。

优化后的 lock() 方法:

@Override
public void lock() {// 1. 加锁while (!tryLock()) {try {TimeUnit.MILLISECONDS.sleep(50);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}// 2. 启动看门狗线程(续期)startWatchDog();
}/*** 看门狗线程:每隔 expire/3 秒续期一次*/
private void startWatchDog() {// 续期间隔(锁过期时间的 1/3)long renewInterval = DEFAULT_EXPIRE / 3;// 后台线程续期Thread watchDog = new Thread(() -> {while (lockValue.get() != null) { // 锁未释放时续期try {TimeUnit.SECONDS.sleep(renewInterval);// 续期锁boolean success = renewLock(DEFAULT_EXPIRE);if (!success) {log.warn("看门狗续期失败,锁可能已过期");break;}log.debug("看门狗续期成功,锁过期时间延长 {} 秒", DEFAULT_EXPIRE);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}});// 设为守护线程(主线程结束后自动退出)watchDog.setDaemon(true);watchDog.start();
}

4.2 问题2:Redis 集群下的“脑裂”问题

若 Redis 是主从集群,主节点加锁成功后,未同步到从节点就宕机,从节点升级为主节点,其他线程会重新加锁,导致“双锁”问题。

解决方案:使用 Redisson
Redisson 是 Redis 官方推荐的分布式锁实现,封装了“红锁”(RedLock)机制,通过多个 Redis 节点加锁,保证集群下的一致性,同时支持:

  • 可重入锁(Reentrant Lock)
  • 公平锁(Fair Lock)
  • 自动看门狗续期
  • 读写锁(ReadWrite Lock)
Redisson 集成步骤
  1. 引入依赖
<dependency><groupId>org.redisson</groupId><artifactId>redisson-spring-boot-starter</artifactId><version>3.23.3</version> <!-- 与 Redis 版本兼容,参考官方文档 -->
</dependency>
  1. 配置 Redisson
spring:redis:host: localhostport: 6379password: 123456
redisson:# 单节点配置(集群/哨兵配置参考官方文档)singleServerConfig:address: redis://localhost:6379password: 123456# 连接池大小connectionPoolSize: 10# 连接超时时间connectTimeout: 3000
  1. 使用 Redisson 锁
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Service;import java.util.concurrent.TimeUnit;@Service
public class RedissonStockService {private final RedissonClient redissonClient;private static final String STOCK_KEY = "stock:1001";private static final String LOCK_KEY = "stock:lock:1001";// 构造函数注入 RedissonClientpublic RedissonStockService(RedissonClient redissonClient) {this.redissonClient = redissonClient;}/*** Redisson 锁扣减库存(自动续期,支持可重入)*/public boolean deductStock() {// 1. 获取锁RLock lock = redissonClient.getLock(LOCK_KEY);try {// 2. 加锁(30秒过期,Redisson 自动续期)lock.lock(30, TimeUnit.SECONDS);// 3. 扣减库存(业务逻辑同上)Integer stock = (Integer) redisTemplate.opsForValue().get(STOCK_KEY);if (stock == null || stock <= 0) {return false;}redisTemplate.opsForValue().set(STOCK_KEY, stock - 1);return true;} finally {// 4. 释放锁(仅当前线程加的锁能释放)if (lock.isHeldByCurrentThread()) {lock.unlock();}}}
}

五、常见问题与解决方案

问题场景原因解决方案
锁无法释放(死锁)业务异常导致 unlock() 未执行1. 锁必须设过期时间;2. unlock() 放在 finally
释放别人的锁未判断锁的唯一标识,直接删除释放锁用 Lua 脚本,先判断 value 再删除
锁过期导致并发业务执行时间超过锁过期时间1. 预估合理过期时间;2. 引入看门狗自动续期
集群下双锁问题Redis 主从同步延迟,主节点宕机使用 Redisson 红锁机制,多节点加锁
高并发下性能差锁竞争激烈,线程频繁自旋1. 减小锁粒度(如按商品ID加锁);2. 用公平锁避免饥饿;3. 引入队列削峰

六、总结与最佳实践

  1. 原理优先:先理解 Redis 分布式锁的核心(SET NX EX + Lua 脚本),再使用工具类或 Redisson。
  2. 生产环境推荐 Redisson:原生实现适合学习,生产环境用 Redisson,避免重复造轮子,且支持更多高级特性。
  3. 锁粒度要小:避免用全局锁(如 stock:lock),改用细粒度锁(如 stock:lock:1001),提高并发效率。
  4. 过期时间要合理:既不能太短(导致续期频繁),也不能太长(死锁时影响范围大),建议 10-60 秒。
  5. 监控不可少:在生产环境中,监控锁的加锁成功率、续期次数、释放失败次数,及时发现问题。

通过本文的实战,你已经掌握了 Spring Boot 集成 Redis 分布式锁的核心流程,从原理到落地,再到高并发优化,可直接应用到实际项目中(如库存、订单、支付等场景)。

如果觉得有帮助,欢迎点赞、收藏,如有疑问,欢迎在评论区留言讨论!

http://www.dtcms.com/a/426852.html

相关文章:

  • nodejs做网站的弊端马来西亚网站后缀
  • CSDN Markdown 编辑器快捷键大全
  • 基于GNS3 web UI配置RIP协议(Wireshark 分析)
  • Helm Chart 中,SeaweedFS的 master.data.type 选择
  • 智能座舱问答
  • kube-prometheus监控服务发现
  • 攻防世界-Web-Web_python_template_injection
  • seo站内优化公司河北邯郸seo网站建设网站优化
  • wordpress网站插件优秀校园网站
  • Hibernate批量查询方法全面解析
  • 深度解析 ChatGPT 和 Claude 的记忆机制
  • 994. 腐烂的橘子,207. 课程表, 208.实现 Trie (前缀树)
  • 有趣的化学元素
  • 深圳网站建设者西安广告公司
  • READ_ONCE、smp_store_release在io_uring中实例分析
  • C/C++数据结构之用数组实现栈
  • Linux timekeeping
  • macOS 下安装 zsh、zsh-syntax-highlighting、powerlevel9k、nerd-font
  • CarveMe:代谢模型构建
  • windows显示驱动开发-调试间接显示驱动程序(二)
  • 企业平台网站建设制作一个网站平台
  • LinuxC++——etcd分布式键值存储系统入门
  • 使用arcgis提取评价指标时,导出数据是负数-9999
  • VUE3+element plus 实现表格行合并
  • LinuxC++——etcd分布式键值存储系统API(libetcd-cpp-api3)下载与二次封装
  • Electron vue项目 打包 exe文件2
  • 【开题答辩全过程】以 springboot高校创新创业课程体系的设计与实现为例,包含答辩的问题和答案
  • package.json详解
  • iOS 应用上架全流程解析,苹果应用发布步骤、ipa 上传工具、TestFlight 测试与 App Store 审核经验
  • QGIS + ArcGIS Pro 下载常见卫星影像及 ESRI Wayback 历史影像