接口的幂等性原则

1、接口调用存在的问题

现如今我们的系统大多拆分为分布式SOA,或者微服务,一套系统中包含了多个子系统服务,而一个子系统服务往往会去调用另一个服务,而服务调用服务无非就是使用RPC通信或者restful,既然是通信,那么就有可能在服务器处理完毕后返回结果的时候挂掉,这个时候用户端发现很久没有反应,那么就会多次点击按钮,这样请求有多次,那么处理数据的结果是否要统一呢?那是肯定的!尤其在支付场景。

2、什么是接口幂等性

接口幂等性就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额返发现多扣钱了,流水记录也变成了两条,这就没有保证接口的幂等性。

代码实现

自定义注解

用于拦截请求该服务的方法上

package com.asiainfo.annotation;

import org.apache.commons.lang.StringUtils;

import java.lang.annotation.*;

/**
 * @program: cust-client-parent
 * @description
 * @author: zhangds5
 * @create: 2020-06-17 09:47
 **/
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotence {
    String desc() default StringUtils.EMPTY;
}

定义拦截器

package com.asiainfo.Interceptor;

import com.ai.bss.custcommon.util.StringUtilEX;
import com.asiainfo.Idempotence.impl.RedisIdempotenceImpl;
import com.asiainfo.annotation.Idempotence;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @program: cust-client-parent
 * @description
 * @author: zhangds5
 * @create: 2020-06-17 09:51
 **/

public class IdempotenceInterceptor extends HandlerInterceptorAdapter {

    @Autowired
    private RedisIdempotenceImpl redisIdempotence;

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        super.afterCompletion(request, response, handler, ex);
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        super.postHandle(request, response, handler, modelAndView);
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        HandlerMethod handlerMethod = (HandlerMethod) handler;

        // 获取方法注解
        Idempotence methodAnnotation = handlerMethod.getMethodAnnotation(Idempotence.class);

        // 处理带有Idempotence注解的方法
        if(!StringUtilEX.isNull(methodAnnotation)){
            String token = request.getHeader("token");
          	// 检查Redis有无此token
            boolean check = redisIdempotence.checkToken(token);
          	// 如果没有返回错误状态码
            if(!check){
                response.sendError(HttpServletResponse.SC_NOT_ACCEPTABLE);
                response.flushBuffer();
            }
          	// 删除token
            redisIdempotence.delToken(token);
        }
				// 继续执行
        return super.preHandle(request, response, handler);
    }
}

注册拦截器

<bean id="idempotenceAspect" class="com.asiainfo.Interceptor.IdempotenceInterceptor"></bean>

<mvc:interceptors>
   <mvc:interceptor>
     <mvc:mapping path="/**"/>
     <ref bean="idempotenceAspect"/>
   </mvc:interceptor>
</mvc:interceptors>

定义操作token抽象接口

package com.asiainfo.Idempotence;

/**
 * @program: cust-client-parent
 * @description
 * @author: zhangds5
 * @create: 2020-06-17 10:44
 **/
public interface IdempotenceAbstract {

    /**
     * 获取token
     * @return
     * @throws Exception
     */
    String getToken()throws Exception;

    /**
     * 检查token
     * @param token
     * @return
     * @throws Exception
     */
    boolean checkToken(String token)throws Exception;

    /**
     * 删除token
     * @param token
     * @throws Exception
     */
    void delToken(String token)throws Exception;
}

Redis方式实现

package com.asiainfo.Idempotence.impl;

import com.asiainfo.Idempotence.IdempotenceAbstract;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.UUID;

/**
 * @program: cust-client-parent
 * @description
 * @author: admin
 * @create: 2020-06-17 10:49
 **/
public class RedisIdempotenceImpl implements IdempotenceAbstract {

    private JedisPool redisPool;

    @Override
    public String getToken() throws Exception {
        String token = UUID.randomUUID().toString().replaceAll("-", "");
        Jedis resource = redisPool.getResource();
        resource.set(token, token);
        resource.close();
        return token;
    }

    @Override
    public boolean checkToken(String token) throws Exception {
        Jedis resource = redisPool.getResource();
        Boolean exists = resource.exists(token);
        resource.close();
        return exists;
    }

    @Override
    public void delToken(String token) throws Exception {
        Jedis resource = redisPool.getResource();
        resource.del(token);
        resource.close();
    }

    public void setRedisPool(JedisPool jedisPool) {
        this.redisPool = jedisPool;
    }
}

集成Redis

Redis.properties

#最大分配的对象数
redis.pool.maxActive=200
#最大能够保持idel状态的对象数
redis.pool.maxIdle=50
redis.pool.minIdle=10
redis.pool.maxWaitMillis=20000
#当池内没有返回对象时,最大等待时间
redis.pool.maxWait=300

redis.host = 192.168.228.56
redis.port = 6379
redis.timeout=30000
redis.password = Wc123456
redis.database = 0

application.xml

<context:property-placeholder location="classpath:application.properties" ignore-unresolvable="true" />

<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
  <property name="maxTotal">
    <value>${redis.pool.maxActive}</value>
  </property>
  <property name="maxIdle">
    <value>${redis.pool.maxIdle}</value>
  </property>
  <property name="testOnBorrow" value="true"/>
  <property name="testOnReturn" value="true"/>
</bean>

<bean id="jedisPool" class="redis.clients.jedis.JedisPool">
  <constructor-arg name ="poolConfig" ref="jedisPoolConfig" />
  <constructor-arg name ="host" value="${redis.host}" />
  <constructor-arg name ="port" value="${redis.port}" type="int" />
  <constructor-arg name ="timeout" value="${redis.timeout}" type="int" />
  <constructor-arg name ="password" value="${redis.password}" />
  <constructor-arg name ="database" value="0" type="int" />
</bean>

<bean id="redisIdempotenceImpl" class="com.asiainfo.Idempotence.impl.RedisIdempotenceImpl">
	<property name="redisPool" ref="jedisPool"/>
</bean>

总结

为需要保证幂等性的每一次请求创建一个唯一标识token, 先获取token, 并将此token存入Redis, 请求接口时, 将此token放到header或者作为请求参数请求接口, 后端接口判断Redis中是否存在此token,如果存在, 正常处理业务逻辑, 并从Redis中删除此token, 那么, 如果是重复请求, 由于token已被删除, 则不能通过校验, 返回请勿重复操作提示,如果不存在, 说明参数不合法或者是重复请求, 返回提示即可。