后端_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 对应的存储实现。
相关推荐
jiayou642 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤3 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者4 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区4 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1774 天前
《从零搭建NestJS项目》
数据库·typescript
加号34 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏4 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐4 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再4 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip
tryCbest4 天前
数据库SQL学习
数据库·sql