背景:
今天在之前的系统中碰到一个以往见过的面试题的实际使用情况,背景是这样子:
我们之前做的电商小程序,最近用户下单的时候出现了一个问题,从商城选了一款iPhone 16 Pro下单,创建订单支付金额后发现自己的订单列表的订单还是待支付(订单列表默认显示待支付状态的),上午的时候就来向客户反馈了,客户下午来找到我,说了情况。
我看下了后台发现这个用户订单的已经支付过了,没看到待支付的那个单子(因为系统有调度任务定时清理未支付的订单,用户上午的问题客户下午才来找,后台看不到要去数据库或者日志看),上k8s找了下当时的日志,发现确实同时创建了1、2两个订单,服务器时间只相差零点几秒左右,是前端同时调用了两次创建订单的接口,一开始我想着可能是前端页面没做控制,同一时间多次点击下单按钮就会多次调用接口。
于是我就给前端同事打电话说了一下情况,调整了一下前端的代码,完事之后测试了一下还是会出现这种情况。我们就商讨了一下,是否需要后端也加一下校验,防止订单重复提交,但是一时间又不知道从何下手,用户的参数相同时那就是买了一个iPhone 16 Pro然后又想再买一个iPhone 16 Pro的情况也不是没有,如何在参数上做校验。恍然间我想起来了以前刷面试题的时候有遇到一个幂等性的问题,好像就是防止接口重复调用的吧,但是具体怎么搞就不知道了,然后去刷DS。
1.幂等性
幂等性:
理论:
是数学和计算机科学中的一个重要概念,指同一操作多次执行与单次执行的效果一致。
实际:
在高并发的电商场景下,用户可能会因为网络延迟或其他原因多次点击提交按钮,导致生成重复订单(携带相同参数短时间内重复调用接口)。
2.实现方法
唯一标识:客户端生成唯一请求 ID(如 UUID),后端校验该 ID 是否已处理过。若已存在,则拒绝请求;若不存在,则处理请求并存储 ID。
状态机:确保业务操作仅在特定状态下执行,如订单状态从“待支付”到“已支付”仅允许一次变更。
Token机制:客户端预获取Token,服务端验证后立即失效,避免重复提交。
分布式锁:通过 Redis 的 SETNX(SET if Not eXists)命令,为每个订单生成一个分布式锁。处理订单时,只有成功获取锁的请求才能继续执行,防止重复提交。
数据库唯一约束:在数据库订单表中设置唯一索引(如 用户ID + 商品ID + 订单号),当重复提交订单时,数据库会抛出唯一约束异常,后端捕获异常并拒绝重复请求。
3.确定方案
最后确定了下方案,因为我们的电商小程序只是分布式部署,QPS极低(低的可怜,理论上讲我们的所有用户同时去下单都不会达到高并发的标准,QPS达到1000以上,没错,基本没什么用户),所以使用唯一标识方法就足够了。
前端在进入创建订单页面时生成一个唯一的 uniqueId,用于标识本次订单创建请求;
第一次请求到达后端时,后端将 uniqueId 存入 Redis,并设置一定有效期(如5分钟);
后续的请求中,后端先验证 uniqueId 是否已存在于 Redis 中,存在:表示是重复请求,直接拒绝;不存在:表示是新请求,继续处理订单业务逻辑,并将 uniqueId 存入 Redis。
由此结合了幂等性校验和缓存验证,在代码中需要先进行幂等性处理,再去进行业务校验。为什么,因为幂等性校验的核心是确保同一请求不会重复执行业务逻辑,避免重复提交、重复支付的问题。
4.幂等性核心
其核心思想是:
提前拦截重复请求:如果请求是重复的,直接返回结果(如“请勿重复操作”),就不用进入后续的业务流程了。
节省资源:避免重复的数据库操作、业务逻辑处理或第三方接口调用,减少系统消耗。
5.实例代码
我项目的真实代码,已部署!!!
//校验创建订单请求的唯一id 接口幂等性处理
String uniqueId = bo.getUniqueId();
if (StringUtils.isNotBlank(uniqueId)) {
if (redisService.hasKey(RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId)){
throw new BizException(BizErrorCodeEnum.ORDER_UNIQUEID_EXIST);
}
String uniqueIdKey = RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId;
redisService.set(uniqueIdKey, uniqueId, loginTimeOut);
}
//业务校验
//业务校验代码
不过这里建议是优化一下redis操作,由于Redis 的 hasKey() + set() 是两个独立操作,存在并发请求时可能都会通过验证的问题,使用 Lua 脚本可以确保校验和写入的原子性。
// Lua 脚本:如果 key 不存在,则设置值并返回 true,否则返回 false
String script = "if redis.call('exists', KEYS[1]) == 0 then " +
"redis.call('setex', KEYS[1], ARGV[2], ARGV[1]); " +
"return 1; else return 0 end";
// 执行 Lua 脚本
String uniqueId = bo.getUniqueId();
Long result = redisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(RedisKeyConstants.SHOP_ORDER_UNIQUEIDKEY_PREFIX + uniqueId),
"1", 300 // 300 秒 = 5 分钟
);
if (result == 0L) {
throw new BusinessException("请勿重复提交订单");
}
那为什么我项目的代码没有这样写呢 ,xianzaidouxiewanleyihouganshenme (拼音不是乱码)?
果然经验不是靠八股文背出来的,还得亲身从项目中体会、实践。