对于一些恳求服务器的接口,可能存在重复建议恳求,假如是查询操作却是并无大碍,但是假如涉及到写入操作,一旦重复,可能对业务逻辑造成很严重的后果,例如买卖的接口假如重复恳求可能会重复下单。

这儿我们运用过滤器的方法对进入服务器的恳求进行过滤操作,完结对相同客户端恳求同一个接口的过滤。

@Slf4j
@Component
public class IRequestFilter extends OncePerRequestFilter {
  @Resource
  private FastMap fastMap;
​
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
    ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
    String address = attributes != null ? attributes.getRequest().getRemoteAddr() : UUID.randomUUID().toString();
    if (Objects.equals(request.getMethod(), "GET")) {
      StringBuilder str = new StringBuilder();
      str.append(request.getRequestURI()).append("|")
           .append(request.getRemotePort()).append("|")
           .append(request.getLocalName()).append("|")
           .append(address);
      String hex = DigestUtil.md5Hex(new String(str));
      log.info("恳求的MD5值为:{}", hex);
      if (fastMap.containsKey(hex)) {
        throw new IllegalStateException("恳求重复,请稍后重试!");
       }
      fastMap.put(hex, 10 * 1000L);
      fastMap.expired(hex, 10 * 1000L, (key, val) -> System.out.println("map:" + fastMap + ",删去的key:" + key + ",线程名:" + Thread.currentThread().getName()));
     }
    log.info("恳求的 address:{}", address);
    chain.doFilter(request, response);
   }
}

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

经过获取恳求体中的数据,计算出MD5值,存储在根据内存完结的FastMap中,FastMap的键为MD5值,value表示多久以内不能重复恳求,这儿装备的是10s内不能重复恳求。经过调用FastMap的expired()方法,设置该恳求的过期时刻和过期时的回调函数

@Component
public class FastMap {
  /**
   * 按照时刻次序保存了会过期key调集,为了完结快速删去,结构:时刻戳 -> key 列表
   */
  private final TreeMap<Long, List<String>> expireKeysMap = new TreeMap<>();
  /**
   * 保存会过期key的过期时刻
   */
  private final Map<String, Long> keyExpireMap = new ConcurrentHashMap<>();
  /**
   * 保存键过期的回调函数
   */
  private final HashMap<String, ExpireCallback<String, Long>> keyExpireCallbackMap = new HashMap<>();
  private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
  /**
   * 数据写锁
   */
  private final Lock dataWriteLock = readWriteLock.writeLock();
  /**
   * 数据读锁
   */
  private final Lock dataReadLock = readWriteLock.readLock();
  private final ReentrantReadWriteLock expireKeysReadWriteLock = new ReentrantReadWriteLock();
  /**
   * 过期key写锁
   */
  private final Lock expireKeysWriteLock = expireKeysReadWriteLock.writeLock();
  /**
   * 过期key读锁
   */
  private final Lock expireKeysReadLock = expireKeysReadWriteLock.readLock();
  /**
   * 守时履行服务(全局共享线程池)
   */
  private volatile ScheduledExecutorService scheduledExecutorService;
  /**
   * 100万,1毫秒=100万纳秒
   */
  private static final int ONE_MILLION = 100_0000;
  /**
   * 构造器,enableExpire装备是否启用过期,不启用排序
   */
  public FastMap() {
    this.init();
   }
  /**
   * 初始化
   */
  private void init() {
    // 两层校验构造一个单例的scheduledExecutorService
    if (scheduledExecutorService == null) {
      synchronized (FastMap.class) {
        if (scheduledExecutorService == null) {
          // 启用守时器,守时删去过期key,1秒后启动,守时1秒, 由于时刻间隔计算根据nanoTime,比timer.schedule更靠谱
          scheduledExecutorService = new ScheduledThreadPoolExecutor(1, runnable -> {
            Thread thread = new Thread(runnable, "expireTask-" + UUID.randomUUID());
            thread.setDaemon(true);
            return thread;
           });
         }
       }
     }
   }
  public boolean containsKey(Object key) {
    dataReadLock.lock();
    try {
      return this.keyExpireMap.containsKey(key);
     } finally {
      dataReadLock.unlock();
     }
   }
  public Long put(String key, Long value) {
    dataWriteLock.lock();
    try {
      return this.keyExpireMap.put(key, value);
     } finally {
      dataWriteLock.unlock();
     }
   }
  public Long remove(Object key) {
    dataWriteLock.lock();
    try {
      return this.keyExpireMap.remove(key);
     } finally {
      dataWriteLock.unlock();
     }
   }
  public Long expired(String key, Long ms, ExpireCallback<String, Long> callback) {
    // 对过期数据写上锁
    expireKeysWriteLock.lock();
    try {
      // 运用nanoTime消除体系时刻的影响,转成毫秒存储降低timeKey数量,过期时刻精确到毫秒等级
      Long expireTime = (System.nanoTime() / ONE_MILLION + ms);
      this.keyExpireMap.put(key, expireTime);
      List<String> keys = this.expireKeysMap.get(expireTime);
      if (keys == null) {
        keys = new ArrayList<>();
        keys.add(key);
        this.expireKeysMap.put(expireTime, keys);
       } else {
        keys.add(key);
       }
      if (callback != null) {
        // 设置的过期回调函数
        this.keyExpireCallbackMap.put(key, callback);
       }
      // 运用延时服务调用整理key的函数,能够及时调用过期回调函数
      // 同key重复调用,会发生多个延时使命,便是屡次调用整理函数,但是不会发生屡次回调,由于回调取决于过期时刻和回调函数)
      scheduledExecutorService.schedule(this::clearExpireData, ms, TimeUnit.MILLISECONDS);
​
      //假定体系时刻不修正前提下的过期时刻
      return System.currentTimeMillis() + ms;
     } finally {
      expireKeysWriteLock.unlock();
     }
   }
  /**
   * 整理过期的数据
   * 调用机遇:设置了过期回调函数的key的延时使命调用
   */
  private void clearExpireData() {
    // 查找过期key
    Long curTimestamp = System.nanoTime() / ONE_MILLION;
    Map<Long, List<String>> expiredKeysMap = new LinkedHashMap<>();
    expireKeysReadLock.lock();
    try {
      // 过期时刻在【早年至此时】区间内的都为过期的key
      // headMap():获取从头到 curTimestamp 元素的调集:不包含 curTimestamp
      SortedMap<Long, List<String>> sortedMap = this.expireKeysMap.headMap(curTimestamp, true);
      expiredKeysMap.putAll(sortedMap);
     } finally {
      expireKeysReadLock.unlock();
     }
​
    for (Map.Entry<Long, List<String>> entry : expiredKeysMap.entrySet()) {
      for (String key : entry.getValue()) {
        // 删去数据
        Long val = this.remove(key);
        // 首次调用删去(val!=null,前提:val存储值都不为null)
        if (val != null) {
          // 假如存在过期回调函数,则履行回调
          ExpireCallback<String, Long> callback;
          expireKeysReadLock.lock();
          try {
            callback = this.keyExpireCallbackMap.get(key);
           } finally {
            expireKeysReadLock.unlock();
           }
          if (callback != null) {
            // 回调函数创建新线程调用,防止由于耗时太久影响线程池的整理工作
            // 这儿为什么不用线程池调用,由于ScheduledThreadPoolExecutor线程池仅支持中心线程数设置,不支持非中心线程的增加
            // 中心线程数用一个就能够完结整理工作,增加额外的中心线程数浪费了
            new Thread(() -> callback.onExpire(key, val), "callback-thread-" + UUID.randomUUID()).start();
           }
         }
        this.keyExpireCallbackMap.remove(key);
       }
      this.expireKeysMap.remove(entry.getKey());
     }
   }
}

FastMap经过ScheduledExecutorService接口完结守时线程使命的方法对恳求处于过期时刻的主动删去。