后端_Redis 分布式锁实现指南

前言

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

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

1、Redis 分布式锁核心原理

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

  1. 加锁 :通过 SET key value NX EX expireTime 命令实现(NX 表示"键不存在时才设置",确保互斥;EX 表示设置过期时间,避免死锁)。
  2. 解锁:通过 Lua 脚本原子执行"判断值是否匹配 + 删除键"(避免误删其他节点的锁),脚本逻辑为:
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 连接):

xml 复制代码
<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 配置:

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 格式(避免硬编码):

java 复制代码
public class RedisKeyConstants {
    /**
     * 支付通知分布式锁 Key:PAY_NOTIFY_LOCK_{订单ID}
     */
    public static final String PAY_NOTIFY_LOCK = "PAY_NOTIFY_LOCK:%s";

    // 其他业务 Key...
}
2. 封装锁操作 DAO(可选)

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

java 复制代码
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 加锁,确保同一订单的通知仅被处理一次:

java 复制代码
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 依赖:

xml 复制代码
<!-- 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 全局默认参数(如锁过期时间、等待时间):

yaml 复制代码
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 锁执行器(如 redissonzookeeper 自动匹配已引入的存储

4.3 实战案例

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

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

java 复制代码
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:自定义锁超时时间

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

java 复制代码
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,可通过全局异常处理器统一捕获:

java 复制代码
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 对应的存储实现。
相关推荐
00后程序员张3 小时前
RabbitMQ核心机制
java·大数据·分布式
liuy96153 小时前
迷你论坛项目
数据库
杨云龙UP3 小时前
小工具大体验:rlwrap加持下的Oracle/MySQL/SQL Server命令行交互
运维·服务器·数据库·sql·mysql·oracle·sqlserver
阿巴~阿巴~3 小时前
使用 C 语言连接 MySQL 客户端(重点)
服务器·数据库·sql·mysql·ubuntu
清水加冰3 小时前
【MySQL】SQL调优-如何分析SQL性能
数据库·sql·mysql
倔强的石头1064 小时前
【金仓数据库】ksql 指南(二) —— 创建与管理本地数据库
数据库·kingbasees·金仓数据库
编程充电站pro4 小时前
SQL 面试题解析:如何用多表查询写用户订单统计?
数据库·sql
TitosZhang5 小时前
BIO、NIO、AIO详解
java·redis·nio
爱吃烤鸡翅的酸菜鱼5 小时前
深度掌握 Git 分支体系:从基础操作到高级策略与实践案例
分布式·git·后端·gitee·github