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

309 阅读6分钟

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

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

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

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

问题探究

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

分布式锁的简单实现

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

一、创建自定义注解类

@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 "系统繁忙,请稍后再试";
}

二、创建自定义切面

@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;
    }

}

三、接口方法增加注解

//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分布式锁对比

维度RedisZookeeper
一致性模型最终一致性强一致性
性能10w+ QPS1w+ 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,最终兜底靠业务容错。