Redis进阶用法:分布式锁实现与实践

前言:为什么需要分布式锁?

在现代分布式系统中,多个服务实例同时访问共享资源已成为常态。想象一下电商平台的秒杀场景:成千上万的请求同时涌入,试图扣减同一商品的库存。如果没有有效的并发控制机制,极可能导致超卖问题------库存减为负数,这显然是业务无法接受的。

单机环境下,我们可以使用语言内置的锁机制(如Java的synchronized或ReentrantLock)解决并发问题。但在分布式系统中,这些本地锁无法跨进程、跨服务器生效,这时就需要分布式锁登场了。

Redis以其高性能、原子操作和丰富的数据结构,成为实现分布式锁的热门选择。本文将深入探讨基于Redis的分布式锁实现方案,从基础实现到生产级优化,帮助你在分布式环境中构建可靠的并发控制机制。

问题探究

现状:用户针对同一张单据重复操作(比如:重复点击提交),导致数据存储重复,虽然在前端做了防重复提交,但该问题还是偶发,由此引入分布式锁。

分布式锁的简单实现

由于系统中存在数据权限控制,所以分布式锁需要控制到用户+单据级别

一、创建自定义注解类

less 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RedissonLock {

    /**
     * 锁名称前缀(默认方法名)
     */
    String name() default "resubmit_lock";

    /**
     * 唯一键的 SpEL 表达式(从方法参数中提取)
     * 示例: "#param.id" 或 "#id"
     * 示例: "#param1.id + '_' + #param2.id"
     */
    String key() default "";

    /**
     * 锁自动释放时间(默认 30 秒)
     */
    long leaseTime() default 10;

    /**
     * 锁自动释放时间单位(默认秒)
     */
    TimeUnit timeUnit() default TimeUnit.SECONDS;

    /**
     * 获取锁失败时提示信息
     */
    String message() default "系统繁忙,请稍后再试";
}

二、创建自定义切面

java 复制代码
@Aspect
@Component
public class RedissonLockAspect {

    private final RedissonClient redissonClient;
    private final SpelExpressionParser spelParser = new SpelExpressionParser();
    private final LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();

    public RedissonLockAspect(RedissonClient redissonClient) {
        this.redissonClient = redissonClient;
    }

    @Around("@annotation(redissonLock)")
    public Object around(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) throws Throwable {
        // 1. 构造锁的 Key
        String lockKey = buildLockKey(joinPoint, redissonLock);

        // 2. 获取分布式锁
        RLock lock = redissonClient.getLock(lockKey);

        try {
            // 3. 尝试加锁(非阻塞式)
            boolean isLocked = lock.tryLock(0, redissonLock.leaseTime(), redissonLock.timeUnit());
            if (!isLocked) {
                throw new OperateException(redissonLock.message()); // 自定义业务异常
            }

            // 4. 执行原方法
            return joinPoint.proceed();
        } finally {
            // 5. 释放锁(确保当前线程持有锁)
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }

    /**
     * 构建锁的 Key 格式: lock:{name}:{account}:{keyValue}
     *
     * @param joinPoint    ProceedingJoinPoint
     * @param redissonLock RedissonLock
     * @return java.lang.String
     **/
    private String buildLockKey(ProceedingJoinPoint joinPoint, RedissonLock redissonLock) {
        // 获取当前用户 ID(需根据实际系统实现)
        String account = StpUtil.getSession().get("account").toString();

        // 解析 SpEL 表达式获取动态 key
        String dynamicKey = parseSpel(redissonLock.key(), joinPoint);

        // 组合完整 Key
        String prefix = redissonLock.name().isEmpty()
                ? joinPoint.getSignature().getName()
                : redissonLock.name();

        return String.format("lock:%s:%s:%s", prefix, account, dynamicKey);
    }

    /**
     * 解析 SpEL 表达式
     *
     * @param spel      String
     * @param joinPoint ProceedingJoinPoint
     * @return java.lang.String
     **/
    private String parseSpel(String spel, ProceedingJoinPoint joinPoint) {
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);

        if (paramNames == null || spel.isEmpty()) {
            return "";
        }
        Expression expression = spelParser.parseExpression(spel);
        return expression.getValue(createContext(method, joinPoint.getArgs()), String.class);
    }

    /**
     * 创建 SpEL 上下文
     *
     * @param method Method
     * @param args   Object[]
     * @return org.springframework.expression.EvaluationContext
     **/
    private EvaluationContext createContext(Method method, Object[] args) {
        // 获取方法参数名
        String[] paramNames = parameterNameDiscoverer.getParameterNames(method);
        // 构建参数名-值映射
        Map<String, Object> variables = new HashMap<>();
        if (paramNames != null) {
            for (int i = 0; i < paramNames.length; i++) {
                variables.put(paramNames[i], args[i]);
            }
        }
        // 使用 StandardEvaluationContext(注意安全风险)
        StandardEvaluationContext context = new StandardEvaluationContext();
        context.setVariables(variables);
        return context;
    }

}

三、接口方法增加注解

less 复制代码
//todoTaskRequest-请求入参,taskId-需要加锁的key(保证唯一)
@PostMapping("/complete")
@Operation(summary = "完成任务")
@RedissonLock(key = "#todoTaskRequest.taskId", message = "请勿重复审批")
public AjaxResult<Object> completeTask(@RequestBody TodoTaskRequest todoTaskRequest) {
    taskServiceImpl.complete(todoTaskRequest.getTaskId(), todoTaskRequest.getVariables());
    return AjaxResult.success("任务处理完成");
}

与Zookeeper分布式锁对比

维度 Redis Zookeeper
一致性模型 最终一致性 强一致性
性能 10w+ QPS 1w+ QPS
锁实现方式 临时键值 临时顺序节点
适用场景 高频短耗时操作 低频长事务操作
故障恢复 依赖异步复制 快速选举恢复

常见问题与解决方案

一、锁过期但业务未执行完怎么办?

方法 实现方式 优点 缺点 适用场景
锁续期(Watch Dog) 获取锁后启动后台线程,定期检查业务是否执行完,若未完成则延长锁过期时间(如Redisson的lock() - 自动续期,减少锁提前释放风险 - 开源框架(如Redisson)已内置支持,使用方便 - 需要额外线程/协程维护 - 客户端崩溃时仍可能无法续期 适用于业务执行时间波动较大的场景
合理设置超时时间 预估业务执行时间(如95%分位数),设置锁过期时间 = 3~5倍业务时间(SET key value NX PX 30000 - 实现简单,无额外开销 - 适合执行时间稳定的业务 - 无法应对极端情况(如网络抖动) - 过长超时会降低并发性能 适用于执行时间稳定的短任务
异步续期+心跳检测 客户端获取锁后定期发送心跳,若业务完成则停止续期,崩溃时心跳停止导致锁自动过期 - 可灵活控制续期逻辑 - 客户端崩溃后锁能自动释放 - 实现复杂,需维护心跳线程 - 可能因网络问题误判 需要精细控制锁生命周期的场景
锁版本号/令牌机制 获取锁时生成随机Token,每次续期更新Token,释放锁时校验Token是否匹配 - 避免误删其他客户端的锁 - 安全性更高 - 实现较复杂 - 需保证Token的唯一性和一致性 对安全性要求高的分布式锁场景
业务拆分 将长任务拆分为多个短任务,每个子任务独立加锁(如分阶段提交) - 避免长时间占用锁 - 提高系统整体并发能力 - 业务改造成本高 - 需保证子任务间的状态一致性 适用于可拆分的长时间任务

二、Redis主从切换导致的锁失效问题

方案 实现方式 优点 缺点 适用场景
Redlock算法 部署多个独立Redis节点,客户端向多数节点(N/2+1)获取锁成功才算有效 - Redis官方推荐 - 容忍部分节点故障 - 理论可靠性高 - 实现复杂 - 需要5个以上节点 - 性能较低(需多节点通信) - 时钟漂移问题 金融、交易等高一致性要求的场景
哨兵模式+锁续期 结合Redis哨兵监控,主从切换时通过Watch Dog自动续期锁,避免切换期间锁失效 - 实现相对简单 - 开源框架(如Redisson)支持 - 平衡性能与可靠性 - 主从切换瞬间仍可能丢锁 - 依赖哨兵配置和监控 大多数业务场景(如订单、库存)
Raft-based实现 使用强一致性的Redis变种(如KeyDB)或etcd/ZooKeeper,基于Raft协议保证数据一致性 - 强一致性保证 - 自动故障转移 - 无需额外算法 - 需更换Redis实现 - 性能可能下降 - 运维复杂度高 对一致性要求极高的基础设施场景
业务层容错设计 通过幂等性、乐观锁、补偿机制等业务逻辑降低锁失效的影响 - 不依赖Redis特性 - 通用性强 - 成本低 - 业务改造成本高 - 无法完全避免并发问题 锁失效风险可接受的业务场景

结语

没有完美的分布式锁方案,需要根据业务需求权衡选择:高并发用Redis+哨兵,强一致用Redlock或etcd,最终兜底靠业务容错。

相关推荐
南囝coding15 分钟前
这个Web新API让任何内容都能画中画!
前端·后端
林太白22 分钟前
VitePress项目工程化应该如何做
前端·后端
字节跳跃者1 小时前
Java 中的 Stream 可以替代 for 循环吗?
java·后端
北执南念1 小时前
如何在 Spring Boot 中设计和返回树形结构的组织和部门信息
java·spring boot·后端
修仙的人1 小时前
【开发环境】 VSCode 快速搭建 Python 项目开发环境
前端·后端·python
FinalLi1 小时前
SpringBoot3.5.0项目使用ALLATORI JAVA混淆器
后端
bobz9652 小时前
用于服务器测试的 MCP 开发工具
后端
SimonKing2 小时前
流式数据服务端怎么传给前端,前端怎么接收?
java·后端·程序员
Laplaces Demon2 小时前
Spring 源码学习(十)—— DispatcherServlet
java·后端·学习·spring
BigYe程普2 小时前
出海技术栈集成教程(一):域名解析与配置
前端·后端·全栈