接口幂等性——项目中实际运用

前言

接口幂等性的定义是:多次调用接口方法要保证和单次调用的结果一致。
针对开发中主要的增删改查来说,查询删除操作天然具有幂等性,我们主要关心的就是新增更新操作。其中更新操作存在幂等性(用户信息修改–重复提交,数据库的值也都是一样的)和非幂等性(商品支付–重复提交,会导致用户账户扣除多笔金额)的情况,要根据具体业务来看。以下内容都是基于新增操作接口来实现接口幂等性。

幂等性的使用场景

在实际项目开发的过程中,以下几种场景会造成非幂等的情况。

1,前端重复提交

这个场景在平时开发工作中非常多见,比如创建用户的时候,前端没有控制创建按钮不能重复点击,导致多次重复请求提交到后端。

2,接口超时重试

我们系统前端发送请求后,可能因为网络问题,请求了5s后还没有收到后端返回的结果,则会进行重试请求,一共进行3次重试,则可能会导致多次重复请求提交到后端。

3,消息重复消费

系统引入MQ后,MQ生产端和消费端都有重试机制,也就是同一消息会被重复消费。

保证幂等性的几种方式

1. 基于唯一索引。

一般是添加业务ID作为唯一索引,如果插入相同业务ID就直接报错。但在实际项目开发中,很多业务表都是逻辑删除,业务ID不能作为唯一索引。在这种情况下想要使用唯一索引的话,需要在数据表中有修改时间或者删除时间的字段,然后使用业务ID和时间字段做联合索引确保唯一性。此方法适用于系统中本身修改删除的业务逻辑中就有时间记录,不需要额外改造逻辑,而且因为是报错只适合作为兜底措施。

2. 基于分布式锁。

分布式锁去实现的话,每次请求过来就,先去尝试获取分布式锁,如果获得成功,就直接执行业务逻辑,获取失败的,就舍弃请求。可以使用redis去实现分布式锁。

3. 基于Token。

一共分为两种方式。
第一步,业务接口请求前,客户端先去请求获取token,服务端生成token返回。
第二步,客户端带着token去请求服务端的业务接口,服务端首先结合Redis进行判断,一共三种情况。
(1)如果Redis存在此token并且value值不是初始值,直接获取Redis中的value值并返回。
(2)如果Redis存在此token并且value值是初始值,则暂停一秒再去获取Redis中的value值,如果发现value值依然是初始值,则抛弃本次请求,否则直接返回。
(3)Redis无此token,保存token与初始化值到Redis中。
如果没有返回结果,则会继续执行接口,将结果存入Redis之前,再去验证Redis中是否存在token与初始值,如果初始值已经被覆盖为了返回值,则通过唯一性索引(如果使用此机制,不会触发该判断,会在重复执行业务时抛出异常)或者触发事务回滚来保证幂等性。
在这里插入图片描述
下图这种方式和上图主要区别是客户端生成唯一token,然后接口发起重试请求的时候也是保持同一token,其他地方一致。
在这里插入图片描述
具体实现是由AOP切面 + 注解来先实现的,代码如下。
(1)幂等性注解定义

/**
 * @author hizoo
 */
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface MyIdempotent {
	//以下属性按需使用,可自行添加
    /**
     * 超过请求幂等性提示信息
     */
    String message() default "请勿重复提交";

    /**
     * 该请求的唯一key
     */
    String key() default "";

    /**
     * key的过期时间 等于请求最小间隔市场
     */
    long expireTime() default 20L;

}

(2)幂等性功能切面逻辑实现

/**
 * @author hizoo
 */
@Aspect
@Component
public class MyIdempotentAspect {
    @Autowired
    private RedisUtils redisUtils;

    /**
     * 接口首次调用存入的默认值
     */
    private String INIT_VALUE = "init";

    @Pointcut("@annotation(com.xxx.xxx.annotation.MyIdempotent)")
    public void myPoint() {

    }

    @Around(value = "myPoint()")
    public Object apiIdempotentCheck(ProceedingJoinPoint pjp) throws Throwable {
        //获取头文件中的uuid作为key(前端生成的)
        String token = RequestUtils.getUuid();

		//获取接口上的自定义幂等性注解
        Signature signature = pjp.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        Method method = methodSignature.getMethod();
        MyIdempotent annotation = method.getAnnotation(MyIdempotent.class);

        //获取接口间隔时间
        long expireTime = annotation.expireTime();

        //无注解直接放行
        if (annotation == null) {
            return pjp.proceed();
        }
        //如果redis存在此token并且value值不是初始值,直接获取redis中的value值返回。
        if (redisUtils.hasKey(token) && !INIT_VALUE.equals(redisUtils.get(token))) {
            ResultInfo resultInfo = JSON.parseObject(redisUtils.get(token).toString(), ResultInfo.class);
            return resultInfo;
        }
        //如果redis存在此token且是初始值,则暂停一秒再去获取redis中的value值。
        else if (redisUtils.hasKey(token) && INIT_VALUE.equals(redisUtils.get(token))) {
            //这个休眠时间可以根据项目中接口的实际情况进行设置调整
            sleep(1000);
            if(redisUtils.hasKey(token) && !INIT_VALUE.equals(redisUtils.get(token))){
                ResultInfo resultInfo = JSON.parseObject(redisUtils.get(token).toString(), ResultInfo.class);
                return resultInfo;
            }
            return null;
        } else {
            redisUtils.set(token, INIT_VALUE, expireTime, TimeUnit.SECONDS);
        }

        //接口执行
        Object proceed = pjp.proceed();
        if(redisUtils.hasKey(token) && INIT_VALUE.equals(redisUtils.get(token))) {
            redisUtils.set(token, JSON.toJSONString(proceed), expireTime, TimeUnit.SECONDS);
        }
        else{
            throw new OverseasException("数据插入失败!");
        }
        
        return proceed;
    }
}

(3)新增接口上使用幂等性注解

    /**
     * 新增
     */
    @RequestMapping(method = RequestMethod.POST, path = "/create")
    @MyIdempotent
    @Transactional
    public ResultInfo create(@RequestBody DataInfo dataInfo) {
        return dataInfoService.insert(dataInfo);
    }

小结

使用接口幂等性处理后,重复数据入库这一问题基本得到解决。但是我这个项目属于老系统改造,无法在所有模块涉及的表都去添加唯一性索引做兜底措施,使用的事务机制。所以在实际项目运行过程中,遇到比较极端的网络波动情况,仍然可能会有重复数据入库。欢迎大家讨论分析,提出更好的解决方案,相互学习,共同进步!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值