news 2026/10/22 23:33:25

Redis防重复提交:从原理到实战的完整方案

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redis防重复提交:从原理到实战的完整方案

接口重复提交,我估计做后端的人多少都遇到过。用户手一抖双击了提交按钮,前端超时自动重试,第三方回调因为网络抖动重发,不管哪种情况,落到数据库可能就是重复订单、重复扣款、重复发放权益。用Redis做防重复提交,核心思路就一句话:把一次请求的唯一身份放进Redis,快速判断这个身份是不是已经处理过。今天我把这个方案从设计到落地完整讲一遍,包括两种常见策略、核心代码实现、压测验证过程,还有我踩过的几个坑。适合正在用Redis做缓存、又被接口幂等和重复提交问题弄到头疼的后端开发者,尤其是Java技术栈的朋友。

1. 重复提交的典型场景,以及为什么最后选了Redis

1.1 你遇到的重复提交,其实是三种完全不同的情况

我在不同项目里见过太多次“重复提交”,但仔细拆开看,大多数纠纷都来自没分清楚它到底属于哪一种。

第一种是用户层面的手滑。页面提交按钮没有做loading禁用,用户点了两次,或者网络卡顿导致用户以为没点上又点了一次。这种重复请求间隔通常在几百毫秒到几秒,危害是产生两条一模一样的数据。

第二种是客户端或者网关层面的重试机制。很多移动端网络库对POST请求有内置超时重试,一旦客户端没有等到底层响应,就会自动重发一次。这种情况间隔可能比较久,几秒甚至几十秒,但请求内容完全一致。

第三种是服务间调用和回调通知的乱序重放。比如支付系统异步通知可能连续发送多条,MQ消费者消费超时后重新投递,合作的第三方系统因为自己没收到响应而重复推送。这种场景下,重复请求可能来自不同节点、不同线程,也必须保证业务上只处理一次。

这三个场景的共性是:请求本身是合法的,但因为各种原因被重复执行了,后端必须有能力识别并拦截。你不能去改用户的浏览器,也不能保证所有上游系统都遵循精确一次投递,只能在接口层做去重。

1.2 为什么不直接用数据库唯一索引,或者分布式锁

很多团队一开始会想到数据库唯一索引。给业务表加一个唯一约束,重复插入时数据库直接报错,可以说是最硬的一道防线。但它的缺点也很明显:第一,你必须在写库阶段才能发现重复,意味着前面所有参数校验、查询、组装、事务都已经跑了一遍,浪费了性能;第二,很多接口并不是简单插入一条记录,比如发短信、调第三方、做异步扣减,根本没有合适的表可以加唯一索引;第三,数据库唯一索引报错后,你需要捕获并翻译成用户友好的提示,这套异常处理在某些框架里还挺麻烦。

分布式锁也被经常拿来对比。分布式锁解决的是并发互斥问题,意思是同一时间只能有一个请求执行,其他请求排队等待;但防重复提交解决的是去重问题,意思是同一个业务标记已经在处理或已经处理过,后续请求应该直接拒绝。如果要靠分布式锁做防重,你得设置一个足够长的持有时间,把已经处理完的和正在处理的都挡在外面,锁释放逻辑会变得很别扭,而且等待机制会让用户感到明显卡顿。

Redis方案的好处正好踩在点上:内存操作延迟一般在亚毫秒级,对正常请求路径几乎没有影响;SETNX命令自带原子性,不需要自己处理并发判断;key可以设置过期时间,天然适合“短时间内去重”这种需求;最重要的是,它判断重复发生在接口一开始进入的时候,根本不用走到数据库。

1.3 Redis防重复提交的基本原理,一句话讲清

核心就一条:利用SET key value NX EX seconds,只有当key不存在时才写入成功,写入成功就代表这个请求拿到了“执行权”,写入失败就说明同一标识的请求已经被处理过或正在处理中,直接返回重复提交提示。

这其实就是拿Redis当一张“判重表”。你用请求里某个唯一标识作为key,第一次进来时Redis里没有这个key,写入成功,请求继续向下执行;第二次携带同样标识的请求进来,Redis里已有key,写入失败,请求被拦下。key过期时间一到自动删除,新的请求又可以正常提交。

这个方案不需要额外的中间件,只要项目里已经用了Redis就能做,接入成本非常低。我在一个小型电商项目里落地时,从写注解到压测通过,一共只用了一个下午。

2. 两种主流防重策略:Token预签发与请求唯一ID

2.1 Token预签发机制,适合用户表单页面

Token机制的做法很经典:用户进入提交页面时,后端先下发一个token并把它存到Redis;用户真正提交数据时必须带上这个token;后端拿到token后,如果能在Redis里查到就删掉并继续执行,如果查不到就说明已经过期或已被消费,直接拒绝。

这个方案的关键在于“预生成”。=token代表的是某一次页面会话的提交凭证,一个页面、一次业务操作只对应一个token。即使前端按钮没设禁用,用户狂点十次,只有第一个带着有效token的请求能通过,后面九个请求拿到的token已经被删除,自然进不了业务逻辑。

我建议在表单类、需要页面交互的接口用这个方案。比如下单页、报名页、预约页,用户操作路径明确,前端有充足机会先获取token。它的额外好处是:可以顺便防范参数被外部篡改后重放,因为攻击者不知道当前的token值。

不过Token机制也有需要注意的地方。后端下发token时一定要设置合理的过期时间,太短用户填表时间长了就失效,太长又起不到防重作用。我一般给10分钟,如果业务的表单特别复杂,视情况延长到30分钟,但要做成可配置项。

2.2 请求唯一幂等键机制,适合服务间调用和回调

Token机制在用户页面很好用,但到了服务间调用就有点别扭:调用方不可能先去拉一个token再调业务接口,很多情况下是没有页面交互的,比如支付回调、MQ消费、开放平台API。这时候就要用请求唯一幂等键。

实现方式有两种。第一种是调用方生成一个全局唯一的requestId,随请求一起传过来,后端拿这个requestId作为Redis key;第二种是后端自己根据业务参数组装幂等键,比如用户ID加接口名加订单号。两种方式都行,但务必保证幂等键在业务语义上唯一。

我个人更倾向后端组装幂等键。依赖调用方传requestId有一个隐患:如果调用方代码写得粗糙,每个重试请求都重新生成一个requestId,那防重就形同虚设。后端根据参数算key,至少能保证参数一样时key一样。当然,如果调用方系统是合作方,你控制不了它的实现,那就必须两条腿走路,同时校验requestId和核心业务参数。

有些业务还需要在幂等键基础上叠加状态校验。比如支付回调场景,订单可能已经处于“已支付”状态,如果Redis里的key刚好过期了,重复回调还是会进入业务逻辑。这种情况下,我在Service层里会再加一道判断:如果订单状态已经是终态,直接返回成功,不进任何处理逻辑。Redis负责短时间去重,数据库状态负责最终幂等。

2.3 两种策略怎么选,我给一个判断标准

如果你做的接口面向人,存在页面、表单、按钮,优先用Token预签发。如果你做的接口面向系统,是API调用、异步通知、消息消费,优先用请求唯一幂等键。如果又有人交互又有系统调用,那就两个都用:页面入口用Token,后端处理时再用幂等键。单测、压测、线上灰度阶段,你会发现这个组合基本能覆盖所有重复入口。

3. 核心代码实现:注解+AOP+Redis,一套复制即用的方案

3.1 依赖和基础配置,先把地基打好

不管你是Spring Boot项目还是普通Spring项目,引入Redis和AOP是第一步。Spring Boot项目直接在pom.xml加上两个依赖就行:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>

然后确认Redis连接配置正确,就一个基础要求:生产环境不要用默认的localhost配置。

我在这个方案里没有引入Redisson,而是直接用Spring Data Redis的StringRedisTemplate。原因很简单,防重复提交只需要一个带过期时间的SETNX,不需要Redisson那套分布式锁的全部能力。越少依赖,线上排查起来越省事。

3.2 自定义注解定义,控制在最小粒度

防重逻辑要加在接口上,最优雅的方式是自定义一个注解,再用AOP统一处理,这样业务代码不需要写任何Redis逻辑。我首先定义注解:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface NoRepeatSubmit { // key的过期时间,单位秒 int expire() default 5; // 自定义幂等键的SpEL表达式,比如 #order.orderNo String keyExpression() default ""; }

注解里只放两个参数。expire控制防重窗口,keyExpression允许你指定用请求里哪个字段来组装Redis key。用SpEL是因为不同接口的核心参数不一样,比如订单提交用的是订单号,签到用的是用户ID加日期,硬编码在切面里会导致复用性很差。

这里有一个设计心得:不要把防重逻辑直接硬编码进业务Service,否则每个接口都要写一遍try-catch和Redis操作,代码会变得极其啰嗦,也容易出现漏删key的问题。注解加切面的方式,业务方法根本感知不到防重的存在,将来要把防重替换成其他方案,也只需要改切面。

3.3 AOP切面实现,关键是异常时释放key

切面是整个方案的核心。它要做三件事:解析幂等键、尝试写入Redis、根据结果决定放行或拦截。

@Aspect @Component public class NoRepeatSubmitAspect { private static final String LOCK_PREFIX = "nrs:"; @Autowired private StringRedisTemplate redisTemplate; @Around("@annotation(noRepeatSubmit)") public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable { // 1. 获取幂等键 String idempotentKey = buildIdempotentKey(joinPoint, noRepeatSubmit); String lockKey = LOCK_PREFIX + idempotentKey; String value = UUID.randomUUID().toString().replace("-", ""); // 2. 尝试占位,写入成功说明第一次请求 Boolean success = redisTemplate.opsForValue() .setIfAbsent(lockKey, value, Duration.ofSeconds(noRepeatSubmit.expire())); if (success == null || !success) { throw new BizException("操作过于频繁,请稍后再试"); } try { // 3. 执行真正的业务逻辑 return joinPoint.proceed(); } catch (Throwable throwable) { // 4. 业务失败,删除占位,允许用户重试 redisTemplate.delete(lockKey); throw throwable; } } private String buildIdempotentKey(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) { // 优先取SpEL表达式中指定的业务字段 if (StringUtils.hasText(noRepeatSubmit.keyExpression())) { // 解析参数,简单场景直接取第一个参数的指定属性 Object arg = joinPoint.getArgs()[0]; // 通过反射或SpEL工具解析,省略具体实现 return parseExpression(arg, noRepeatSubmit.keyExpression()); } // 兜底:当前用户ID + 接口路径 String userId = getCurrentUserId(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); return userId + ":" + signature.getMethod().getDeclaringClass().getSimpleName() + ":" + signature.getMethod().getName(); } }

这里有一个经常被忽略的细节:setIfAbsent成功后,value不能存死值,要存一个UUID。因为清洗key、排查问题时,你得能区分同一个key下的多个请求到底是谁占的位。另外,如果业务执行抛异常,一定要在catch里删除key,否则用户会被“卡死”——业务明明失败了,防重key还在,他再提交就永远提示重复。删key之后要重新抛出异常,让事务层正确回滚。

3.4 为什么不能先get再delete,要用Lua脚本保证原子性

防重逻辑里有一处特别容易踩坑:Token机制或业务成功后的释放动作,不能写成先get再delete两步。假设两个请求同时带着同一个token进来,请求A执行get,发现token存在,紧接着请求B也执行get,也发现token存在,然后A删掉了token,B也跟着删,两个请求就都通过了,防重直接失效。

正确做法是用Lua脚本把“判断存在并删除”合并成一个原子操作:

if redis.call("get", KEYS[1]) == ARGV[1] then return redis.call("del", KEYS[1]) else return 0 end

这个脚本先校验value是否等于自己写入的随机值,相等才删除。这样做有两个好处:一是避免误删其他第一次请求写入的key;二是把check和delete合并成一次Redis操作,不存在并发窗口。我在Spring中注入这个脚本,再用DefaultRedisScript调用,整个过程非常干净。

如果你不想用Lua,用StringRedisTemplate的delete方法也行,但必须知道它只能做“无脑删除”,没法判断value是否是当前请求写入的。在分布式环境下,如果前一个请求因为某种原因还没结束,key被新请求误删,一样会出问题。所以能用脚本就别嫌麻烦。

3.5 释放时机:短时间去重与业务结果去重的区别

加完AOP之后,你还要想清楚一个策略问题:key是业务成功后就删除,还是等过期时间到了自动删除。

如果业务成功就主动删除key,那这个方案本质上是“防并发重入”,同一时间内同一个请求只允许一个在执行。用户第一次请求处理完后,马上可以再提交新内容。这种策略适合幂等键由业务参数组装,或者Token模式先删后执行的场景。

如果业务成功也不删key,等过期时间到了自动释放,那这个方案就是“防重复提交窗口”,在窗口期内同一个幂等键只能提交一次。比如用户提交订单后10秒内不允许再次提交同一个订单号,哪怕业务已经成功,也照样拦截。这种策略更适合防用户手滑重复点击。

我把这两种做法都在项目里用过。如果你的业务对“处理后是否可以马上再来一次”没有明确要求,我建议让key在业务成功后保留一段时间,牺牲一点点灵活性,换来更强的拦截力度。用户双击导致的两个请求通常会落在几百毫秒内,5秒的防重窗口足够覆盖绝大多数手滑场景。

4. 从零到一完整落地,包含压测验证

4.1 项目结构和配置,按这个搭就不会乱

一个标准的落地工程,我一般分成四层:

  • Controller层:只负责接收参数和返回结果,接口上标注@NoRepeatSubmit。
  • Service层:真正的业务逻辑,不感知防重逻辑。
  • AOP切面层:负责统一的Redis防重判断。
  • Redis配置层:负责序列化和Lua脚本注册。

这样的分层在做单元测试时特别省心。我可以直接Mock掉RedisTemplate来测试Controller返回逻辑,也可以绕过Controller直接测试Service的业务正确性。

Redis的key结构我统一用nrs:业务前缀:具体幂等键,比如nrs:order:10001。带业务前缀的好处是排查问题时可以通过redis-cli立即定位,也避免不同模块之间key撞车。Redis的String类型value就存一个随机UUID,不存业务对象,保持简洁。

4.2 完整落地时,幂等键的SpEL解析怎么实现

前面代码里buildIdempotentKey方法留了个解析SpEL的口子,很多人会卡在这里。我直接把写法补齐。

如果接口是单参数,且该参数是一个DTO对象,我想通过注解指定去重维度:

@PostMapping("/order/submit") @NoRepeatSubmit(expire = 5, keyExpression = "#order.orderNo") public Result<Void> submit(@RequestBody OrderSubmitDTO order) { return orderService.submit(order); }

那切面里解析keyExpression,本质上就是从方法参数里取字段值。如果你已经引入了spring-expression,可以直接这样写:

private String parseSpEL(String expression, Object[] args) { MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Parameter[] parameters = signature.getMethod().getParameters(); EvaluationContext context = new StandardEvaluationContext(); for (int i = 0; i < parameters.length; i++) { context.setVariable(parameters[i].getName(), args[i]); } Expression exp = new SpelExpressionParser().parseExpression(expression); Object value = exp.getValue(context); return value == null ? null : value.toString(); }

SpEL解析有一个容易踩的坑:Spring的@PathVariable和@RequestBody参数名在编译后可能变成arg0、arg1,导致表达式绑定不到真正的参数名。所以我的建议是:要么在编译参数中加上-parameters参数,要么直接用@RequestParam注解的value和@RequestBody的类型名称来定位参数。生产环境我更推荐限定keyExpression只适用于单DTO参数场景,避免过度设计。

4.3 压测验证:200个并发请求,只允许1个成功

写好代码之后,压测这一步千万不能省。我用JMeter模拟了同一个订单号的200个并发请求:

线程数:200 循环次数:1 请求路径:POST /order/submit 请求体:{"orderNo":"10001","productId":"sku_123"} 并发策略:同时启动

压测预期是:1个请求正常处理成功,199个请求返回“操作过于频繁”。

第一轮压测结果却出乎意料,出现了十几个请求同时成功。排查发现,这些请求的orderNo都是动态生成的,JMeter中每个线程读取CSV里不同的订单号。对后端来说这其实是200个不同请求,当然应该全部成功。真正的重复提交压测,必须让所有线程携带完全相同的幂等键。

修正压测参数后,第二轮的日志就很清晰了:唯一一个成功请求执行了业务逻辑,其余请求全部在AOP层被拦截,没有一条打到了Service层。从响应时间看,被拦截的请求平均耗时不到3毫秒,Redis的延迟基本可以忽略。

这里我还特意验证了异常释放逻辑。在Service第一行人为抛出一个运行时异常后,再发送第二次请求,发现能正常进入业务逻辑,说明catch块里的delete确实在执行。很多人在压测时只看成功的正常流程,不看失败后的恢复能力,这一步容易漏。

4.4 生产环境要额外注意的Redis故障降级

写代码的时候多考虑一层:如果Redis服务挂掉,setIfAbsent命令会抛异常。这时候你的接口应该怎么办?

我见过一些团队直接把异常抛出去,结果Redis一抖动,所有接口全挂,这明显违背了防重组件“旁路”的定位。防重复提交是辅助能力,它的职责是防止脏数据,不是成为新的故障点。正确的做法是在切面里捕获Redis异常并放行,让请求正常进入业务逻辑,同时用日志或监控报警记录Redis异常情况。

降级策略和业务敏感性要权衡。如果是支付、下单这类强一致业务,我倾向于放行,并在数据库层用唯一索引兜底;如果是优惠券秒杀这类纯粹防刷场景,Redis挂了那你宁可接口暂不可用也不能放行。这个判断要在设计阶段就定下来,不要等线上故障了再做决定。

5. 线上踩过的坑:排查实录和补救办法

5.1 并发下偶发穿透:到底是谁把防重捅破了

项目上线后遇到一个诡异的问题:高并发下偶尔会出现两个相同订单号都成功创建了订单。理论上SETNX是原子的,为什么还会穿透?

我拉日志排查后发现,第一次请求的key过期时间设置成了5秒,但它对应的业务链路处理耗时超过了5秒,第一个请求还在执行时,key已经过期被自动删除,第二个请求又成功写入了一个新key,自然就把防重穿破了。

这个问题本质上不是Redis命令的问题,而是防重窗口小于业务处理时长。解决方式有两个:第一,把过期时间调大,单位从秒改成分钟级;第二,如果业务链路确实很长,防重窗口必须覆盖“业务最短完成时间”,不能只按用户手动点击间隔来设。我在订单场景中把expire设置了30秒,既不影响用户重试体验,又足够覆盖业务处理时间。

5.2 误拦截正常请求:key范围设计得太粗了

有段时间用户反馈,同一个用户在同一秒内提交两个不同的商品订单,第二个请求被拦截了。查了半天发现,切面里兜底生成的key只用了用户ID加接口方法名,没有拼接订单号,等于把“用户在某接口上的并发提交”全部串在一个key下面。

这个教训很典型。防重key的粒度必须精确到一次业务操作,而不是一个用户或一个接口。如果一个用户允许同时提交多个不同类型的单子,key里必须带上订单号、商品ID或请求参数中的唯一标识。设计key时先在纸上写一写:同一个key代表什么,两个key相同是否一定意味着业务重复?如果答案是否定的,那key粒度就是错的。

5.3 过期时间的取舍,给一个可参考的值

不同业务的防重窗口差别很大,我用一个表格记录一下各场景的参考值,改动成本不高但效果差异很大:

业务场景过期时间参考说明
普通表单提交5秒覆盖用户双击和前端超时重试
下单支付30秒到1分钟覆盖较长业务链路
第三方异步回调1到5分钟回调可能延迟重试
签到/浏览类1天按自然日固定幂等键

原则是:过期时间必须大于单次请求从入口到结束的最长耗时,同时小于用户心理上“允许再次操作”的等待阈值。像签到接口我甚至会直接把key设计成nrs:sign:userId:yyyyMMdd,跨天自动失效,根本不需要手动算过期时间。

5.4 集群环境下的主从延迟,这个坑比较隐蔽

Redis单机版没有太多问题,但生产环境一般至少是主从架构。SetIfAbsent命令在主节点执行成功,主节点还没来得及同步给从节点,这时另一个请求如果走了从节点读旧数据,会认为key不存在,从而穿透。

解决思路有三个等级。第一,强制读主节点,很多Redis客户端默认读写都走主节点,这样可以规避;第二,改用Redisson的分布式锁,它会多做一些一致性保证;第三,对极端严苛的业务,用RedLock或者在业务数据库加唯一索引做最终兜底。普通业务做到第一点就够了,不必动不动上RedLock,毕竟RedLock本身也有争议和复杂性。

5.5 数据库唯一索引依然建议保留,作为最后一道防线

Redis防重再完善,也只是应用层的拦截。真正写入数据库的时候,如果多实例同时因为某些极端情况穿透,数据库唯一索引是唯一能保证“最终一定不会产生重复数据”的手段。

我强烈建议在订单表、流水表、交易记录表这些核心表上,把业务唯一键设置为数据库唯一索引,比如order_no、transaction_id、source_type加source_id。应用层防重负责拦截99.9%的重复请求,数据库唯一索引负责兜底剩下的万分之一。捕获到唯一索引冲突后,不要把异常信息直接抛给用户,转换成友好提示“您已提交过该请求”。

5.6 排查防重问题时,我建议你先看这三个位置

如果线上仍然出现了重复数据,我的排查顺序是:先看访问日志里这个幂等键是否真的都到了后端,有可能网关层直接重写请求移除了参数;再看AOP日志里拦截成功的次数,拦截次数很少但数据重复了,问题大概率不在防重,在业务侧幂等处理;最后看Redis监控里key的过期时间,valttl一查就知道是不是窗口太短。按这个顺序排查,一般十分钟内就能定位。

6. 防重复提交的边界与扩展思考

6.1 防重不等于幂等,很多人把这两个概念搞混了

防重和幂等是两个不同层面的能力。防重针对的是“重复的请求不能再次执行”,它把请求挡在门外;幂等针对的是“同一个请求执行多次和一次效果相同”,它在业务内部消化重复。Redis防重只是让你尽量少遇到重复,一旦重复进来了,业务代码经过状态校验后依然要能安全返回,不能假设所有重复都被防住了。

一个合理的系统设计是:接口入口处用Redis拦截重复提交,业务内部用状态机和唯一索引保证即使重复到达也不会产生脏数据,消息队列消费端再通过消息ID做一次消费幂等。三层防线各管一段,这才是成熟的做法。

6.2 消息队列场景下的防重,做法略有不同

MQ消费者的重复消息和HTTP接口的重复提交成因很相似,但处理方式有区别。HTTP接口可以用AOP统一拦截,MQ消费者则更习惯在消费逻辑里先查一下消息去重表,或者使用Redis记录消息ID。

我一般会为每个消费者维护一个去重集合,消费前用SETNXredisMsg:消费组名:消息ID,写入成功才继续消费。消费完成后不急着删除,让key保留一段时间,具体时间根据业务接受重复的窗口来定。这个方案和接口防重最大的不同在于:MQ消费者允许消息在业务上重复到达,但处理结果必须一致,所以除了Redis去重,消费逻辑内部的状态校验反而更重要。

6.3 要不要用布隆过滤器,本地缓存能不能也掺一脚

防重key的量如果特别大,比如百万级用户签到,每个用户每天存一个key,Redis内存虽然撑得住,但监控起来不太好看。布隆过滤器可以在写入Redis之前先判断一下key是否可能存在,减少无效的Redis写入,但布隆过滤器本身有误判率且无法删除元素,用在精确去重场景要特别小心。我目前只在数据量达到千万级且允许极低误判时才考虑它,常规项目直接SETNX就够了。

另外我也试过在本地加一层Caffeine缓存做一级防抖,命中本地缓存就直接拒绝,减少对Redis的压力。这个方案适合极端热点场景,比如秒杀按钮。但要注意,本地缓存无法跨节点共享,多实例部署时可靠性下降了,只能作为前置加速,不能替代Redis判重。

6.4 这个方案的边界条件,别忘了考虑网关层已经做过的处理

有不少团队在网关层已经做了统一的API限流或者鉴权,接入Redis防重时会想是不是重复了。我的观点是,网关限流解决的是整体流量控制,比如每秒最多1000个请求;接口防重解决的是单业务请求的重复性,两者关注维度完全不同。已经做了网关限流,接口层防重依然要做。

如果网关层有SDK可以针对用户维度做频控,你也可以用它来覆盖一部分防重场景,但我还是建议在业务侧保留注解级别的防重能力。理由很简单:网关频控一般只能做到用户级或IP级,无法区分同一次业务和不同业务,而接口防重必须精确到业务参数。

这篇文章从场景到方案,从代码到踩坑,把Redis防重复提交的完整落地过程都过了一遍。如果你正在做一个接口反复出现重复数据的项目,先别急着上重型中间件,按这篇文章里的思路,用一个注解加一个切面,半天时间就能把东墙补起来。真正复杂的从来不是技术选型,而是你到底要挡哪一类重复,又愿意在一致性上做到多狠。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/22 23:29:41

手写Tool Use三套方案:从函数注册表到对话记忆与动态工具选择

1. 从面试题到工程实践&#xff1a;一次手写 Tool Use 的完整还原前阵子去某家公司面试 AI 应用开发岗&#xff0c;聊到一半&#xff0c;面试官突然把屏幕转过来&#xff0c;说&#xff1a;“别讲概念了&#xff0c;现场给我写一个 Tool Use 的调用框架&#xff0c;要求能注册工…

作者头像 李华
网站建设 2026/10/22 23:25:52

LangGraph实战:用图编排搞定复杂智能体流程

做应用层的朋友应该都有同感&#xff1a;模型本身往往不是最难的&#xff0c;真正难的是把模型塞进一条会转弯、会分叉、还会重复执行的业务流水线里。我帮某电商业务搭智能客服时&#xff0c;需求其实很朴素——用户进来先判断意图&#xff0c;查订单的走查询工具&#xff0c;…

作者头像 李华
网站建设 2026/10/22 23:25:17

SIGIR 2018论文列表精读指南:神经检索与推荐系统技术演进

1. 为什么一份五年前的会议论文列表至今仍值得逐篇精读信息检索这个圈子有个很实在的特点&#xff1a;技术迭代快&#xff0c;但底层问题的生命周期极长。你翻遍近两年的推荐系统、搜索引擎、对话式AI的工程博客&#xff0c;会发现很多核心思路的源头都能追溯到五六年前的学术会…

作者头像 李华
网站建设 2026/10/22 23:25:10

OpenCV虾苗计数实战:从图像分割到连通域统计

我第一次接到"OpenCV虾苗计数"这个需求时&#xff0c;心里第一反应是&#xff1a;这不太就是先分割再数连通域吗&#xff1f;真做起来才发现&#xff0c;越小的目标越考验细节处理。养殖场的诉求非常朴素——一批虾苗要按数量卖给下游养殖户&#xff0c;人工数既慢又…

作者头像 李华
网站建设 2026/10/22 23:23:32

智能体项目实战:从架构设计到工具调用的LLM Agent开发全记录

我最早做AI相关项目&#xff0c;是从一个很朴素的聊天机器人项目开始的。它做得不算差&#xff0c;能回答常见问题&#xff0c;能闲聊&#xff0c;也接了一些办公系统里的工单查询。但是当我尝试把它往真正的业务场景推的时候&#xff0c;问题就暴露了&#xff1a;用户提出一个…

作者头像 李华
网站建设 2026/10/22 23:22:08

全球首个AI管理体系标准ISO/IEC 42001核心逻辑与落地实践

做AI项目这几年&#xff0c;我最大的感受是&#xff1a;技术跑得太快&#xff0c;管理一直没跟上。模型精度、训练效率、推理成本这些指标大家张口就来&#xff0c;但真要问一句“这个AI系统上线之后&#xff0c;谁对它的输出负责”“训练数据里有没有偏见”“第三方模型接入后…

作者头像 李华