SpringBoot 之使用 Redis 实现接口幂等性

幂等概念

在实际的开发项目中,一个对外暴露的接口往往会面临,瞬间大量的重复的请求提交,如果想过滤掉重复请求造成对业务的伤害,那就需要实现幂等!

幂等的概念:
幂等性,就是一个接口, 多次发起同一个请求,,必须保证操作只能执行一次,最终的含义就是对数据库的影响只能是一次性的,不能重复处理。
比如:

  • 订单接口,不能多次创建订单。
  • 支付接口,重复支付同一笔订单只能扣一次钱。
  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调。
  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次。

常见解决方案:

  • 唯一索引 – 防止新增脏数据
  • token机制 – 防止页面重复提交
  • 悲观锁 – 获取数据的时候加锁(锁表或锁行)
  • 乐观锁 – 基于版本号version实现, 在更新数据那一刻校验数据
  • 分布式锁 – redis(jedis、redisson)或zookeeper实现
  • 状态机 – 状态变更, 更新数据时判断状态

本文实现

通过Redis+Token机制实现接口幂等性校验。
原理图:
SpringBoot 之使用 Redis 实现接口幂等性_第1张图片

实现思路

为需要保证幂等性,每一次请求创建一个唯一标识token,先获取token,并将此token存入Redis,请求接口时,将此token放到header或者作为请求参数请求接口,后端接口判断Redis中是否存在此token:

  • 如果存在,正常处理业务逻辑, 并从redis中删除此token, 那么,如果是重复请求, 由于token已被删除,则不能通过校验,返回请勿重复操作提示。
  • 如果不存在,说明参数不合法或者是重复请求,返回提示即可。

代码实现

@AutoIdempotent 注解 + 拦截器对请求进行拦截。
@ControllerAdvice 全局异常处理

封装一个操作Redis的API工具类,使用RedisTemplate进行封装,需引入Redis的stater:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;

@Component
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 写入缓存
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时效时间
     *
     * @param key
     * @param value
     * @return
     */
    public boolean setEx(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 判断缓存中是否有对应的value
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }


    /**
     * 删除对应的value
     *
     * @param key
     */
    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}

自定义注解AutoIdempotent:
自定义一个注解,定义此注解的主要目的是把它添加在需要实现幂等的方法上,凡是某个方法注解了它,都会实现自动幂等。后台利用反射如果扫描到这个注解,就会处理这个方法实现自动幂等,使用元注解ElementType.METHOD表示它只能放在方法上,etentionPolicy.RUNTIME表示它在运行时。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoIdempotent {
}

token服务接口,token创建和检验:
createToken创建token采用随机算法工具类生成随机uuid字符串,然后放入到redis中(为了防止数据的冗余保留,这里设置过期时间为10000秒,具体可视业务而定),如果放入成功,返回这个token值。
checkToken方法就是从header中获取token到值(如果header中拿不到,就从paramter中获取),如若不存在,直接抛出异常。这个异常信息可以被拦截器捕捉到,然后返回给前端。

import javax.servlet.http.HttpServletRequest;

public interface TokenService {
    // 创建token
    String createToken();
    // 检验token
    void checkToken(HttpServletRequest request);
}


import com.demo.exception.ServiceException;
import com.demo.service.RedisService;
import com.demo.service.TokenService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.UUID;

@Component
public class TokenServiceImpl implements TokenService {

    private static final String TOKEN_NAME = "token";

    @Autowired
    private RedisService redisService;

    @Override
    public String createToken() {
        String token = UUID.randomUUID().toString();
        try {
            redisService.setEx(token, token, 10000L);
            return token;
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @Override
    public void checkToken(HttpServletRequest request) {
        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.isBlank(token)) {
            // header中不存在token
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.isBlank(token)) {
                // parameter中也不存在token
                throw new ServiceException("参数不合法,必须带token参数");
            }
        }
        if (!redisService.exists(token)) {
            throw new ServiceException("请勿重复操作");
        }
        boolean remove = redisService.remove(token);
        // 必须再次判断是否移除成功,因为可能多个请求同时执行上面移除的代码,但是最终只有一个返回移除成功的,如果不判断是否移除成功,就会失去幂等性的
        if (!remove) {
            throw new ServiceException("请勿重复操作");
        }
    }
}

拦截器处理幂等:
主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * 接口幂等性拦截器
 */
@Component
public class AutoIdempotentInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * 预处理
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        if (!(handler instanceof HandlerMethod)) {
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
        if (methodAnnotation != null) {
            tokenService.checkToken(request);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
    }
}

配置拦截器:
继承WebMvcConfigurationSupport,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。

import com.demo.interceptor.AutoIdempotentInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;

import javax.annotation.Resource;

@Configuration
public class WebConfiguration extends WebMvcConfigurationSupport {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}

业务请求类:
通过/get/token路径通过getToken()方法去获取具体的token。
调用test/idempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者。

import com.demo.annotation.AutoIdempotent;
import com.demo.service.TokenService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

@RestController
public class BusinessController {

    @Resource
    private TokenService tokenService;

    @GetMapping("/get/token")
    public String getToken() {
        String token = tokenService.createToken();
        return token;
    }


    @AutoIdempotent
    @GetMapping("/test/idempotence")
    public String testIdempotence() {
        return "ok";
    }
}

获取Token:
SpringBoot 之使用 Redis 实现接口幂等性_第2张图片
第一次请求:
SpringBoot 之使用 Redis 实现接口幂等性_第3张图片
第二次请求:
SpringBoot 之使用 Redis 实现接口幂等性_第4张图片
参考:
瞬间几千次的重复提交,我用 SpringBoot+Redis 扛住了
Sprinig Boot + Redis 实现接口幂等性,写得太好了!

你可能感兴趣的:(#,SpringBoot)