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

后端_Redis 分布式锁实现指南

前言

在分布式系统中,多节点并发操作共享资源时,传统单机锁(如 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.1port: 6379password: 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;}@Overridepublic 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; // 用户IDprivate 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.dtcms.com/a/456817.html

相关文章:

  • K8s学习笔记(十六) 探针(Probe)
  • 企业个人网站口碑营销策略
  • c语言网站三星网上商城分期
  • Gradient Descent and Its Implementation in TensorFlow|梯度下降及其在 TensorFlow 中的实现
  • 大模型解码策略深度解析:从原理到工程实践
  • 【Java并发】揭秘Lock体系 -- 深入理解ReentrantReadWriteLock
  • xedu和5070
  • gitlab 在centos7 下的安装和基本使用
  • 优化GitHub访问问题
  • 二、项目结构与版本控制规范
  • 快消存量竞争时代:洗衣液 “三级加速器” 成行业新范本
  • 网站建设实训致谢语电商网站运营策划
  • 三分钟做网站网站访客统计代码
  • Arduino开发ESP32点亮一个LED【适合新手】
  • 【心理分析】好为人师
  • 离线二维码生成器,无需网络自制专属二维码
  • OpenCV(六):TrackBar控件
  • 网站开发 验收模板手机网站案例 鸿
  • 向量化编码和RAG增强搜索
  • 分布式场景下防止【缓存击穿】的不同方案
  • 《Cargo 参考手册》第二章:工作区(Workspaces)
  • 2025山西旅游攻略(个人摩旅版-国庆从北京到山西)
  • 博弈论——一些概念
  • 注册安全工程师资源合集
  • C++ 位运算 高频面试考点 力扣 面试题 17.19. 消失的两个数字 题解 每日一题
  • 深圳著名设计网站wordpress 目录配置
  • Benders 文献推荐
  • 【C语言基础详细版】08. 结构体、共用体、枚举详解:从定义到内存管理
  • 整理 tcp 服务器的设计思路
  • 域名备案未做网站个人做广播网站需要注意什么