Chinaunix首页 | 论坛 | 博客
  • 博客访问: 604379
  • 博文数量: 298
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 3077
  • 用 户 组: 普通用户
  • 注册时间: 2019-06-17 10:57
文章分类

全部博文(298)

文章存档

2022年(96)

2021年(201)

2019年(1)

我的朋友

分类: Java

2021-12-20 12:03:00


点击(此处)折叠或打开


  1. 前言
  2. 在实际的开发项目中,一个对外暴露的接口往往会面临很多次请求,我们来解释一下幂等的概念:任意多次执行所产生的影响均与一次执行的影响相同。按照这个含义,最终的含义就是 对数据库的影响只能是一次性的,不能重复处理。如何保证其幂等性,通常有以下手段:

  3. 1、数据库建立唯一性索引,可以保证最终插入数据库的只有一条数据。

  4. 2、token机制,每次接口请求前先获取一个token,然后再下次请求的时候在请求的header体中加上这个token,后台进行验证,如果验证通过删除token,下次请求再次判断token。

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

  6. 4、先查询后判断,首先通过查询数据库是否存在数据,如果存在证明已经请求过了,直接拒绝该请求,如果没有存在,就证明是第一次进来,直接放行。

  7. redis 实现自动幂等的原理图:



  8. 搭建 Redis 服务 API
  9. 1、首先是搭建redis服务器。

  10. 2、引入springboot中到的redis的stater,或者Spring封装的jedis也可以,后面主要用到的api就是它的set方法和exists方法,这里我们使用springboot的封装好的redisTemplate。

  11. 推荐一个 Spring Boot 基础教程及实战示例:

  12. /**java项目 fhadmin.cn
  13.  * redis工具类
  14.  */
  15. @Component
  16. public class RedisService {

  17.     @Autowired
  18.     private RedisTemplate redisTemplate;

  19.     /**
  20.      * 写入缓存
  21.      * @param key
  22.      * @param value
  23.      * @return
  24.      */
  25.     public boolean set(finalString key, Object value) {
  26.         boolean result = false;
  27.         try {
  28.             ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  29.             operations.set(key, value);
  30.             result = true;
  31.         } catch (Exception e) {
  32.             e.printStackTrace();
  33.         }
  34.         return result;
  35.     }

  36.     /**
  37.      * 写入缓存设置时效时间
  38.      * @param key
  39.      * @param value
  40.      * @return
  41.      */
  42.     public boolean setEx(finalString key, Object value, Long expireTime) {
  43.         boolean result = false;
  44.         try {
  45.             ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  46.             operations.set(key, value);
  47.             redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
  48.             result = true;
  49.         } catch (Exception e) {
  50.             e.printStackTrace();
  51.         }
  52.         return result;
  53.     }

  54.     /**
  55.      * 判断缓存中是否有对应的value
  56.      * @param key
  57.      * @return
  58.      */
  59.     public boolean exists(finalString key) {
  60.         return redisTemplate.hasKey(key);
  61.     }

  62.     /**
  63.      * 读取缓存
  64.      * @param key
  65.      * @return
  66.      */
  67.     public Objectget(finalString key) {
  68.         Object result = null;
  69.         ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
  70.         result = operations.get(key);
  71.         return result;
  72.     }

  73.     /**
  74.      * 删除对应的value
  75.      * @param key
  76.      */
  77.     public boolean remove(finalString key) {
  78.         if (exists(key)) {
  79.             Boolean delete = redisTemplate.delete(key);
  80.             return delete;
  81.         }
  82.         returnfalse;

  83.     }

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

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

  90. }
  91. token 创建和检验
  92. token服务接口:我们新建一个接口,创建token服务,里面主要是两个方法,一个用来创建token,一个用来验证token。创建token主要产生的是一个字符串,检验token的话主要是传达request对象,为什么要传request对象呢?主要作用就是获取header里面的token,然后检验,通过抛出的Exception来获取具体的报错信息返回给前端。

  93. publicinterface TokenService {

  94.     /**java项目 fhadmin.cn
  95.      * 创建token
  96.      * @return
  97.      */
  98.     public String createToken();

  99.     /**
  100.      * 检验token
  101.      * @param request
  102.      * @return
  103.      */
  104.     public boolean checkToken(HttpServletRequest request) throws Exception;

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

  107. @Service
  108. publicclass TokenServiceImpl implements TokenService {

  109.     @Autowired
  110.     private RedisService redisService;

  111.     /**
  112.      * 创建token
  113.      * java fhadmin.cn
  114.      * @return
  115.      */
  116.     @Override
  117.     public String createToken() {
  118.         String str = RandomUtil.randomUUID();
  119.         StrBuilder token = new StrBuilder();
  120.         try {
  121.             token.append(Constant.Redis.TOKEN_PREFIX).append(str);
  122.             redisService.setEx(token.toString(), token.toString(),10000L);
  123.             boolean notEmpty = StrUtil.isNotEmpty(token.toString());
  124.             if (notEmpty) {
  125.                 return token.toString();
  126.             }
  127.         }catch (Exception ex){
  128.             ex.printStackTrace();
  129.         }
  130.         returnnull;
  131.     }

  132.     /**
  133.      * 检验token
  134.      *
  135.      * @param request
  136.      * @return
  137.      */
  138.     @Override
  139.     public boolean checkToken(HttpServletRequest request) throws Exception {

  140.         String token = request.getHeader(Constant.TOKEN_NAME);
  141.         if (StrUtil.isBlank(token)) {// header中不存在token
  142.             token = request.getParameter(Constant.TOKEN_NAME);
  143.             if (StrUtil.isBlank(token)) {// parameter中也不存在token
  144.                 thrownew ServiceException(Constant.ResponseCode.ILLEGAL_ARGUMENT, 100);
  145.             }
  146.         }

  147.         if (!redisService.exists(token)) {
  148.             thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
  149.         }

  150.         boolean remove = redisService.remove(token);
  151.         if (!remove) {
  152.             thrownew ServiceException(Constant.ResponseCode.REPETITIVE_OPERATION, 200);
  153.         }
  154.         returntrue;
  155.     }
  156. }
  157. 拦截器的配置
  158. web配置类,实现WebMvcConfigurerAdapter,主要作用就是添加autoIdempotentInterceptor到配置类中,这样我们到拦截器才能生效,注意使用@Configuration注解,这样在容器启动是时候就可以添加进入context中。

  159. @Configuration
  160. publicclass WebConfiguration extends WebMvcConfigurerAdapter {

  161.     @Resource
  162.    private AutoIdempotentInterceptor autoIdempotentInterceptor;

  163.     /**
  164.      * 添加拦截器
  165.      * @param registry
  166.      */
  167.     @Override
  168.     public void addInterceptors(InterceptorRegistry registry) {
  169.         registry.addInterceptor(autoIdempotentInterceptor);
  170.         super.addInterceptors(registry);
  171.     }
  172. }
  173. 拦截处理器:主要的功能是拦截扫描到AutoIdempotent到注解到方法,然后调用tokenService的checkToken()方法校验token是否正确,如果捕捉到异常就将异常信息渲染成json返回给前端。

  174. /** fhadmin.cn
  175.  * 拦截器
  176.  */
  177. @Component
  178. publicclass AutoIdempotentInterceptor implements HandlerInterceptor {

  179.     @Autowired
  180.     private TokenService tokenService;

  181.     /**
  182.      * 预处理
  183.      *
  184.      * @param request
  185.      * @param response
  186.      * @param handler
  187.      * @return
  188.      * @throws Exception
  189.      */
  190.     @Override
  191.     public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

  192.         if (!(handler instanceof HandlerMethod)) {
  193.             returntrue;
  194.         }
  195.         HandlerMethod handlerMethod = (HandlerMethod) handler;
  196.         Method method = handlerMethod.getMethod();
  197.         //被ApiIdempotment标记的扫描
  198.         AutoIdempotent methodAnnotation = method.getAnnotation(AutoIdempotent.class);
  199.         if (methodAnnotation != null) {
  200.             try {
  201.                 return tokenService.checkToken(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示
  202.             }catch (Exception ex){
  203.                 ResultVo failedResult = ResultVo.getFailedResult(101, ex.getMessage());
  204.                 writeReturnJson(response, JSONUtil.toJsonStr(failedResult));
  205.                 throw ex;
  206.             }
  207.         }
  208.         //必须返回true,否则会被拦截一切请求
  209.         returntrue;
  210.     }

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

  213.     }

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

  216.     }

  217.     /**
  218.      * 返回的json值
  219.      * @param response
  220.      * @param json
  221.      * @throws Exception
  222.      */
  223.     private void writeReturnJson(HttpServletResponse response, String json) throws Exception{
  224.         PrintWriter writer = null;
  225.         response.setCharacterEncoding("UTF-8");
  226.         response.setContentType("text/html; charset=utf-8");
  227.         try {
  228.             writer = response.getWriter();
  229.             writer.print(json);

  230.         } catch (IOException e) {
  231.         } finally {
  232.             if (writer != null)
  233.                 writer.close();
  234.         }
  235.     }

  236. }
  237. 测试用例
  238. 模拟业务请求类,首先我们需要通过/get/token路径通过getToken()方法去获取具体的token,然后我们调用testIdempotence方法,这个方法上面注解了@AutoIdempotent,拦截器会拦截所有的请求,当判断到处理的方法上面有该注解的时候,就会调用TokenService中的checkToken()方法,如果捕获到异常会将异常抛出调用者,下面我们来模拟请求一下:

  239. @RestController
  240. publicclass BusinessController {

  241.     @Resource
  242.     private TokenService tokenService;

  243.     @Resource
  244.     private TestService testService;

  245.     @PostMapping("/get/token")
  246.     public String getToken(){
  247.         String token = tokenService.createToken();
  248.         if (StrUtil.isNotEmpty(token)) {
  249.             ResultVo resultVo = new ResultVo();
  250.             resultVo.setCode(Constant.code_success);
  251.             resultVo.setMessage(Constant.SUCCESS);
  252.             resultVo.setData(token);
  253.             return JSONUtil.toJsonStr(resultVo);
  254.         }
  255.         return StrUtil.EMPTY;
  256.     }

  257.     @AutoIdempotent
  258.     @PostMapping("/test/Idempotence")
  259.     public String testIdempotence() {
  260.         String businessResult = testService.testIdempotence();
  261.         if (StrUtil.isNotEmpty(businessResult)) {
  262.             ResultVo successResult = ResultVo.getSuccessResult(businessResult);
  263.             return JSONUtil.toJsonStr(successResult);
  264.         }
  265.         return StrUtil.EMPTY;
  266.     }
  267. }

  268. 使用postman请求,首先访问get/token路径获取到具体到token:




  269. 利用获取到到token,然后放到具体请求到header中,可以看到第一次请求成功,接着我们请求第二次:



  270. 第二次请求,返回到是重复性操作,可见重复性验证通过,再多次请求到时候我们只让其第一次成功,第二次就是失败:




阅读(383) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~