经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » Java相关 » Spring » 查看文章
SpringBoot基于过滤器和内存实现重复请求拦截功能
来源:jb51  时间:2023/2/1 9:35:50  对本文有异议

对于一些请求服务器的接口,可能存在重复发起请求,如果是查询操作倒是并无大碍,但是如果涉及到写入操作,一旦重复,可能对业务逻辑造成很严重的后果,例如交易的接口如果重复请求可能会重复下单。

这里我们使用过滤器的方式对进入服务器的请求进行过滤操作,实现对相同客户端请求同一个接口的过滤。

  1. @Slf4j
  2. @Component
  3. public class IRequestFilter extends OncePerRequestFilter {
  4. @Resource
  5. private FastMap fastMap;
  6. ?
  7. @Override
  8. protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
  9. ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  10. String address = attributes != null ? attributes.getRequest().getRemoteAddr() : UUID.randomUUID().toString();
  11. if (Objects.equals(request.getMethod(), "GET")) {
  12. StringBuilder str = new StringBuilder();
  13. str.append(request.getRequestURI()).append("|")
  14. .append(request.getRemotePort()).append("|")
  15. .append(request.getLocalName()).append("|")
  16. .append(address);
  17. String hex = DigestUtil.md5Hex(new String(str));
  18. log.info("请求的MD5值为:{}", hex);
  19. if (fastMap.containsKey(hex)) {
  20. throw new IllegalStateException("请求重复,请稍后重试!");
  21. }
  22. fastMap.put(hex, 10 * 1000L);
  23. fastMap.expired(hex, 10 * 1000L, (key, val) -> System.out.println("map:" + fastMap + ",删除的key:" + key + ",线程名:" + Thread.currentThread().getName()));
  24. }
  25. log.info("请求的 address:{}", address);
  26. chain.doFilter(request, response);
  27. }
  28. }

通过继承Spring中的OncePerRequestFilter过滤器,确保在一次请求中只通过一次filter,而不需要重复的执行

通过获取请求体中的数据,计算出MD5值,存储在基于内存实现的FastMap中,FastMap的键为MD5值,value表示多久以内不能重复请求,这里配置的是10s内不能重复请求。通过调用FastMap的expired()方法,设置该请求的过期时间和过期时的回调函数

  1. @Component
  2. public class FastMap {
  3. /**
  4. * 按照时间顺序保存了会过期key集合,为了实现快速删除,结构:时间戳 -> key 列表
  5. */
  6. private final TreeMap<Long, List<String>> expireKeysMap = new TreeMap<>();
  7. /**
  8. * 保存会过期key的过期时间
  9. */
  10. private final Map<String, Long> keyExpireMap = new ConcurrentHashMap<>();
  11. /**
  12. * 保存键过期的回调函数
  13. */
  14. private final HashMap<String, ExpireCallback<String, Long>> keyExpireCallbackMap = new HashMap<>();
  15. private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  16. /**
  17. * 数据写锁
  18. */
  19. private final Lock dataWriteLock = readWriteLock.writeLock();
  20. /**
  21. * 数据读锁
  22. */
  23. private final Lock dataReadLock = readWriteLock.readLock();
  24. private final ReentrantReadWriteLock expireKeysReadWriteLock = new ReentrantReadWriteLock();
  25. /**
  26. * 过期key写锁
  27. */
  28. private final Lock expireKeysWriteLock = expireKeysReadWriteLock.writeLock();
  29. /**
  30. * 过期key读锁
  31. */
  32. private final Lock expireKeysReadLock = expireKeysReadWriteLock.readLock();
  33. /**
  34. * 定时执行服务(全局共享线程池)
  35. */
  36. private volatile ScheduledExecutorService scheduledExecutorService;
  37. /**
  38. * 100万,1毫秒=100万纳秒
  39. */
  40. private static final int ONE_MILLION = 100_0000;
  41. /**
  42. * 构造器,enableExpire配置是否启用过期,不启用排序
  43. */
  44. public FastMap() {
  45. this.init();
  46. }
  47. /**
  48. * 初始化
  49. */
  50. private void init() {
  51. // 双重校验构造一个单例的scheduledExecutorService
  52. if (scheduledExecutorService == null) {
  53. synchronized (FastMap.class) {
  54. if (scheduledExecutorService == null) {
  55. // 启用定时器,定时删除过期key,1秒后启动,定时1秒, 因为时间间隔计算基于nanoTime,比timer.schedule更靠谱
  56. scheduledExecutorService = new ScheduledThreadPoolExecutor(1, runnable -> {
  57. Thread thread = new Thread(runnable, "expireTask-" + UUID.randomUUID());
  58. thread.setDaemon(true);
  59. return thread;
  60. });
  61. }
  62. }
  63. }
  64. }
  65. public boolean containsKey(Object key) {
  66. dataReadLock.lock();
  67. try {
  68. return this.keyExpireMap.containsKey(key);
  69. } finally {
  70. dataReadLock.unlock();
  71. }
  72. }
  73. public Long put(String key, Long value) {
  74. dataWriteLock.lock();
  75. try {
  76. return this.keyExpireMap.put(key, value);
  77. } finally {
  78. dataWriteLock.unlock();
  79. }
  80. }
  81. public Long remove(Object key) {
  82. dataWriteLock.lock();
  83. try {
  84. return this.keyExpireMap.remove(key);
  85. } finally {
  86. dataWriteLock.unlock();
  87. }
  88. }
  89. public Long expired(String key, Long ms, ExpireCallback<String, Long> callback) {
  90. // 对过期数据写上锁
  91. expireKeysWriteLock.lock();
  92. try {
  93. // 使用nanoTime消除系统时间的影响,转成毫秒存储降低timeKey数量,过期时间精确到毫秒级别
  94. Long expireTime = (System.nanoTime() / ONE_MILLION + ms);
  95. this.keyExpireMap.put(key, expireTime);
  96. List<String> keys = this.expireKeysMap.get(expireTime);
  97. if (keys == null) {
  98. keys = new ArrayList<>();
  99. keys.add(key);
  100. this.expireKeysMap.put(expireTime, keys);
  101. } else {
  102. keys.add(key);
  103. }
  104. if (callback != null) {
  105. // 设置的过期回调函数
  106. this.keyExpireCallbackMap.put(key, callback);
  107. }
  108. // 使用延时服务调用清理key的函数,可以及时调用过期回调函数
  109. // 同key重复调用,会产生多个延时任务,就是多次调用清理函数,但是不会产生多次回调,因为回调取决于过期时间和回调函数)
  110. scheduledExecutorService.schedule(this::clearExpireData, ms, TimeUnit.MILLISECONDS);
  111. ?
  112. //假定系统时间不修改前提下的过期时间
  113. return System.currentTimeMillis() + ms;
  114. } finally {
  115. expireKeysWriteLock.unlock();
  116. }
  117. }
  118. /**
  119. * 清理过期的数据
  120. * 调用时机:设置了过期回调函数的key的延时任务调用
  121. */
  122. private void clearExpireData() {
  123. // 查找过期key
  124. Long curTimestamp = System.nanoTime() / ONE_MILLION;
  125. Map<Long, List<String>> expiredKeysMap = new LinkedHashMap<>();
  126. expireKeysReadLock.lock();
  127. try {
  128. // 过期时间在【从前至此刻】区间内的都为过期的key
  129. // headMap():获取从头到 curTimestamp 元素的集合:不包含 curTimestamp
  130. SortedMap<Long, List<String>> sortedMap = this.expireKeysMap.headMap(curTimestamp, true);
  131. expiredKeysMap.putAll(sortedMap);
  132. } finally {
  133. expireKeysReadLock.unlock();
  134. }
  135. ?
  136. for (Map.Entry<Long, List<String>> entry : expiredKeysMap.entrySet()) {
  137. for (String key : entry.getValue()) {
  138. // 删除数据
  139. Long val = this.remove(key);
  140. // 首次调用删除(val!=null,前提:val存储值都不为null)
  141. if (val != null) {
  142. // 如果存在过期回调函数,则执行回调
  143. ExpireCallback<String, Long> callback;
  144. expireKeysReadLock.lock();
  145. try {
  146. callback = this.keyExpireCallbackMap.get(key);
  147. } finally {
  148. expireKeysReadLock.unlock();
  149. }
  150. if (callback != null) {
  151. // 回调函数创建新线程调用,防止因为耗时太久影响线程池的清理工作
  152. // 这里为什么不用线程池调用,因为ScheduledThreadPoolExecutor线程池仅支持核心线程数设置,不支持非核心线程的添加
  153. // 核心线程数用一个就可以完成清理工作,添加额外的核心线程数浪费了
  154. new Thread(() -> callback.onExpire(key, val), "callback-thread-" + UUID.randomUUID()).start();
  155. }
  156. }
  157. this.keyExpireCallbackMap.remove(key);
  158. }
  159. this.expireKeysMap.remove(entry.getKey());
  160. }
  161. }
  162. }

FastMap通过ScheduledExecutorService接口实现定时线程任务的方式对请求处于过期时间的自动删除。

到此这篇关于SpringBoot基于过滤器和内存实现重复请求拦截的文章就介绍到这了,更多相关SpringBoot重复请求拦截内容请搜索w3xue以前的文章或继续浏览下面的相关文章希望大家以后多多支持w3xue!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号