目录
在面试中,经常会遇到一些实际场景的问题,比如当网络出现阻塞时,用户不断点击下单按钮导致重复下单的情况。本文将详细介绍这种场景下的问题分析及解决方案。
一、普通下单流程
- 用户发起下单操作,订单服务保存一条订单信息,此时订单状态为未支付。
- 用户确认订单,订单状态从未支付改为待支付。
- 用户选择支付方式进行钱包支付,此步骤由第三方支付平台负责。
- 支付完成后,支付平台回调到支付服务,支付服务再调用订单服务将订单状态修改为已支付。
二、幂等性问题分析
真正会出现重复下单和幂等性问题的是用户下单这一步。如果在网络阻塞时用户多次点击下单按钮,订单服务会生成很多条订单信息,这不仅造成资源浪费,还会影响用户体验,因为用户会莫名其妙地发现出现了很多未支付的订单信息。
三、解决方案
(一)前端限制
当用户点击了第一次下单按钮后,将按钮置为不可用,防止用户无意点击多次。但这种方式只能防君子,不能防小人。
(二)后端限制
采用 Redis 提供的 C 的 NX(SETNX)来保证唯一幂等性。
- 当用户进行立即购买下单时,可以通过当前用户唯一标识(如登录后的 token)结合当前商品的 URL 以及一个表示重复下单的 key(如 “recommit”)作为 key,调用 SETNX 进行存储。如果 value 无值则返回 true 并保存成功,若有值则返回 false。
- 同时需要设置过期时间,比如三秒或五秒。因为在这个时间内,用户基本上不可能有这么快的速度重复购买商品,如果重复购买,很可能是恶意的,可以排除掉。
以下是一个通用的防止重复提交的示例代码:
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Pointcut("@annotation(com.example.RepeatSubmit)")
public void repeatSubmitPointcut() {}
@Around("repeatSubmitPointcut()")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取注解
RepeatSubmit repeatSubmit = joinPoint.getTarget().getClass().getAnnotation(RepeatSubmit.class);
if (repeatSubmit == null) {
return joinPoint.proceed();
}
// 获取用户唯一标识,这里假设用户已登录且有 token
String token = "user_token";
// 获取当前 URL 和参数等
String url = joinPoint.getSignature().toShortString();
// 生成唯一 key
String key = token + url + repeatSubmit.key();
// 调用 SETNX
Boolean result = redisTemplate.opsForValue().setIfAbsent(key, UUID.randomUUID().toString(), repeatSubmit.expireSeconds(), repeatSubmit.timeUnit());
if (result!= null && result) {
return joinPoint.proceed();
} else {
// 重复提交的处理,可以抛出异常或记录日志
return null;
}
}
}
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RepeatSubmit {
String key();
int expireSeconds();
TimeUnit timeUnit();
}
需要注意的是,这里只是保证了防重复提交,在下单的过程当中还会有一系列的线程不安全问题,比如重复扣减库存、网络不稳定导致支付失败等。如果对这些问题感兴趣,可以通过作者主页的联系方式进行学习。
3461

被折叠的 条评论
为什么被折叠?



