• (Redis使用系列) Springboot 使用redis实现接口幂等性拦截 十一


    前言

    近期一个老项目出现了接口幂等性 校验问题,前端加了按钮置灰,

    依然被人拉着接口参数一顿输出,还是重复调用了接口,小陈及时赶到现场,通过复制粘贴,完成了后端接口幂等性调用校验。

    以前写过一篇关于接口简单限流防止重复调用的,但是跟该篇还是不一样的,该篇的角度是接口和参数整体一致才当做重复。

    简单限流:  (Redis使用系列) Springboot 使用redis实现接口Api限流 十

    该篇内容:


    实现接口调用的幂等性校验

    方案 :自定义注解+redis+拦截器+MD5 实现

    草图,意会(用户标识不是必要,看业务场景是针对个人还是只针对接口&参数):

    话不多说,开始实战。

    PS: 前排提醒,如果你还不知道怎么springboot整合redis,可以先去看下redis使用系列的 一、二。

    (Redis使用系列) SpringBoot 中对应2.0.x版本的Redis配置 一

    (Redis使用系列) SpringBoot中Redis的RedisConfig 二

    正文

    自定义注解 怎么玩的 :      

    ①标记哪个接口需要进行幂等性拦截        

    ②每个接口可以要求幂等性范围时间不一样,举例:可以2秒内,可以3秒内,时间自己传        

    ③ 一旦触发了,提示语可以不同 ,举例:VIP的接口,普通用户的接口,提示语不一样(开玩笑)

    效果:

    实战开始:

    核心三件套


    注解、拦截器、拦截器配置



    ① RepeatDaMie.java

    1. import java.lang.annotation.ElementType;
    2. import java.lang.annotation.Retention;
    3. import java.lang.annotation.RetentionPolicy;
    4. import java.lang.annotation.Target;
    5. /**
    6. * @Author: JCccc
    7. * @Date: 2022-6-13 9:04
    8. * @Description: 自定义注解,防止重复提交
    9. */
    10. @Target({ElementType.METHOD})
    11. @Retention(RetentionPolicy.RUNTIME)
    12. public @interface RepeatDaMie {
    13. /**
    14. * 时间ms限制
    15. */
    16. public int second() default 1;
    17. /**
    18. * 提示消息
    19. */
    20. public String describe() default "重复提交了,兄弟";
    21. }

    ②ApiRepeatInterceptor.java

    1. import com.example.repeatdemo.annotation.RepeatDaMie;
    2. import com.example.repeatdemo.util.ContextUtil;
    3. import com.example.repeatdemo.util.Md5Encrypt;
    4. import com.example.repeatdemo.util.RedisUtils;
    5. import com.example.repeatdemo.wrapper.CustomHttpServletRequestWrapper;
    6. import com.fasterxml.jackson.databind.ObjectMapper;
    7. import org.slf4j.Logger;
    8. import org.slf4j.LoggerFactory;
    9. import org.springframework.stereotype.Component;
    10. import org.springframework.web.method.HandlerMethod;
    11. import org.springframework.web.servlet.HandlerInterceptor;
    12. import javax.servlet.http.HttpServletRequest;
    13. import javax.servlet.http.HttpServletResponse;
    14. import java.io.IOException;
    15. import java.util.Objects;
    16. /**
    17. * @Author: JCccc
    18. * @Date: 2022-6-15 9:11
    19. * @Description: 接口幂等性校验拦截器
    20. */
    21. @Component
    22. public class ApiRepeatInterceptor implements HandlerInterceptor {
    23. private final Logger log = LoggerFactory.getLogger(this.getClass());
    24. private static final String POST="POST";
    25. private static final String GET="GET";
    26. @Override
    27. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    28. try {
    29. if (handler instanceof HandlerMethod) {
    30. HandlerMethod handlerMethod = (HandlerMethod) handler;
    31. // 获取RepeatDaMie注解
    32. RepeatDaMie repeatDaMie = handlerMethod.getMethodAnnotation(RepeatDaMie.class);
    33. if (null==repeatDaMie) {
    34. return true;
    35. }
    36. //限制的时间范围
    37. int seconds = repeatDaMie.second();
    38. //这个用户唯一标识,可以自己细微调整,是userId还是token还是sessionId还是不需要
    39. String userUniqueKey = request.getHeader("userUniqueKey");
    40. String method = request.getMethod();
    41. String apiParams = "";
    42. if (GET.equals(method)){
    43. log.info("GET请求来了");
    44. apiParams = new ObjectMapper().writeValueAsString(request.getParameterMap());
    45. }else if (POST.equals(method)){
    46. log.info("POST请求来了");
    47. CustomHttpServletRequestWrapper wrapper = (CustomHttpServletRequestWrapper) request;
    48. apiParams = wrapper.getBody();
    49. }
    50. log.info("当前参数是:{}",apiParams);
    51. // 存储key
    52. String keyRepeatDaMie = Md5Encrypt.md5(userUniqueKey+request.getServletPath()+apiParams) ;
    53. RedisUtils redisUtils = ContextUtil.getBean(RedisUtils.class);
    54. if (Objects.nonNull(redisUtils.get(keyRepeatDaMie))){
    55. log.info("重复请求了,重复请求了,拦截了");
    56. returnData(response,repeatDaMie.describe());
    57. return false;
    58. }else {
    59. redisUtils.setWithTime(keyRepeatDaMie, true,seconds);
    60. }
    61. }
    62. return true;
    63. } catch (Exception e) {
    64. log.warn("请求过于频繁请稍后再试");
    65. e.printStackTrace();
    66. }
    67. return true;
    68. }
    69. public void returnData(HttpServletResponse response,String msg) throws IOException {
    70. response.setCharacterEncoding("UTF-8");
    71. response.setContentType("application/json; charset=utf-8");
    72. ObjectMapper objectMapper = new ObjectMapper();
    73. //这里传提示语可以改成自己项目的返回数据封装的类
    74. response.getWriter().println(objectMapper.writeValueAsString(msg));
    75. return;
    76. }
    77. }

    ③ WebConfig.java

    1. import org.springframework.context.annotation.Configuration;
    2. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    3. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    4. /**
    5. * @Author: JCccc
    6. * @Date: 2022-6-15 9:24
    7. * @Description:
    8. */
    9. @Configuration
    10. public class WebConfig implements WebMvcConfigurer {
    11. @Override
    12. public void addInterceptors(InterceptorRegistry registry) {
    13. registry.addInterceptor(new ApiRepeatInterceptor()).addPathPatterns("/**");
    14. }
    15. }

    工具类三件套

    ①ContextUtil.java

    1. import org.springframework.beans.BeansException;
    2. import org.springframework.context.ApplicationContext;
    3. import org.springframework.context.ApplicationContextAware;
    4. import org.springframework.stereotype.Component;
    5. /**
    6. * @Author: JCccc
    7. * @Date: 2022-6-15 9:24
    8. * @Description:
    9. */
    10. @Component
    11. public final class ContextUtil implements ApplicationContextAware {
    12. protected static ApplicationContext applicationContext ;
    13. @Override
    14. public void setApplicationContext(ApplicationContext arg0) throws BeansException {
    15. if (applicationContext == null) {
    16. applicationContext = arg0;
    17. }
    18. }
    19. public static Object getBean(String name) {
    20. //name表示其他要注入的注解name名
    21. return applicationContext.getBean(name);
    22. }
    23. /**
    24. * 拿到ApplicationContext对象实例后就可以手动获取Bean的注入实例对象
    25. */
    26. public static <T> T getBean(Class<T> clazz) {
    27. return applicationContext.getBean(clazz);
    28. }
    29. }

    ②Md5Encrypt.java

    1. import java.io.UnsupportedEncodingException;
    2. import java.security.MessageDigest;
    3. import java.security.NoSuchAlgorithmException;
    4. /**
    5. * @Author: JCccc
    6. * @CreateTime: 2018-10-30
    7. * @Description:
    8. */
    9. public class Md5Encrypt {
    10. private static final char[] DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a',
    11. 'b', 'c', 'd', 'e', 'f'};
    12. /**
    13. * 对字符串进行MD5加密
    14. *
    15. * @param text 明文
    16. * @return 密文
    17. */
    18. public static String md5(String text) {
    19. MessageDigest msgDigest = null;
    20. try {
    21. msgDigest = MessageDigest.getInstance("MD5");
    22. } catch (NoSuchAlgorithmException e) {
    23. throw new IllegalStateException("System doesn't support MD5 algorithm.");
    24. }
    25. try {
    26. // 注意该接口是按照指定编码形式签名
    27. msgDigest.update(text.getBytes("UTF-8"));
    28. } catch (UnsupportedEncodingException e) {
    29. throw new IllegalStateException("System doesn't support your EncodingException.");
    30. }
    31. byte[] bytes = msgDigest.digest();
    32. String md5Str = new String(encodeHex(bytes));
    33. return md5Str;
    34. }
    35. private static char[] encodeHex(byte[] data) {
    36. int l = data.length;
    37. char[] out = new char[l << 1];
    38. // two characters form the hex value.
    39. for (int i = 0, j = 0; i < l; i++) {
    40. out[j++] = DIGITS[(0xF0 & data[i]) >>> 4];
    41. out[j++] = DIGITS[0x0F & data[i]];
    42. }
    43. return out;
    44. }
    45. }

    ③RedisUtils.java

    1. import org.springframework.beans.factory.annotation.Autowired;
    2. import org.springframework.data.redis.core.*;
    3. import org.springframework.stereotype.Component;
    4. import java.io.Serializable;
    5. import java.util.List;
    6. import java.util.Set;
    7. import java.util.concurrent.TimeUnit;
    8. @Component
    9. public class RedisUtils {
    10. @Autowired
    11. private RedisTemplate redisTemplate;
    12. /**
    13. * 写入String型 [ 键,值]
    14. *
    15. * @param key
    16. * @param value
    17. * @return
    18. */
    19. public boolean set(final String key, Object value) {
    20. boolean result = false;
    21. try {
    22. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
    23. operations.set(key, value);
    24. result = true;
    25. } catch (Exception e) {
    26. e.printStackTrace();
    27. }
    28. return result;
    29. }
    30. /**
    31. * 写入String型,顺便带有过期时间 [ 键,值]
    32. *
    33. * @param key
    34. * @param value
    35. * @return
    36. */
    37. public boolean setWithTime(final String key, Object value,int seconds) {
    38. boolean result = false;
    39. try {
    40. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
    41. operations.set(key, value,seconds, TimeUnit.SECONDS);
    42. result = true;
    43. } catch (Exception e) {
    44. e.printStackTrace();
    45. }
    46. return result;
    47. }
    48. /**
    49. * 批量删除对应的value
    50. *
    51. * @param keys
    52. */
    53. public void remove(final String... keys) {
    54. for (String key : keys) {
    55. remove(key);
    56. }
    57. }
    58. /**
    59. * 批量删除key
    60. *
    61. * @param pattern
    62. */
    63. public void removePattern(final String pattern) {
    64. Set<Serializable> keys = redisTemplate.keys(pattern);
    65. if (keys.size() > 0)
    66. redisTemplate.delete(keys);
    67. }
    68. /**
    69. * 删除对应的value
    70. *
    71. * @param key
    72. */
    73. public void remove(final String key) {
    74. if (exists(key)) {
    75. redisTemplate.delete(key);
    76. }
    77. }
    78. /**
    79. * 判断缓存中是否有对应的value
    80. *
    81. * @param key
    82. * @return
    83. */
    84. public boolean exists(final String key) {
    85. return redisTemplate.hasKey(key);
    86. }
    87. /**
    88. * 读取缓存
    89. *
    90. * @param key
    91. * @return
    92. */
    93. public Object get(final String key) {
    94. Object result = null;
    95. ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
    96. result = operations.get(key);
    97. return result;
    98. }
    99. /**
    100. * 哈希 添加
    101. * hash 一个键值(key->value)对集合
    102. *
    103. * @param key
    104. * @param hashKey
    105. * @param value
    106. */
    107. public void hmSet(String key, Object hashKey, Object value) {
    108. HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    109. hash.put(key, hashKey, value);
    110. }
    111. /**
    112. * Hash获取数据
    113. *
    114. * @param key
    115. * @param hashKey
    116. * @return
    117. */
    118. public Object hmGet(String key, Object hashKey) {
    119. HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
    120. return hash.get(key, hashKey);
    121. }
    122. /**
    123. * 列表添加
    124. * list:lpush key value1
    125. *
    126. * @param k
    127. * @param v
    128. */
    129. public void lPush(String k, Object v) {
    130. ListOperations<String, Object> list = redisTemplate.opsForList();
    131. list.rightPush(k, v);
    132. }
    133. /**
    134. * 列表List获取
    135. * lrange: key 0 10 (读取的个数 从0开始 读取到下标为10 的数据)
    136. *
    137. * @param k
    138. * @param l
    139. * @param l1
    140. * @return
    141. */
    142. public List<Object> lRange(String k, long l, long l1) {
    143. ListOperations<String, Object> list = redisTemplate.opsForList();
    144. return list.range(k, l, l1);
    145. }
    146. /**
    147. * Set集合添加
    148. *
    149. * @param key
    150. * @param value
    151. */
    152. public void add(String key, Object value) {
    153. SetOperations<String, Object> set = redisTemplate.opsForSet();
    154. set.add(key, value);
    155. }
    156. /**
    157. * Set 集合获取
    158. *
    159. * @param key
    160. * @return
    161. */
    162. public Set<Object> setMembers(String key) {
    163. SetOperations<String, Object> set = redisTemplate.opsForSet();
    164. return set.members(key);
    165. }
    166. /**
    167. * Sorted set :有序集合添加
    168. *
    169. * @param key
    170. * @param value
    171. * @param scoure
    172. */
    173. public void zAdd(String key, Object value, double scoure) {
    174. ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
    175. zset.add(key, value, scoure);
    176. }
    177. /**
    178. * Sorted set:有序集合获取
    179. *
    180. * @param key
    181. * @param scoure
    182. * @param scoure1
    183. * @return
    184. */
    185. public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
    186. ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
    187. return zset.rangeByScore(key, scoure, scoure1);
    188. }
    189. /**
    190. * 根据key获取Set中的所有值
    191. *
    192. * @param key 键
    193. * @return
    194. */
    195. public Set<Integer> sGet(String key) {
    196. try {
    197. return redisTemplate.opsForSet().members(key);
    198. } catch (Exception e) {
    199. e.printStackTrace();
    200. return null;
    201. }
    202. }
    203. /**
    204. * 根据value从一个set中查询,是否存在
    205. *
    206. * @param key 键
    207. * @param value 值
    208. * @return true 存在 false不存在
    209. */
    210. public boolean sHasKey(String key, Object value) {
    211. try {
    212. return redisTemplate.opsForSet().isMember(key, value);
    213. } catch (Exception e) {
    214. e.printStackTrace();
    215. return false;
    216. }
    217. }
    218. }

    REDIS配置类

    RedisConfig.java

    1. import com.fasterxml.jackson.annotation.JsonAutoDetect;
    2. import com.fasterxml.jackson.annotation.PropertyAccessor;
    3. import com.fasterxml.jackson.databind.ObjectMapper;
    4. import org.springframework.cache.CacheManager;
    5. import org.springframework.cache.annotation.EnableCaching;
    6. import org.springframework.context.annotation.Bean;
    7. import org.springframework.context.annotation.Configuration;
    8. import org.springframework.data.redis.cache.RedisCacheConfiguration;
    9. import org.springframework.data.redis.cache.RedisCacheManager;
    10. import org.springframework.data.redis.connection.RedisConnectionFactory;
    11. import org.springframework.data.redis.core.RedisTemplate;
    12. import org.springframework.data.redis.core.StringRedisTemplate;
    13. import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
    14. import org.springframework.data.redis.serializer.RedisSerializationContext;
    15. import org.springframework.data.redis.serializer.StringRedisSerializer;
    16. import static org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig;
    17. /**
    18. * @Author: JCccc
    19. * @CreateTime: 2018-09-11
    20. * @Description:
    21. */
    22. @Configuration
    23. @EnableCaching
    24. public class RedisConfig {
    25. @Bean
    26. public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
    27. RedisCacheConfiguration cacheConfiguration =
    28. defaultCacheConfig()
    29. .disableCachingNullValues()
    30. .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)));
    31. return RedisCacheManager.builder(connectionFactory).cacheDefaults(cacheConfiguration).build();
    32. }
    33. @Bean
    34. public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
    35. RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    36. redisTemplate.setConnectionFactory(factory);
    37. Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    38. ObjectMapper om = new ObjectMapper();
    39. om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    40. om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
    41. jackson2JsonRedisSerializer.setObjectMapper(om);
    42. //序列化设置 ,这样为了存储操作对象时正常显示的数据,也能正常存储和获取
    43. redisTemplate.setKeySerializer(new StringRedisSerializer());
    44. redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
    45. redisTemplate.setHashKeySerializer(new StringRedisSerializer());
    46. redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    47. return redisTemplate;
    48. }
    49. @Bean
    50. public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
    51. StringRedisTemplate stringRedisTemplate = new StringRedisTemplate();
    52. stringRedisTemplate.setConnectionFactory(factory);
    53. return stringRedisTemplate;
    54. }
    55. }

    最后写测试接口,看看效果(一个POST,一个GET):

    故意把时间放大,1000秒内重复调用,符合我们拦截规则的都会被拦截。


     TestController.java

    1. import com.example.repeatdemo.dto.PayOrderApply;
    2. import com.example.repeatdemo.annotation.RepeatDaMie;
    3. import org.slf4j.Logger;
    4. import org.slf4j.LoggerFactory;
    5. import org.springframework.web.bind.annotation.*;
    6. /**
    7. * @Author: JCccc
    8. * @Date: 2022-6-05 9:44
    9. * @Description:
    10. */
    11. @RestController
    12. public class TestController {
    13. private final Logger log = LoggerFactory.getLogger(this.getClass());
    14. @RepeatDaMie(second = 1000,describe = "尊敬的客户,您慢点")
    15. @PostMapping(value = "/doPost")
    16. @ResponseBody
    17. public void test(@RequestBody PayOrderApply payOrderApply) {
    18. log.info("Controller POST请求:"+payOrderApply.toString());
    19. }
    20. @RepeatDaMie(second = 1000,describe = "大哥,你冷静点")
    21. @GetMapping(value = "/doGet")
    22. @ResponseBody
    23. public void doGet( PayOrderApply payOrderApply) {
    24. log.info("Controller GET请求:"+payOrderApply.toString());
    25. }
    26. }

    PayOrderApply.java

    1. /**
    2. * @Author: JCccc
    3. * @Date: 2022-6-12 9:46
    4. * @Description:
    5. */
    6. public class PayOrderApply {
    7. private String sn;
    8. private Long amount;
    9. private String proCode;
    10. public String getSn() {
    11. return sn;
    12. }
    13. public void setSn(String sn) {
    14. this.sn = sn;
    15. }
    16. public Long getAmount() {
    17. return amount;
    18. }
    19. public void setAmount(Long amount) {
    20. this.amount = amount;
    21. }
    22. public String getProCode() {
    23. return proCode;
    24. }
    25. public void setProCode(String proCode) {
    26. this.proCode = proCode;
    27. }
    28. @Override
    29. public String toString() {
    30. return "PayOrderApply{" +
    31. "sn='" + sn + '\'' +
    32. ", amount=" + amount +
    33. ", proCode='" + proCode + '\'' +
    34. '}';
    35. }
    36. }

     redis生成了值:

     

    好了,该篇就到这吧、

  • 相关阅读:
    MySQL - order by排序查询 (查询操作 四)
    python第五次作业
    yarn的安装与配置(Windows/macOS)
    【PAT甲级】1058 A+B in Hogwarts
    [鹏程杯2023]复现
    -级数求和-
    「C#」异步编程玩法笔记-async、await
    Linux编程知识之GLIB的GOption接口函数
    Web&Http&Servlet&Request&Response(完整知识点汇总)
    WAN-LAN以及一些计算机网络概念
  • 原文地址:https://blog.csdn.net/qq_35387940/article/details/125426400