后端_Redis 分布式锁实现指南
前言
在分布式系统中,多节点并发操作共享资源时,传统单机锁(如 synchronized
、ReentrantLock
)无法跨节点生效,Redis 分布式锁通过 Redis 的原子性操作实现跨节点互斥,成为解决分布式并发问题的核心方案。
本文基于 Redisson 和 Lock4j 框架,讲解 Redis 分布式锁的两种使用方式(编程式、声明式),并提供完整实践案例。
1、Redis 分布式锁核心原理
Redis 分布式锁的实现依赖 Redis 的原子命令和过期机制,核心逻辑如下:
- 加锁:通过
SET key value NX EX expireTime
命令实现(NX
表示“键不存在时才设置”,确保互斥;EX
表示设置过期时间,避免死锁)。 - 解锁:通过 Lua 脚本原子执行“判断值是否匹配 + 删除键”(避免误删其他节点的锁),脚本逻辑为:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
- 防死锁:通过“过期时间”自动释放锁,即使持有锁的节点宕机,锁也会在过期后释放。
- 高级特性:部分框架(如 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.host
、spring.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 注意事项
- 解锁安全性:必须通过
isHeldByCurrentThread()
校验锁持有者,避免误删其他线程的锁。 - 超时设置:
leaseTime
需大于业务执行时间(若业务耗时不确定,可使用 Redisson 的“自动续期”功能,需开启lock.setKeepLockAlive(true)
)。 - 异常处理:加锁失败需抛出异常或返回友好提示,避免业务静默失败。
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
注解参数说明
参数名 | 类型 | 说明 | 默认值 |
---|---|---|---|
keys | String[] | 锁的 Key 表达式(支持 Spring EL),用于动态生成锁 Key | 空(需手动指定) |
expire | long | 锁过期时间(毫秒) | 全局配置的 lock4j.expire |
acquireTimeout | long | 获取锁的等待时间(毫秒) | 全局配置的 lock4j.acquire-timeout |
lockType | LockType | 锁类型(REENTRANT 可重入锁、FAIR 公平锁) | REENTRANT |
executor | String | 锁执行器(如 redisson 、zookeeper ) | 自动匹配已引入的存储 |
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 各类锁的使用场景) | 低(注解参数简单,易上手) |
适用场景 | 复杂分布式场景(如分布式事务、高并发互斥) | 简单场景(如防重复提交、定时任务) |
选型建议
- 若业务逻辑简单(如接口防重、定时任务),优先选择 Lock4j 声明式锁,减少代码冗余。
- 若需复杂锁类型(如读写锁、红锁)或自定义锁逻辑,优先选择 Redisson 编程式锁,确保可靠性。
- 若项目已集成 Redisson,推荐统一使用 Redisson 避免引入过多框架。
6、常见问题与解决方案
- 锁过期导致业务未执行完?
- 方案1:合理设置
leaseTime
(大于业务最大耗时); - 方案2:使用 Redisson 的“自动续期”功能(
RLock
默认开启,需确保 Redisson 客户端正常运行)。
- 方案1:合理设置
- Redis 单点故障导致锁失效?
- 方案:使用 Redisson 红锁(
RedissonRedLock
),在多个 Redis 节点(如 3 个)加锁,只要多数节点加锁成功即视为锁有效,容忍单点故障。
- 方案:使用 Redisson 红锁(
- Lock4j 注解不生效?
- 检查是否引入 Lock4j 对应的存储实现。