让代码自带「防重」Buff: 手写分布式幂等组件

一、组件概述

该分布式幂等组件基于 Spring AOPRedis 实现,通过注解声明式配置,提供灵活、高性能的接口幂等性保障。支持快速失败与阻塞等待两种模式,集成自定义策略扩展能力,适用于springboot项目分布式场景下的重复请求拦截与处理。


代码已上传github,欢迎star: 传送门

二、核心功能点

  1. 声明式注解支持
    通过 @Idempotent 注解标记需幂等控制的方法,支持参数动态配置。
  2. 多种策略组合
    • 幂等策略:默认基于 Redis 缓存,支持自定义处理器(如数据库持久化)。
    • 响应策略 :支持返回空值(NULL)、抛出异常(EXCEPTION)、返回历史结果(PREVIOUS)。
  3. 并发控制
    基于 Redisson 分布式锁实现,支持快速失败(failFast=true)或阻塞等待锁(failFast=false)。
  4. 动态参数识别
    通过 columns 参数指定幂等键生成规则,支持 EL 表达式动态提取参数。
  5. 自动配置与扩展
    通过 Spring Boot starter 自动装配,支持自定义处理器工厂扩展,无缝集成现有系统。

三、代码逻辑流程

1. 请求拦截与上下文构建

  • AOP 切面拦截 :通过 IdempotentAspect 拦截被 @Idempotent 注解标记的方法。
  • 生成唯一标识 :根据 uid类名+方法名 构建唯一键,结合参数生成 Redis Key。
  • 上下文初始化 :收集参数、策略配置、处理器实例,封装为 IdempotentContext

核心代码:

java 复制代码
    @Around("@annotation(anno)")
    public final Object proceed(ProceedingJoinPoint joinPoint, Idempotent anno) throws Throwable {
        // 所有的参数值的数组
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 幂等增强 委托给IdempotentExecutor
        Object retObj = executor.execute(new IdempotentCallback<Object>() {
            @Override
            public IdempotentContext<Object> initContext() throws Throwable {
                return new IdempotentContext<>()
                        .setUid(buildUid(joinPoint, signature, anno))
                        .setDuration(anno.duration())
                        .setArgMap(buildArgMap(joinPoint, signature))
                        .setIdemParam(buildIdemParam(joinPoint, anno))
                        .setFailFast(anno.failFast())
                        .setIdemStrategy(anno.custom().idemStrategy())
                        .setIdemHandler(IdemHandlerUtils.getIdemHandler(joinPoint, anno.custom()))
                        .setRespStrategy(anno.custom().respStrategy())
                        .setRespHandler(IdemHandlerUtils.getRespHandler(joinPoint, anno.custom()));
            }

            @Override
            public Object execute() throws Throwable {
                return joinPoint.proceed();
            }
        });
        return JSON.parseObject(JSON.toJSONString(retObj), (Class<?>) signature.getReturnType());
    }

2. 幂等校验与执行

  • 分布式锁控制 :通过 IdempotentExecutor 获取 Redisson 锁,确保并发安全。
    • 快速失败模式:尝试获取锁,失败则直接触发响应策略。
    • 阻塞模式:等待锁释放后执行逻辑。
  • 持久化校验 :调用 PersistProcessor 检查 Redis 中是否存在历史记录,避免重复提交。
  • 业务逻辑执行:若校验通过,执行业务代码并将结果持久化到 Redis。

3. 结果处理

  • 自定义响应:根据策略返回预定义结果(如异常、空值或历史数据)。
  • 自动清理:通过 Redis 的 TTL 机制自动清理过期记录。

核心代码:

java 复制代码
   public final <R> R execute(IdempotentCallback<R> callback) throws Throwable {

        IdempotentContext<R> context = callback.initContext();
        String uid = context.getUid();
        if (StringUtils.isBlank(uid)) {
            throw new Exception("幂等组件: uid不能为空");
        }
        List<Object> param = context.getIdemParam();
        if (CollectionUtils.isEmpty(param)) {
            throw new Exception("幂等组件: idemParam不能为空");
        }
        RLock lock = client.getLock(buildLock(uid, param));
        return context.isFailFast() ? doFast(callback, context, lock) : doLazy(callback, context, lock);
    }

    private <R> R doLazy(IdempotentCallback<R> callback, IdempotentContext<R> context, RLock lock) throws Throwable {
        lock.lock();
        try {
            IdempotentResult<R> result = context.getIdemHandler().handle(context);
            if (result.isFlag()) {
                //执行
                R r = callback.execute();
                //缓存结果,自定义策略不缓存
                processor.persist(context.setResult(r));
                return r;
            }
            return context.setResult(result.getResult()).getRespHandler().handle(context);
        } finally {
            lock.unlock();
        }
    }

    private <R> R doFast(IdempotentCallback<R> callback, IdempotentContext<R> context, RLock lock) throws Throwable {
        if (lock.tryLock()) {
            try {
                //获取到锁,执行逻辑
                IdempotentResult<R> result = context.getIdemHandler().handle(context);
                if (result.isFlag()) {
                    //执行
                    R r = callback.execute();
                    //缓存结果,自定义策略不缓存
                    processor.persist(context.setResult(r));
                    return r;
                }
            } finally {
                lock.unlock();
            }
        }
        return context.getRespHandler().handle(context);
    }

四、使用方式

1. 添加依赖(pom.xml)

xml 复制代码
<dependency>
    <groupId>com.shemuel</groupId>
    <artifactId>idempotent-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

2. 配置文件

yaml 复制代码
idempotent:
  enable: true
  env: prod
  redis:
    host: 127.0.0.1
    port: 6379
    password: redis-pass

3. 添加注解

java 复制代码
@Idempotent(
    uid = "order.create", 
    columns = {"#userId", "#orderId"}, // 动态提取参数
    failFast = true,
    duration = 10,
    custom = @Strategy(
        idemStrategy = IdemStrategy.DEFAULT,
        respStrategy = RespStrategy.EXCEPTION
    )
)
public Order createOrder(Long userId, String orderId) {
    // 业务逻辑
}

4. 自定义扩展(按需)

  • 实现 IdemHandler :覆盖 doHandle 方法定义自定义幂等逻辑。
  • 实现 RespHandler:覆盖 doHandle 方法定义自定义响应逻辑。
  • 工厂注册:通过 IdemHandlerFactoryRespHandlerFactory 注册 Bean。

五、设计模式应用

  1. 模板方法模式
    IdempotentExecutor 封装执行流程,子流程(如锁获取、持久化)通过回调接口 IdempotentCallback 实现。
  2. 策略模式
    通过 IdemStrategyRespStrategy 定义多套处理策略,支持运行时动态切换。
  3. 工厂模式
    IdemHandlerFactoryRespHandlerFactory 集中管理处理器实例,解耦创建与使用逻辑。
  4. 代理模式
    Spring AOP 动态代理拦截方法调用,实现非侵入式幂等控制。
  5. 单例模式
    Spring Bean 默认单例,确保处理器和配置类的全局唯一性。

六、总结

该组件通过 注解驱动 + 分布式锁 + 策略扩展 的设计,实现了轻量级、高可用的幂等控制。开发者无需修改业务代码即可集成,同时支持灵活扩展,适用于电商、金融等高并发场景,有效保障系统数据一致性。

代码已上传github, 欢迎star; github.com/ShemuelDeng...

最后

欢迎关注gzh:加瓦点灯, 每天推送干货知识!

相关推荐
你的人类朋友14 分钟前
【Node.js】什么是Node.js
javascript·后端·node.js
David爱编程2 小时前
面试必问!线程生命周期与状态转换详解
java·后端
LKAI.2 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi
Victor3562 小时前
Redis(11)如何通过命令行操作Redis?
后端
Victor3562 小时前
Redis(10)如何连接到Redis服务器?
后端
他日若遂凌云志4 小时前
深入剖析 Fantasy 框架的消息设计与序列化机制:协同架构下的高效转换与场景适配
后端
快手技术4 小时前
快手Klear-Reasoner登顶8B模型榜首,GPPO算法双效强化稳定性与探索能力!
后端
二闹4 小时前
三个注解,到底该用哪一个?别再傻傻分不清了!
后端
用户49055816081255 小时前
当控制面更新一条 ACL 规则时,如何更新给数据面
后端
林太白5 小时前
Nuxt.js搭建一个官网如何简单
前端·javascript·后端