保证其幂等性,通常有以下手段:
1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。
2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。

3、悲观锁或者乐观锁,悲观锁可以保证每次for update的时候其他sql无法update数据(在数据库引擎是innodb的时候,select的条件必须是唯一索引,防止锁全表)

4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。
redis 实现自动幂等的原理图:
在这里插入图片描述

<!--redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

vim application.yml

spring:
  redis:
    host: localhost
    port: 6379
    password: 123456
    database: 0
    jedis:
      pool:
        max-active: 8 #连接池最大连接数
        max-wait: -1 #连接池最大阻塞等待时间
        max-idle: 8 #连接池中的最大空闲连接
        min-idle: 0 #连接池中的最小空闲连接
        time-between-eviction-runs: -1
    timeout: 0
    lettuce:
      shutdown-timeout: 0
@Service
public class RedisUtils {
    @Autowired
    private RedisTemplate redisTemplate;
    
    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;
    }

    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;
    }

    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }
    
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    public boolean remove(final String key) {
        if (exists(key)) {
            Boolean delete = redisTemplate.delete(key);
            return delete;
        }
        return false;
    }
}
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotency {
	boolean required() default true;
}
public interface TokenService {
    /**
     * 创建token
     */
    String createToken();

    /**
     * 检验token 
     */
    boolean checkToken(HttpServletRequest request) throws Exception;

}
@Service
public class TokenServiceImpl implements TokenService {
    @Autowired
    RedisUtils redisUtils;

    private final String TOKEN_PREFIX = "idempotency";
    private final String TOKEN_NAME = "ACCESS-Token";

    @Override
    public String createToken() {
        String str = UUID.randomUUID().toString();
        StringBuilder token = new StringBuilder();
        try {
            token.append(TOKEN_PREFIX).append(str);
            redisUtils.setEx(token.toString(), token.toString(), 10000L);
            if (!StringUtils.isEmpty(token.toString())) {
                return token.toString();
            }
        } catch (Exception ex) {
            ex.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean checkToken(HttpServletRequest request) throws Exception {

        String token = request.getHeader(TOKEN_NAME);
        if (StringUtils.hasText(token)) {
            token = request.getParameter(TOKEN_NAME);
            if (StringUtils.hasText(token)) {
                throw new CustomException(20001, "缺少参数ACCESS-Token");
            }
        }

        if (!redisUtils.exists(token)) {
            throw new CustomException(20001, "不能重复提交");
        }

        boolean remove = redisUtils.remove(token);
        if (!remove) {
            throw new CustomException(20001, "Token刷新失败");
        }
        return true;
    }
}

自定义异常

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CustomException extends RuntimeException {
    private Integer code;
    private String msg;
}

自定义异常拦截

@ControllerAdvice
public class ExceptionHandler {
    //指定出现什么异常值执行这个方法 @org.springframework.web.bind.annotation.ExceptionHandler(CustomException.class)
    @ResponseBody//为了返回数据
    public Map<String, Object> customExceptionHandler(CustomException ex) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", ex.getCode());
        map.put("msg", ex.getMsg());
        return map;
    }
}

配置拦截器

 <!--fastjson-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.47</version>
        </dependency>
        <!-- JSONObject对象依赖的jar包 -->
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
        </dependency>
        <dependency>
            <groupId>commons-collections</groupId>
            <artifactId>commons-collections</artifactId>
            <version>3.2.1</version>
        </dependency>
        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>commons-logging</groupId>
            <artifactId>commons-logging</artifactId>
            <version>1.1.1</version>
        </dependency>
        <dependency>
            <groupId>net.sf.ezmorph</groupId>
            <artifactId>ezmorph</artifactId>
            <version>1.0.6</version>
        </dependency>
        <dependency>
            <groupId>net.sf.json-lib</groupId>
            <artifactId>json-lib</artifactId>
            <version>2.2.3</version>
            <classifier>jdk15</classifier><!-- 指定jdk版本 -->
        </dependency>

WebConfiguration

@Configuration
public class WebConfiguration extends WebMvcConfigurerAdapter {

    @Resource
    private AutoIdempotentInterceptor autoIdempotentInterceptor;

    /** * 添加拦截器 * @param registry */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(autoIdempotentInterceptor);
        super.addInterceptors(registry);
    }
}
/**
 * 拦截器
 */
@Component
public class IdempotencyInterceptor implements HandlerInterceptor {

    @Autowired
    private TokenService tokenService;

    /**
     * 预处理 
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (!(handler instanceof HandlerMethod)) {//如果没有注解,直接返回true
            return true;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        //被ApiIdempotment标记的扫描
        if (method.isAnnotationPresent(Idempotency.class)) {
            Idempotency idempotencyAnnotation = method.getAnnotation(Idempotency.class);//通过反射获取注解
            if (idempotencyAnnotation.required()) {
                return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
            }
        }
        return true;
    }

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

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

    }

    private void writeReturnJson(HttpServletResponse response, String json) throws Exception {
        PrintWriter writer = null;
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html; charset=utf-8");
        try {
            writer = response.getWriter();
            writer.print(json);

        } catch (IOException e) {
        } finally {
            if (writer != null)
                writer.close();
        }
    }
}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐