通过注解开发来实现下单接口的防重提交

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 通过注解开发来实现下单接口的防重提交

公众号merlinsea


  • 下单流程设计
  • 首先需要生成token,这个token作为唯一标识这个用户下的这笔订单
  • 其次携带token和订单信息创建订单并完成支付功能。


640.jpg


  • 如何设计才能更加解耦呢?
  • 第5步的过程可以通过注解切面来实现判断是否存在token的步骤下单支付的流程解耦,通过拦截器将用户携带token和订单信息的请求拦截下来,判断是否携带了token,如果是且验证成功则放行,否则不放行。


640.jpg


  • 代码实现
  • 编写自定义注解
  • 这里的自定义注解的核心作用是【标识】哪个方法需要注入切面类,其验证的类型分为方法参数类型和token类型,即有该自定义注解的方法就需要验证request-token。

         
/**
 * 自定义防重提交注解,用于标识是这次request的类型是方法参数还是token类型
 * @author lianglin
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
    /**
     * 防重提交类型:
     * 1、方法参数类型 : key=ip+method+param
     * 2、token类型:key=accountNo+token
     */
    enum Type {
        PARAM,
        TOKEN
    }
    /**
     * 默认防重提交是方法参数
     * @return
     */
    Type limitType() default Type.PARAM;
    /**
     * 加锁过期时间是5秒
     * @return
     */
    long LockTime() default 5;
}


  • 编写切面类【核心重点】
  • 切面类的核心作用就是配合自定义注解@RepeatSubmit,在有自定义注解的地方注入这个切面方法并执行
  • @PointCut代表切入点
  • @Around代表核心切入逻辑
/**
 * 定义切面类
 */
@Aspect
@Component
@Slf4j
public class RepeatSubmitAspect {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    /**
     * 定义 @Pointcut注解表达式,
     *  方式一:@annotation:当执行的方法上拥有指定的注解时生效(我们采用这)
     *  方式二:execution:一般用于指定方法的执行
     * 
     * @param repeatSubmit
     */
    @Pointcut("@annotation(repeatSubmit)")
    public void pointcutNoRepeatSubmit(RepeatSubmit repeatSubmit) {
    }
    /**
     * 环绕通知, 围绕着方法执行
     * @Around 可以用来在调用一个具体方法前和调用后来完成一些具体的任务。
     *
     * 方式一:单用 @Around("execution(* net.xdclass.controller.*.*(..))")可以
     * 方式二:用@Pointcut和@Around联合注解也可以(我们采用这个)
     *
     *
     * 两种方式
     * 方式一:加锁 固定时间内不能重复提交
     * <p>
     * 方式二:先请求获取token,这边再删除token,删除成功则是第一次提交
     *
     * @param joinPoint
     * @param noRepeatSubmit
     * @return
     * @throws Throwable
     * 所有@RepeatSubmit注解的方法都添加该环绕通知
     */
    @Around("pointcutNoRepeatSubmit(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit noRepeatSubmit) throws Throwable {
        //获取request请求
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        boolean res=false;
        String type = noRepeatSubmit.limitType().name();
        if (type.equals(RepeatSubmit.Type.PARAM.name())) {
            //方式一方法参数            TODO
        } else {
            //方式二,令牌形式
            String requestToken = request.getHeader("request-token");
            if (StringUtils.isBlank(requestToken)) {
                throw new BizException(BizCodeEnum.ORDER_CONFIRM_TOKEN_EQUAL_FAIL);
            }
            LoginUser loginUser = LoginInterceptor.threadLocal.get();
            //"order:submit:%s:%s"
            String key = String.format(RedisKey.SUBMIT_ORDER_TOKEN_KEY, loginUser.getAccountNo(),requestToken);
            /**
             * 提交表单的token key
             * 方式一:不用lua脚本获取再判断,之前是因为 key组成是 order:submit:accountNo, value是对应的token,所以需要先获取值,再判断
             * 方式二:可以直接key是 order:submit:accountNo:token,然后直接删除成功则完成
             */
            res = stringRedisTemplate.delete(key);
        }
        if (!res) {
            //删除失败,说明redis中没有这个token
            throw new BizException(BizCodeEnum.ORDER_CONFIRM_REPEAT);
        }
        System.out.println("目标方法执行前");
        //被自定义注解标识的方法继续执行
        Object object = joinPoint.proceed();
        System.out.println("目标方法执行后");
        return object;
    }
}


  • 注解方式解决防重提交问题的测试
  • 业务controller层测试接口
  • 这个接口用@RepeatSubmit()标识,spring就会在执行这个方法前先执行切面类中的around逻辑代码,根据around逻辑代码的执行结果来判断是否需要继续执行testRequestToken代码。
/**
 * 测试下单token的获取
 * @return
 */
@GetMapping("test")
@RepeatSubmit(limitType = RepeatSubmit.Type.TOKEN)
public JsonData testRequestToken(){
    return JsonData.buildSuccess("测试成功");
}
  • 测试结果
  • 不携带request-token的情况
  • 结论:下单之前必须先获取下单token,否则校验失败

640.jpg

  • 第一次携带request-token的情况
  • 结论:携带了下单token且第一次使用这个token才起作用

640.jpg

  • 第二次携带同一个token的情况  
  • 结论:多次使用同一个下单token会导致校验失败的,因此避免了重复下单的问题

640.jpg

  • 总结一下自定义注解的实现逻辑
  • 自定义注解,需要指定这个注解作用在哪些地方 即@Target的功能
  • 定义切面类
  • 通过@Aspect告诉spring这个是一个切面类并交给spring ioc容器管理。
  • 配合自定义注解指明该切面逻辑怎么执行
  • @Around是核心执行逻辑。


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
存储 缓存 NoSQL
防止订单重复提交或支付分布式锁方案设计
防止订单重复提交或支付分布式锁方案设计
786 0
|
NoSQL Java Redis
服务端如何防止订单重复支付!
如图是一个简化的下单流程,首先是提交订单,然后是支付。 支付的话,一般是走支付网关(支付中心),然后支付中心与第三方支付渠道(微信、支付宝、银联)交互。 支付成功以后,异步通知支付中心,支付中心更新自身支付订单状态,再通知业务应用,各业务再更新各自订单状态。
服务端如何防止订单重复支付!
|
3月前
|
消息中间件 设计模式 SQL
谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存
完成提交订单功能,并使用分布式事务方案,保证了订单提交的幂等性
谷粒商城笔记+踩坑(21)——提交订单。原子性验令牌+锁定库存
|
4月前
|
SQL NoSQL 前端开发
大厂如何解决订单幂等问题
本文探讨了分布式系统中接口幂等性的重要性和实现方法,特别是在防止重复下单的场景中。首先介绍了通过数据库事务处理创建订单时的原子性需求。接着分析了服务间调用时可能遇到的重复请求问题,提出每个请求需具备唯一标识,并记录处理状态以识别并阻止重复操作。具体实践包括生成全局唯一的订单ID,利用数据库主键唯一性约束来防止重复插入,以及使用Redis存储订单支付状态。此外,文章还讨论了解决ABA问题(即数据在两次检查之间被修改的问题)的方法,引入版本号机制来确保数据更新的原子性和一致性。这些技术方案不仅限于订单服务,也可广泛应用于需要实现幂等性的其他业务场景中。
|
5月前
|
前端开发 Java
支付系统20-----支付宝支付-----统一收单下单并支付页面接口----定义controller,跨域注解,统一收单下单并支付页面接口的创建,打印日志的注解
支付系统20-----支付宝支付-----统一收单下单并支付页面接口----定义controller,跨域注解,统一收单下单并支付页面接口的创建,打印日志的注解
|
设计模式 SQL 数据库
淘东电商项目(61) -聚合支付(基于模板方法设计模式管理支付回调)
淘东电商项目(61) -聚合支付(基于模板方法设计模式管理支付回调)
89 0
|
负载均衡 Dubbo NoSQL
Dubbo分布式服务接口的幂等性防止重复扣款
Dubbo分布式服务接口的幂等性防止重复扣款
217 0
|
7月前
|
缓存
什么情景与接口需要做幂等
什么情景与接口需要做幂等
74 0
|
7月前
|
NoSQL Java API
SpringBoot项目中防止表单重复提交的两种方法(自定义注解解决API接口幂等设计和重定向)
SpringBoot项目中防止表单重复提交的两种方法(自定义注解解决API接口幂等设计和重定向)
561 0