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,最终兜底靠业务容错。

相关推荐
晴空月明1 小时前
分布式系统高可用性设计 - 监控与日志系统
后端
songroom2 小时前
【转】Rust: PhantomData,#may_dangle和Drop Check 真真假假
开发语言·后端·rust
红尘散仙3 小时前
Rust 终端 UI 开发新玩法:用 Ratatui Kit 轻松打造高颜值 CLI
前端·后端·rust
mldong3 小时前
mldong-goframe:基于 GoFrame + Vben5 的全栈快速开发框架正式开源!
vue.js·后端·go
canonical_entropy3 小时前
集成NopReport动态生成复杂Word表格
后端·低代码
come112344 小时前
Go 包管理工具详解:安装与使用指南
开发语言·后端·golang
绝无仅有4 小时前
OSS文件上传解析失败,错误:文件下载失败的排查与解决
后端·面试·架构
LaoZhangAI4 小时前
Kiro vs Cursor:2025年AI编程IDE深度对比
前端·后端
brzhang6 小时前
OpenAI 7周发布Codex,我们的数据库迁移为何要花一年?
前端·后端·架构