【BUG记录】防止记录重复提交方案
这是一个很经典的后端开发问题。防止重复提交的核心思路是在服务端识别并拦截短时间内相同的请求。下面我给你梳理几种主流且实用的方案按推荐程度排序。方案一Token 令牌机制最推荐防重最彻底前端发起请求前先去后端获取一个一次性Token后端保存Token用Redis。请求时带上Token后端处理请求后立即删除Token。原理Token一次性有效能有效防止因网络延迟、用户狂点导致的多笔相同业务如支付、下单生成。适用所有写操作尤其涉及金额、库存、订单等核心业务。步骤用户访问表单页后端生成Token存入Redis设短期过期返回给前端。前端提交时携带这个Token。后端检查Redis中Token是否存在存在则合法删除Token并执行业务不存在或不匹配则拦截。方案二基于 Redis 的幂等性校验最常用高性能利用Redis的原子性操作比如SETNX命令只在键不存在时设置来判断请求是否重复。原理用唯一业务标识如用户ID订单ID作为Key存入Redis处理完成后删除Key。适用于需要高并发且能接受短暂状态存储的场景。步骤根据请求的关键参数比如用户ID 订单ID生成唯一Key。使用SET key value NX EX 5NX表示不存在时才设置EX 5表示5秒过期。设置成功则执行业务业务完成后可立即删除Key设置失败则说明是重复提交。方案三分布式锁适合分布式环境当你的服务部署在多个节点时用基于Redis或ZooKeeper的分布式锁。原理每个请求尝试获取同一个资源的锁获取不到则被丢弃。这比Redis幂等性更侧重解决“并发修改”问题。实现Redis锁用SETNXkey value配合Lua脚本释放锁确保原子性。Redisson提供了封装好的可重入锁使用简单。方案四数据库唯一约束最后一道防线绝对防重在数据库层面为关键字段组合如订单号设置唯一索引。原理任何事务都脱不开底层存储这是防止数据重复的终极保障。步骤创建表时对能唯一标识业务记录的字段或字段组合添加UNIQUE KEY。代码执行INSERT时捕获DuplicateKeyException异常将其视为重复提交提示用户。方案五前端控制用户可见级别但不能依赖在按钮点击后立即置灰或显示“提交中”的Loading状态。局限性这只防君子不防小人。用户刷新页面、用脚本发起请求前端控制就失效了。必须和后端方案配合使用。不同场景下的选择建议场景推荐方案核心交易无状态服务如支付、订单Token 数据库唯一约束最安全可靠高并发耗时1-3秒如秒杀、抢购Redis SETNX 前端Loading性能极高分布式集群部署多台服务器分布式锁如Redisson 全局请求ID低并发中小项目AOP切面 缓存如Caffeine本地缓存一个典型的高并发防重组合策略推荐架构前端点击后立即置灰按钮加上Loading动画。网关/过滤器解析请求中的requestId如UUIDtimestamp若Redis中requestId已存在则直接返回“重复提交”否则将requestId存入Redis并设30秒过期。拦截到业务层对核心资源如订单号加分布式锁或Redis SETNX。数据库为关键字段如订单表的主键或单号设置唯一索引兜底保护。一个简单的代码示例基于 Redis AOPTarget(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface NoRepeatSubmit { int expireSeconds() default 5; // 锁的过期时间 } // 切面实现 Around(annotation(noRepeatSubmit)) public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable { String key repeat:submit: getRequestUniqueKey(); // 比如用户ID 请求URL 参数MD5 // Redis SETNX操作设置成功后老的时间为expireSeconds秒 Boolean success redisTemplate.opsForValue().setIfAbsent(key, 1, Duration.ofSeconds(noRepeatSubmit.expireSeconds())); if (success null || !success) { throw new RepeatSubmitException(请勿重复提交); } try { return joinPoint.proceed(); } finally { // 业务执行完后可以选择删除key或者让它自然过期 // 注意对于耗时很短的业务可以立刻删除否则让其自动过期即可 // redisTemplate.delete(key); } }容易踩的坑锁的粒度太大对用户ID加锁可能导致该用户的所有请求被串行化。建议使用用户ID 业务类型 关键参数的组合。没有兜底方案只依赖Redis。Redis挂了会导致请求无法提交或全部放行。务必加上数据库唯一索引作为最后防线。业务执行时间超过锁的过期时间业务还没处理完锁自动释放了导致下一个请求进来看不到锁又提交了一次。解决方案使用看门狗Watch Dog机制如Redisson的tryLock会自动续期。或根据业务平均耗时将锁过期时间设为足够长如15-30秒。总结一句话后端用Redis做快速拦截数据库唯一索引进最后兜底前端做按钮防抖提升用户体验。根据业务重要性选择组合即可。