详细介绍:后端_Redis 分布式锁实现指南

news/2025/11/6 19:49:23/文章来源:https://www.cnblogs.com/gccbuaa/p/19197594

前言

在分布式系统中,多节点并发操作共享资源时,传统单机锁(如 synchronizedReentrantLock)无法跨节点生效,Redis 分布式锁通过 Redis 的原子性操作实现跨节点互斥,成为解决分布式并发问题的核心方案。

本文基于 Redisson 和 Lock4j 框架,讲解 Redis 分布式锁的两种使用方式(编程式、声明式),并提供完整实践案例。

1、Redis 分布式锁核心原理

Redis 分布式锁的实现依赖 Redis 的原子命令过期机制,核心逻辑如下:

  1. 加锁:通过 SET key value NX EX expireTime 命令实现(NX 表示“键不存在时才设置”,确保互斥;EX 表示设置过期时间,避免死锁)。
  2. 解锁:通过 Lua 脚本原子执行“判断值是否匹配 + 删除键”(避免误删其他节点的锁),脚本逻辑为:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
  1. 防死锁:通过“过期时间”自动释放锁,即使持有锁的节点宕机,锁也会在过期后释放。
  2. 高级特性:部分框架(如 Redisson)还支持可重入锁(通过记录线程标识和重入次数实现)、红锁(多 Redis 节点加锁,提升可靠性)、读写锁(读操作共享,写操作互斥,提升并发效率)等。

2、技术选型与依赖引入

Redis 分布式锁主流实现框架有两种,需根据使用场景选择:

框架核心特点适用场景依赖坐标
Redisson支持多种锁类型(可重入、红锁、读写锁等),可靠性高复杂分布式场景(如分布式事务、高并发互斥)org.redisson:redisson-spring-boot-starter
Lock4j基于注解的声明式锁,配置简单,支持多存储(Redis/ZooKeeper)简单互斥场景(如接口防重复提交、定时任务)com.baomidou:lock4j-redisson-spring-boot-starter

3、方式一:编程式锁(基于 Redisson)

编程式锁通过 Redisson 提供的 API 手动控制锁的“加锁-业务执行-解锁”流程,灵活性高,支持复杂锁逻辑。

3.1 环境准备

1. 引入依赖

在项目 pom.xml 中添加 Redisson 依赖(若项目已集成 Spring Data Redis,无需额外配置 Redis 连接):

<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.23.3</version> <!-- 建议使用最新稳定版 -->
</dependency>
2. Redis 配置

Redisson 会自动复用 Spring Data Redis 的配置(如 spring.redis.hostspring.redis.port),无需额外配置。示例 application.yaml 配置:

spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456 # 若Redis无密码可省略
database: 0

3.2 核心 API 说明

Redisson 提供多种锁实现,常用 API 如下:

锁类型核心类适用场景
可重入锁RLock单节点多次加锁(如递归调用、嵌套业务)
公平锁RFairLock按请求顺序获取锁(避免饥饿问题)
读写锁RReadWriteLock读多写少场景(读操作共享,写操作互斥)
红锁RedissonRedLock高可靠性场景(多 Redis 节点加锁,容忍单点故障)

3.3 实战案例:支付通知防重复处理

在支付系统中,“支付通知回调”需确保同一笔订单的通知仅被处理一次(避免重复入账),可通过 Redisson 分布式锁实现。

1. 定义 Redis 锁 Key 常量

创建 RedisKeyConstants 类,统一管理锁 Key 格式(避免硬编码):

public class RedisKeyConstants {
/**
* 支付通知分布式锁 Key:PAY_NOTIFY_LOCK_{订单ID}
*/
public static final String PAY_NOTIFY_LOCK = "PAY_NOTIFY_LOCK:%s";
// 其他业务 Key...
}
2. 封装锁操作 DAO(可选)

创建 PayNotifyLockRedisDAO 类,封装 Redisson 锁的加锁、解锁逻辑,降低业务代码耦合:

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
@Component
public class PayNotifyLockRedisDAO {
private final RedissonClient redissonClient;
// 构造函数注入 RedissonClient(Spring 自动配置)
public PayNotifyLockRedisDAO(RedissonClient redissonClient) {
this.redissonClient = redissonClient;
}
/**
* 加锁:获取支付通知锁
* @param orderId 订单ID
* @param waitTime 等待锁的时间(毫秒)
* @param leaseTime 锁的持有时间(毫秒,超时自动释放)
* @return 锁对象(用于后续解锁)
*/
public RLock lock(String orderId, long waitTime, long leaseTime) {
String lockKey = String.format(RedisKeyConstants.PAY_NOTIFY_LOCK, orderId);
RLock lock = redissonClient.getLock(lockKey);
try {
// 尝试加锁:最多等待 waitTime,持有 leaseTime 后自动释放
boolean isLocked = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
if (!isLocked) {
throw new RuntimeException("获取支付通知锁失败,订单ID:" + orderId);
}
return lock;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException("加锁过程被中断,订单ID:" + orderId, e);
}
}
/**
* 解锁:手动释放锁(需确保锁是当前线程持有)
* @param lock 锁对象
*/
public void unlock(RLock lock) {
if (lock != null && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
3. 业务层使用锁

PayNotifyServiceImpl 中调用 DAO 加锁,确保同一订单的通知仅被处理一次:

import org.redisson.api.RLock;
import org.springframework.stereotype.Service;
@Service
public class PayNotifyServiceImpl implements PayNotifyService {
private final PayNotifyLockRedisDAO payNotifyLockRedisDAO;
private final OrderService orderService; // 订单业务服务
// 构造函数注入依赖
public PayNotifyServiceImpl(PayNotifyLockRedisDAO payNotifyLockRedisDAO, OrderService orderService) {
this.payNotifyLockRedisDAO = payNotifyLockRedisDAO;
this.orderService = orderService;
}
@Override
public void handlePayNotify(String orderId, String notifyData) {
RLock lock = null;
try {
// 1. 加锁:等待1秒,持有5秒(根据业务调整超时时间)
lock = payNotifyLockRedisDAO.lock(orderId, 1000, 5000);
// 2. 校验订单状态(避免重复处理)
if (orderService.isOrderPaid(orderId)) {
System.out.println("订单已处理,无需重复执行:" + orderId);
return;
}
// 3. 执行核心业务(如更新订单状态、入账等)
orderService.updateOrderStatus(orderId, "PAID");
System.out.println("支付通知处理成功,订单ID:" + orderId);
} finally {
// 4. 解锁(必须在 finally 中执行,确保锁释放)
payNotifyLockRedisDAO.unlock(lock);
}
}
}

3.4 注意事项

  1. 解锁安全性:必须通过 isHeldByCurrentThread() 校验锁持有者,避免误删其他线程的锁。
  2. 超时设置leaseTime 需大于业务执行时间(若业务耗时不确定,可使用 Redisson 的“自动续期”功能,需开启 lock.setKeepLockAlive(true))。
  3. 异常处理:加锁失败需抛出异常或返回友好提示,避免业务静默失败。

4、方式二:声明式锁(基于 Lock4j)

声明式锁通过 @Lock4j 注解简化锁操作,无需手动控制加锁/解锁,底层自动完成“注解解析→加锁→业务执行→解锁”流程,适合简单互斥场景。

4.1 环境准备

1. 引入依赖

Lock4j 需结合具体存储实现(如 Redis),在 pom.xml 中添加 Lock4j + Redisson 依赖:

<!-- Lock4j 核心依赖 --><dependency><groupId>com.baomidou</groupId><artifactId>lock4j-core</artifactId><version>2.2.4</version> <!-- 建议使用最新稳定版 --></dependency><!-- Lock4j Redis 实现(基于 Redisson) --><dependency><groupId>com.baomidou</groupId><artifactId>lock4j-redisson-spring-boot-starter</artifactId><version>2.2.4</version></dependency>
2. 全局配置

application.yaml 中配置 Lock4j 全局默认参数(如锁过期时间、等待时间):

lock4j:
# 默认锁过期时间(毫秒):避免死锁
expire: 5000
# 默认获取锁等待时间(毫秒):超时未获取则失败
acquire-timeout: 1000
# Redis 配置(复用 Spring Redis 配置,无需重复填写)
redisson:
config: classpath:redisson.yaml # 若需自定义 Redisson 配置,可指定配置文件

4.2 @Lock4j 注解参数说明

参数名类型说明默认值
keysString[]锁的 Key 表达式(支持 Spring EL),用于动态生成锁 Key空(需手动指定)
expirelong锁过期时间(毫秒)全局配置的 lock4j.expire
acquireTimeoutlong获取锁的等待时间(毫秒)全局配置的 lock4j.acquire-timeout
lockTypeLockType锁类型(REENTRANT 可重入锁、FAIR 公平锁)REENTRANT
executorString锁执行器(如 redissonzookeeper自动匹配已引入的存储

4.3 实战案例

案例 1:简单接口防重复提交

用户提交订单时,通过锁 Key 为“用户ID+订单类型”,防止同一用户重复提交同一类型订单:

import com.baomidou.lock.annotation.Lock4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class OrderController {
private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
/**
* 提交订单:防重复提交
* @param req 订单请求(含 userId、orderType 等字段)
*/
@PostMapping("/order/submit")
// 锁 Key:ORDER_SUBMIT_LOCK_{用户ID}_{订单类型}(Spring EL 表达式动态生成)
@Lock4j(keys = {"'ORDER_SUBMIT_LOCK_' + #req.userId + '_' + #req.orderType"})
public String submitOrder(@RequestBody OrderSubmitReq req) {
orderService.createOrder(req);
return "订单提交成功,订单号:" + req.getOrderNo();
}
}
// 订单请求DTO
class OrderSubmitReq {
private Long userId; // 用户ID
private String orderType; // 订单类型(如 "NORMAL"、"SECKILL")
private String orderNo; // 订单号
// Getter + Setter
}
案例 2:自定义锁超时时间

定时任务“统计每日销售额”需确保同一时间仅一个节点执行,且业务耗时较长,需自定义锁过期时间:

import com.baomidou.lock.annotation.Lock4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class SalesStatService {
private final SalesService salesService;
public SalesStatService(SalesService salesService) {
this.salesService = salesService;
}
/**
* 每日凌晨1点统计销售额
* 锁 Key:SALES_STAT_LOCK_{当前日期}(确保每日仅执行一次)
* 过期时间:300000ms(5分钟),等待时间:0ms(不等待,直接失败)
*/
@Scheduled(cron = "0 0 1 * * ?")
@Lock4j(
keys = {"'SALES_STAT_LOCK_' + T(java.time.LocalDate).now()"},
expire = 300000,
acquireTimeout = 0
)
public void statDailySales() {
String date = java.time.LocalDate.now().toString();
salesService.calculateDailySales(date);
System.out.println("每日销售额统计完成,日期:" + date);
}
}

4.4 异常处理

当获取锁超时(超过 acquireTimeout)时,Lock4j 会抛出 LockFailureException,可通过全局异常处理器统一捕获:

import com.baomidou.lock.exception.LockFailureException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(LockFailureException.class)
public String handleLockFailure(LockFailureException e) {
return "操作过于频繁,请稍后再试!";
}
}

5、两种方式对比与选型建议

维度编程式锁(Redisson)声明式锁(Lock4j)
代码侵入性高(需手动写加锁/解锁逻辑)低(仅需注解)
灵活性高(支持复杂锁逻辑,如红锁、读写锁)低(仅支持基础锁类型,复杂逻辑需扩展)
学习成本高(需理解 Redisson 各类锁的使用场景)低(注解参数简单,易上手)
适用场景复杂分布式场景(如分布式事务、高并发互斥)简单场景(如防重复提交、定时任务)

选型建议

  1. 若业务逻辑简单(如接口防重、定时任务),优先选择 Lock4j 声明式锁,减少代码冗余。
  2. 若需复杂锁类型(如读写锁、红锁)或自定义锁逻辑,优先选择 Redisson 编程式锁,确保可靠性。
  3. 若项目已集成 Redisson,推荐统一使用 Redisson 避免引入过多框架。

6、常见问题与解决方案

  1. 锁过期导致业务未执行完?
    • 方案1:合理设置 leaseTime(大于业务最大耗时);
    • 方案2:使用 Redisson 的“自动续期”功能(RLock 默认开启,需确保 Redisson 客户端正常运行)。
  2. Redis 单点故障导致锁失效?
    • 方案:使用 Redisson 红锁(RedissonRedLock),在多个 Redis 节点(如 3 个)加锁,只要多数节点加锁成功即视为锁有效,容忍单点故障。
  3. Lock4j 注解不生效?
    • 检查是否引入 Lock4j 对应的存储实现。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/958049.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

构建AI智能体:五十七、LangGraph + Gradio:构建可视化AI工作流的趣味指南 - 教程

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

日总结 23

在软件开发中,DTO(Data Transfer Object,数据传输对象)和 Entity(实体)是两个不同场景下的核心概念,主要区别体现在用途、设计目的、属性特征等方面,具体如下:定义与核心用途 Entity(实体)本质是与数据库表…

详细介绍:基于Echarts+HTML5可视化数据大屏展示-车辆综合管控平台

详细介绍:基于Echarts+HTML5可视化数据大屏展示-车辆综合管控平台pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "…

基于ollama和streamlit的聊天机器人

基于ollama和streamlit的聊天机器人 利用deepseek蒸馏模型 model=deepseek-r1:1.5b这个可以在client.chat里头自己定义会自动下载import streamlit as st import ollama client=ollama.Client(host=http://localhost:1…

CSP-S 2025 T2 [道路建设]

前言 赛时只拿到了$ kruskal$板子的 \(A\)性质的 \(32pts\),真正唐完,,,, 切入 首先我们可以一眼顶针看出这是一道最小生成树的题,然后发现\(K\leq10\),可以想到直接暴力枚举\(2^k\)次的不同排列,然后跑一遍\(…

使用Git钩子+ husky + lint语法检查提高前端项目代码质量

@目录配置 Git Hook原理介绍安装 Husky启用 Husky添加 Git Hook测试脚本执行效果添加语法检查安装Prettier配置Prettier格式化规则调优添加 Git Hook添加Git提交规范检查安装commitlint配置commitlint添加 Git Hook 作…

[题解]P10277 [USACO24OPEN] Bessies Interview S

P10277 [USACO24OPEN] Bessies Interview S 第一问可以用优先队列模拟,存储每个人的结束时间即可。 第二问,一开始考虑的是对于某一时刻队列中结束时间最小的人是可以任意互换顺序的,所以就用并查集把这些人合在一起…

关于 Java快速查找详细

package V_Recursion;public class C_QuickSort {public static void main(String[] args) {int[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 10, 8};quicSort(arr, 0, arr.length - 1);for (int i = 0; i < arr.length; i++…

什么是Ansible 清单 - 详解

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

完整教程:如何用开源软件

完整教程:如何用开源软件pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &…

第一次团队项目作业

软件工程团队项目第一次作业 - VALORANT 智能战术助手(第一部分) 作业信息这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzu/202501SoftwareEngineering/这个作业要求在哪里 https://edu.cnblogs.com/campu…

隨機變量本質之最終闡述

懷特博士: 我的理智所剩無幾,必須儘快警告您——切勿觸碰概率的本質!那个不可名狀的存在堂而皇之地通過霍爾教授的死宣誓著自己的主宰。Kl-mghrov——我無法準確發音,這個名字本身就讓燈光閃爍、指針顫動。我是如此…

足式机器人适应多地形的方案

基于视觉的感知运动 两阶段方法:流程:首先在完全可观测的马尔可夫决策过程(MDP) 中,利用特权信息训练一个强大的教师策略。然后,通过知识蒸馏,将教师策略的知识迁移给一个仅能使用真实传感器数据的学生策略。优…

使用vLLM实测3090和4090的大模型推理性能

使用`vLLM`测试下大模型并发推理场景下3090和4090两张显卡的性能表现,看下4090是否在高并发场景下具备更高的扩展性。 3090显卡和4090显卡在模型推理过程中的显存和GPU使用率都比较接近,1~8并发度场景下,3090和409…

CF1700F Puzzle

考虑如果是 \(1 \times n\) 怎么做。 显然是前缀和的差的绝对值的和,因为每次移动改变一位前缀和。 考虑上下交换的本质是什么,就是给第一行减 \(1\),第二行加 \(1\),反过来同理,那么在前缀和异号时显然交换是不劣…

Redis高可用与高并发探险之旅:从单机到集群的完美进化【第三部分】

可以结合之前的文章融合起来一起理解学习:分布式缓存-Redis集群在一个名为"数据大陆"的世界里,Redis王国正面临着前所未有的挑战。随着用户流量的激增,单机Redis服务器已经不堪重负。今天,就让我们跟随年…

UE:论运行时动画录制的关键-正确获取骨骼数据与保存

© mengzhishanghun 原创文章 首发于 博客园 禁止未经授权转载核心问题 在 UE5.4 中实现运行时动画录制,最关键的两个问题是:如何获取正确的骨骼数据 - 避免崩溃和数据不匹配如何正确保存 AnimSequence - 使用…

a-menu 当设置折叠状态如何穿透悬浮菜单样式

效果antReset.css .ant-menu-submenu .ant-menu-submenu-popup .ant-menu .ant-menu-light {border: 1px solid #173808 !important; }/* 直接针对 popup 整体背景 */ .ant-menu-submenu-popup {background-color: #17…