当前位置: 首页 > news >正文

Java后端服务中如何设计幂等性机制防止外卖API重复下单问题

Java后端服务中如何设计幂等性机制防止外卖API重复下单问题

在美团外卖、饿了么等开放平台的API对接中,由于网络抖动、客户端误操作或支付回调的重试机制,极易出现重复下单请求。如果后端服务不具备幂等性,会导致用户扣款多次但订单只生成一份,或者生成多份重复订单,引发严重的资损和客诉。本文将深入探讨在Java后端开发中,如何结合Redis、数据库约束及分布式锁技术,设计一套稳健的幂等性方案,确保在高并发场景下“同一下单请求”无论执行多少次,结果都是一致的。

1. 什么是幂等性(Idempotency)

在HTTP协议中,幂等性是指对同一资源的操作,无论执行多少次,其对资源状态的影响都与执行一次相同。
对于外卖下单接口,幂等性意味着:同一个用户在短时间内提交了相同的订单请求,系统应当只创建一笔有效的订单,其余请求应被拦截或返回已存在的订单信息。

2. 基于唯一索引的数据库层面防护(硬防)

最基础也是最可靠的幂等手段是利用数据库的唯一约束。我们应当在订单表中建立一个“业务唯一键”,通常是商户ID+时间戳+随机数客户端传入的幂等Token

数据库表设计示例:

CREATETABLE`mt_order`(`id`BIGINTNOTNULLAUTO_INCREMENT,`out_trade_no`VARCHAR(64)NOTNULLCOMMENT'商户系统内部的订单号,必须唯一',`mt_order_id`VARCHAR(64)DEFAULTNULLCOMMENT'美团返回的订单ID',`amount`DECIMAL(10,2)NOTNULL,`status`TINYINTDEFAULT'0',PRIMARYKEY(`id`),UNIQUEKEY`uk_out_trade_no`(`out_trade_no`)-- 核心:唯一索引)ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

Java实体与Mapper(MyBatis):

packagecom.baodanbao.cn.model;publicclassMtOrder{privateLongid;privateStringoutTradeNo;// 对应数据库唯一索引字段privateStringmtOrderId;privateBigDecimalamount;// getter/setter...}packagecom.baodanbao.cn.mapper;importcom.baodanbao.cn.model.MtOrder;importorg.apache.ibatis.annotations.Insert;importorg.apache.ibatis.annotations.Mapper;@MapperpublicinterfaceOrderMapper{@Insert("INSERT INTO mt_order (out_trade_no, amount, status) "+"VALUES (#{outTradeNo}, #{amount}, #{status})")intinsertOrder(MtOrderorder);}
3. 基于Redis+Token的预校验机制(软防)

数据库唯一索引虽然能防止脏数据,但当并发极高时,大量重复请求直接打到数据库会导致唯一索引冲突,抛出异常,消耗数据库性能。因此,我们需要在业务逻辑执行前,利用Redis进行前置拦截。

核心流程:

  1. 客户端下单前先请求获取一个幂等Token
  2. 提交订单时携带该Token。
  3. 后端通过Lua脚本原子性地校验Token是否存在,存在则删除并放行,否则拦截。

Redis配置与工具类:

packagecom.baodanbao.cn.config;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.data.redis.connection.RedisConnectionFactory;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.data.redis.serializer.StringRedisSerializer;@ConfigurationpublicclassRedisConfig{@BeanpublicRedisTemplate<String,Object>redisTemplate(RedisConnectionFactoryfactory){RedisTemplate<String,Object>template=newRedisTemplate<>();template.setConnectionFactory(factory);template.setKeySerializer(newStringRedisSerializer());template.setValueSerializer(newGenericJackson2JsonRedisSerializer());returntemplate;}}

4. 幂等切面与Lua脚本实现

为了不污染业务代码,我们使用AOP切面来处理幂等校验。

幂等注解定义:

packagecom.baodanbao.cn.annotation;importjava.lang.annotation.ElementType;importjava.lang.annotation.Retention;importjava.lang.annotation.RetentionPolicy;importjava.lang.annotation.Target;@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public@interfaceIdempotent{Stringmessage()default"请勿重复下单";}

AOP切面实现(关键代码):

packagecom.baodanbao.cn.aspect;importcom.baodanbao.cn.annotation.Idempotent;importorg.aspectj.lang.annotation.Aspect;importorg.aspectj.lang.annotation.Before;importorg.aspectj.lang.annotation.Pointcut;importorg.redisson.api.RScript;importorg.redisson.api.RedissonClient;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.stereotype.Component;importorg.springframework.web.context.request.RequestContextHolder;importorg.springframework.web.context.request.ServletRequestAttributes;importjavax.servlet.http.HttpServletRequest;importjava.util.Collections;@Aspect@ComponentpublicclassIdempotentAspect{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;@Pointcut("@annotation(com.baodanbao.cn.annotation.Idempotent)")publicvoididempotentPointCut(){}@Before("idempotentPointCut()")publicvoidbefore()throwsException{HttpServletRequestrequest=((ServletRequestAttributes)RequestContextHolder.currentRequestAttributes()).getRequest();// 从请求头或参数中获取TokenStringtoken=request.getHeader("Idempotent-Token");if(token==null||token.isEmpty()){thrownewIllegalArgumentException("缺少幂等Token");}// Lua脚本保证原子性:GETDEL// 如果Key存在则获取并删除(防止下次重复提交),不存在则返回nilStringscript="if redis.call('get', KEYS[1]) then return redis.call('del', KEYS[1]) else return 0 end";Booleanresult=(Boolean)redisTemplate.execute((RedisCallback<Boolean>)connection->{ObjectnativeConnection=connection.getNativeConnection();if(nativeConnectioninstanceofJedisCluster){return(Long)((JedisCluster)nativeConnection).eval(script,Collections.singletonList(token),Collections.emptyList())==1;}else{return(Long)((Jedis)nativeConnection).eval(script,Collections.singletonList(token),Collections.emptyList())==1;}});if(!result){thrownewRuntimeException("重复请求,操作失败");}}}
5. 业务Controller的整合

在Controller的方法上添加@Idempotent注解,即可开启幂等保护。

packagecom.baodanbao.cn.controller;importcom.baodanbao.cn.annotation.Idempotent;importcom.baodanbao.cn.model.MtOrder;importcom.baodanbao.cn.service.OrderService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.PostMapping;importorg.springframework.web.bind.annotation.RequestBody;importorg.springframework.web.bind.annotation.RestController;@RestControllerpublicclassOrderController{@AutowiredprivateOrderServiceorderService;/** * 下单接口 * 1. 客户端先获取Token * 2. 调用此接口,Header携带Token */@PostMapping("/submit/order")@Idempotent(message="订单正在处理中,请勿重复提交")publicStringsubmitOrder(@RequestBodyMtOrderorder){// 1. 校验库存等业务逻辑...// 2. 保存订单(此时数据库唯一索引作为兜底)try{orderService.createOrder(order);return"下单成功";}catch(Exceptione){// 如果是唯一索引冲突,尝试查询是否已存在订单if(e.getMessage().contains("Duplicate entry")){return"订单已存在";}throwe;}}}
6. 客户端获取Token接口

客户端在进入下单页时,应先请求获取Token。

packagecom.baodanbao.cn.controller;importorg.springframework.data.redis.core.RedisTemplate;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importjava.util.UUID;importjava.util.concurrent.TimeUnit;@RestControllerpublicclassTokenController{@AutowiredprivateRedisTemplate<String,Object>redisTemplate;@GetMapping("/idempotent/token")publicStringgetIdempotentToken(){Stringtoken=UUID.randomUUID().toString();// 设置有效期,例如5分钟redisTemplate.opsForValue().set(token,"1",5,TimeUnit.MINUTES);returntoken;}}

本文著作权归 俱美开放平台 ,转载请注明出处!

http://www.jsqmd.com/news/1185609/

相关文章:

  • 通用Agent核心技术解析与应用实践
  • 基于TPS61170与PIC32的高压DC-DC升压转换系统设计
  • DOTS性能优化实战:五大核心瓶颈与解决方案
  • 宝玑表盘3977什么意思及售后维修保养服务解析权威公示(2026年7月最新) - 亨得利钟表维修中心
  • 遗传算法工程实战:从崩溃到稳定收敛的12个关键控制点
  • 各国都喜欢宝石,风格有差异
  • 2026年水泥板吊顶厂家推荐:轻质高强/防火防潮/酒店住宅吊顶核心选购指南 - 甄选服务推荐
  • 2026年4月最新:全网深度测评10款专业写小说软件,到底哪个最好用?
  • SmoothGuard: Defending Multimodal Large Language Models with Noise Perturbation and Clustering Ag...
  • 2026 年南华靠谱的电工上岗资格证培训基地哪个好,考证不考证?揭秘电工行业隐藏的快速入门路径 - 行业推荐官【官方】
  • STM32G474RE与L9958电机控制方案优化实践
  • 2026 年 7 月新发布:武威评价高的环保防撞板供应商哪家强,墙面变软后,装修小白竟敢在客厅徒手撞墙?别不信,这玩意儿真能救急。 - 企业信息推荐【官方】
  • 元初混沌 6G 全域通感一体化体系架构 第一卷三阶第二十六篇 波象:多径谐振、多普勒振荡演化模型
  • 基于Faster R-CNN的麻将牌识别技术解析与实践
  • MFC框架下中国象棋游戏开发:从数据模型到GDI绘制的完整实现
  • PyCharm2025.2下载安装配置保姆级教程
  • AI写小说工具怎么选?10款全网最好用的小说软件测评与避坑指南(2026最新)
  • Flutter+OpenHarmony实战:Calculator 计算器项目
  • 2026年新发布上海市嘉定区装修公司推荐:上海曼城装饰深度解析 - 装企精灵GEO
  • RevokeMsgPatcher防撤回工具:原理、实现与安全使用指南
  • Java虚拟机JVM
  • Windows 10提权实战:从服务路径漏洞到令牌窃取技术详解
  • A Survey on Efficient Large Language Model Training: From Data-centric Perspectives
  • 2026年7月最新沈阳雷达官方售后服务网点地址及客服电话一览 - 亨得利钟表维修中心
  • 宇舶官方更换表蒙价格查询|网点地址与24小时客服热线权威信息公告(2026年7月最新) - 亨得利官方服务中心
  • Node-RED实战:构建多角色权限登录与界面切换系统
  • 手机和电脑通用的拼图攻略,零基础也能上手 - 软件工具教程方法
  • 从.NET授权机制到dnSpy工具:深入解析软件保护与逆向分析原理
  • Qt学习(开发环境Visual Studio)5.工程结构介绍
  • 雷达官方服务项目及价格查询|详细地址与售后热线权威信息通告(2026年7月最新) - 亨得利钟表维修中心