专栏名称: 朱小厮的博客
著有畅销书:《深入理解Kafka》和《RabbitMQ实战指南》。公众号主要用来分享Java技术栈、Golang技术栈、消息中间件(如Kafka、RabbitMQ)、存储、大数据以及通用型技术架构等相关的技术。
目录
相关文章推荐
光明日报  ·  王楚钦亚洲杯夺冠,赛后庆祝动作上热搜 ·  昨天  
光明日报  ·  王楚钦亚洲杯夺冠,赛后庆祝动作上热搜 ·  昨天  
安徽商报  ·  王曼昱4:0孙颖莎 ·  2 天前  
安徽商报  ·  王曼昱4:0孙颖莎 ·  2 天前  
南京日报  ·  8秒16!吴艳妮夺冠 ·  2 天前  
惠济发布  ·  “冰雪”活动,开赛启幕! ·  2 天前  
51好读  ›  专栏  ›  朱小厮的博客

Spring Boot + Redis 实现接口幂等性 | 分布式开发必知!

朱小厮的博客  · 公众号  ·  · 2019-08-19 17:31

正文

点击上方“ 朱小厮的博客 ”,选择“ 设为星标

回复” 1024 “获取独家整理的学习资料

来源:http://tinyurl.com/y5k2sx5t


一、概念

幂等性, 通俗的说就是一个接口, 多次发起同一个请求, 必须保证操作只能执行一次 比如:

  • 订单接口, 不能多次创建订单

  • 支付接口, 重复支付同一笔订单只能扣一次钱

  • 支付宝回调接口, 可能会多次回调, 必须处理重复回调

  • 普通表单提交接口, 因为网络超时等原因多次点击提交, 只能成功一次 等等

二、常见解决方案

  1. 唯一索引 -- 防止新增脏数据

  2. token机制 -- 防止页面重复提交

  3. 悲观锁 -- 获取数据的时候加锁(锁表或锁行)

  4. 乐观锁 -- 基于版本号version实现, 在更新数据那一刻校验数据

  5. 分布式锁 -- redis(jedis、redisson)或zookeeper实现

  6. 状态机 -- 状态变更, 更新数据时判断状态

三、本文实现

本文采用第2种方式实现, 即通过redis + token机制实现接口幂等性校验

四、实现思路

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

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

  • 如果不存在, 说明参数不合法或者是重复请求, 返回提示即可

五、项目简介

  • springboot

  • redis

  • @ApiIdempotent 注解 + 拦截器对请求进行拦截

  • @ControllerAdvice全局异常处理

  • 压测工具: jmeter

说明:

  • 本文重点介绍幂等性核心实现, 关于 springboot如何集成redis ServerResponse ResponseCode 等细枝末节不在本文讨论范围之内, 有兴趣的小伙伴可以查看作者的Github项目: https://github.com/wangzaiplus/springboot/tree/wxw

六、代码实现

pom

  1. redis.clients

  2. jedis

  3. 2.9.0


  4. org.projectlombok

  5. lombok

  6. 1.16.10

JedisUtil

  1. package com.wangzaiplus.test.util;


  2. import lombok.extern.slf4j.Slf4j;

  3. import org.springframework.beans.factory.annotation.Autowired;

  4. import org.springframework.stereotype.Component;

  5. import redis.clients.jedis.Jedis;

  6. import redis.clients.jedis.JedisPool;


  7. @Component

  8. @Slf4j

  9. public class JedisUtil {


  10. @Autowired

  11. private JedisPool jedisPool;


  12. private Jedis getJedis() {

  13. return jedisPool.getResource();

  14. }


  15. /**

  16. * 设值

  17. *

  18. * @param key

  19. * @param value

  20. * @return

  21. */

  22. public String set(String key, String value) {

  23. Jedis jedis = null;

  24. try {

  25. jedis = getJedis();

  26. return jedis.set(key, value);

  27. } catch (Exception e) {

  28. log.error("set key:{} value:{} error", key, value, e);

  29. return null;

  30. } finally {

  31. close(jedis);

  32. }

  33. }


  34. /**

  35. * 设值

  36. *

  37. * @param key

  38. * @param value

  39. * @param expireTime 过期时间, 单位: s

  40. * @return

  41. */

  42. public String set(String key, String value, int expireTime) {

  43. Jedis jedis = null;

  44. try {

  45. jedis = getJedis();

  46. return jedis.setex(key, expireTime, value);

  47. } catch (Exception e) {

  48. log.error( "set key:{} value:{} expireTime:{} error", key, value, expireTime, e);

  49. return null;

  50. } finally {

  51. close(jedis);

  52. }

  53. }


  54. /**

  55. * 取值

  56. *

  57. * @param key

  58. * @return

  59. */

  60. public String get(String key) {

  61. Jedis jedis = null;

  62. try {

  63. jedis = getJedis();

  64. return jedis.get(key);

  65. } catch (Exception e) {

  66. log.error("get key:{} error", key, e);

  67. return null;

  68. } finally {

  69. close(jedis);

  70. }

  71. }


  72. /**

  73. * 删除key

  74. *

  75. * @param key

  76. * @return

  77. */

  78. public Long del(String key) {

  79. Jedis jedis = null;

  80. try {

  81. jedis = getJedis();

  82. return jedis.del(key.getBytes());

  83. } catch (Exception e) {

  84. log.error("del key:{} error", key, e);

  85. return null;

  86. } finally {

  87. close(jedis);

  88. }

  89. }


  90. /**

  91. * 判断key是否存在

  92. *

  93. * @param key

  94. * @return

  95. */

  96. public Boolean exists(String key) {

  97. Jedis jedis = null;

  98. try {

  99. jedis = getJedis();

  100. return jedis.exists(key.getBytes());

  101. } catch (Exception e) {

  102. log.error("exists key:{} error", key, e);

  103. return null;

  104. } finally {

  105. close(jedis);

  106. }

  107. }


  108. /**

  109. * 设值key过期时间

  110. *

  111. * @param key

  112. * @param expireTime 过期时间, 单位: s

  113. * @return

  114. */

  115. public Long expire(String key, int expireTime) {

  116. Jedis jedis = null;

  117. try {

  118. jedis = getJedis();

  119. return jedis.expire(key.getBytes(), expireTime);

  120. } catch (Exception e) {

  121. log.error("expire key:{} error", key, e);

  122. return null;

  123. } finally {

  124. close(jedis);

  125. }

  126. }


  127. /**

  128. * 获取剩余时间

  129. *

  130. * @param key

  131. * @return

  132. */

  133. public Long ttl(String key) {

  134. Jedis jedis = null;

  135. try {

  136. jedis = getJedis();

  137. return jedis.ttl(key);

  138. } catch (Exception e) {

  139. log.error("ttl key:{} error", key, e);

  140. return null;

  141. } finally {

  142. close(jedis);

  143. }

  144. }


  145. private void close(Jedis jedis) {

  146. if (null != jedis) {

  147. jedis.close();

  148. }

  149. }


  150. }

自定义注解 @ApiIdempotent

  1. package com.wangzaiplus.test.annotation;


  2. import java.lang.annotation.ElementType;

  3. import java.lang.annotation.Retention;

  4. import java.lang.annotation.RetentionPolicy;

  5. import java.lang.annotation.Target;


  6. /**

  7. * 在需要保证 接口幂等性 的Controller的方法上使用此注解

  8. */

  9. @Target({ElementType.METHOD})

  10. @Retention(RetentionPolicy.RUNTIME)

  11. public @interface ApiIdempotent {

  12. }

  1. ApiIdempotentInterceptor 拦截器

  1. package com.wangzaiplus.test.interceptor;


  2. import com.wangzaiplus.test.annotation.ApiIdempotent;

  3. import com.wangzaiplus.test.service.TokenService;

  4. import org.springframework.beans.factory.annotation.Autowired;

  5. import org.springframework.web.method.HandlerMethod;

  6. import org.springframework.web.servlet.HandlerInterceptor;

  7. import org.springframework.web.servlet.ModelAndView;


  8. import javax.servlet.http. HttpServletRequest;

  9. import javax.servlet.http.HttpServletResponse;

  10. import java.lang.reflect.Method;


  11. /**

  12. * 接口幂等性拦截器

  13. */

  14. public class ApiIdempotentInterceptor implements HandlerInterceptor {


  15. @Autowired

  16. private TokenService tokenService;


  17. @Override

  18. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

  19. if (!(handler instanceof HandlerMethod)) {

  20. return true;

  21. }


  22. HandlerMethod handlerMethod = (HandlerMethod) handler;

  23. Method method = handlerMethod.getMethod();


  24. ApiIdempotent methodAnnotation = method.getAnnotation(ApiIdempotent.class );

  25. if (methodAnnotation != null) {

  26. check(request);// 幂等性校验, 校验通过则放行, 校验失败则抛出异常, 并通过统一异常处理返回友好提示

  27. }


  28. return true;

  29. }


  30. private void check(HttpServletRequest request) {

  31. tokenService.checkToken(request);

  32. }


  33. @Override

  34. public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

  35. }


  36. @Override

  37. public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

  38. }

  39. }

TokenServiceImpl

  1. package com.wangzaiplus.test.service.impl;


  2. import com.wangzaiplus.test.common.Constant;

  3. import com.wangzaiplus.test.common.ResponseCode;

  4. import com.wangzaiplus.test.common.ServerResponse;

  5. import com.wangzaiplus.test.exception.ServiceException;

  6. import com.wangzaiplus.test.service.TokenService;

  7. import com.wangzaiplus.test.util.JedisUtil;

  8. import com.wangzaiplus.test.util.RandomUtil;

  9. import lombok.extern.slf4j.Slf4j;

  10. import org.apache.commons.lang3.StringUtils;

  11. import org.apache.commons.lang3.text.StrBuilder;

  12. import org.springframework.beans.factory.annotation.Autowired;

  13. import org.springframework.stereotype.Service;


  14. import javax.servlet.http.HttpServletRequest;


  15. @Service

  16. public class TokenServiceImpl implements TokenService {


  17. private static final String TOKEN_NAME = "token";


  18. @Autowired

  19. private JedisUtil jedisUtil;


  20. @Override

  21. public ServerResponse createToken() {

  22. String str = RandomUtil.UUID32();

  23. StrBuilder token = new StrBuilder();

  24. token.append(Constant.Redis.TOKEN_PREFIX).append(str);


  25. jedisUtil.set(token.toString(), token.toString(), Constant.Redis.EXPIRE_TIME_MINUTE);


  26. return ServerResponse.success(token.toString());

  27. }


  28. @Override

  29. public void checkToken(HttpServletRequest request) {

  30. String token = request.getHeader(TOKEN_NAME);

  31. if (StringUtils.isBlank(token)) { // header中不存在token

  32. token = request.getParameter(TOKEN_NAME);

  33. if (StringUtils.isBlank(token)) {// parameter中也不存在token

  34. throw new ServiceException(ResponseCode.ILLEGAL_ARGUMENT.getMsg());

  35. }

  36. }


  37. if (!jedisUtil.exists(token)) {

  38. throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());

  39. }


  40. Long del = jedisUtil.del(token);

  41. if (del <= 0) {

  42. throw new ServiceException(ResponseCode.REPETITIVE_OPERATION.getMsg());

  43. }

  44. }


  45. }

TestApplication

  1. package com.wangzaiplus.test;


  2. import com.wangzaiplus.test.interceptor.ApiIdempotentInterceptor;

  3. import org.mybatis.spring.annotation.MapperScan;

  4. import org.springframework.boot.SpringApplication;

  5. import org.springframework.boot.autoconfigure.SpringBootApplication;

  6. import org.springframework.context.annotation.Bean;

  7. import org.springframework.web.cors.CorsConfiguration;

  8. import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

  9. import org.springframework.web.filter.CorsFilter;

  10. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;

  11. import







请到「今天看啥」查看全文