add
yj
2024-12-05 b9900893177c78fc559223521fe839aa21000017
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package com.dobbinsoft.fw.support.rate;
 
import com.alibaba.fastjson.JSONObject;
import com.dobbinsoft.fw.core.annotation.HttpMethod;
import com.dobbinsoft.fw.core.annotation.RateLimitType;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
 
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
 
/**
 * ClassName: RateLimiterRedisSlidingWindow
 * Description: Redis 实现 滑动窗口 限流器
 *
 * @author: e-weichaozheng
 * @date: 2021-04-12
 */
@Slf4j
public class RateLimiterRedisSlidingWindow implements RateLimiter {
 
    @Autowired
    private StringRedisTemplate lockRedisTemplate;
 
    private static final String SLIDING_BUCKET = "RT_SLIDING_";
 
 
 
    /**
     * 将窗口分为6格 分别为Hash表的 K0 --- K5
     */
    private static final int DIV = 6;
 
    /**
     * 时间戳KEY
     */
    private static final String TIMESTAMP_KEY = "TIMESTAMP_KEY";
 
    /**
     * IO 操作 不滑动窗口 2
     *         滑动窗口  4
     * @param fullMethod 方法全名 group.method
     * @param httpMethod 方法注解
     * @param personId 人的ID,管理员 或 用户
     * @param ip 外网IP
     * @return
     */
    @Override
    public boolean acquire(String fullMethod, HttpMethod httpMethod, Long personId, String ip) {
        if (httpMethod.rateLimit() != RateLimitType.NONE) {
            String key = SLIDING_BUCKET + fullMethod;
            if (httpMethod.rateLimit() == RateLimitType.USER_ID) {
                key = key + "_U_" + personId;
            } else if (httpMethod.rateLimit() == RateLimitType.IP) {
                key = key + "_P_" + ip;
            }
            key = key + "_S_" + httpMethod.rateWindow();
            // 大窗 ms
            int bigWindow = httpMethod.rateWindow() * 1000;
            // 小窗
            int smallWindow = bigWindow / DIV;
            TimeHolder timeHolder = new TimeHolder();
            Map<Object, Object> entries = lockRedisTemplate.opsForHash().entries(key);
            log.info(JSONObject.toJSONString(entries));
            int sum = entries.values().stream().filter(item -> {
                long v = new Long((String) item);
                if (v > 1618284149838L) {
                    // 为了减少一次redis操作,hash表里面会存一个时间戳,大于这个阈值的就认为是时间戳
                    // 也可以通过KEY来,但是会在JAVA增加多次Hash操作,不划算
                    timeHolder.setTimestamp(v);
                    return false;
                } else {
                    return true;
                }
            }).mapToInt(item -> new Integer((String) item)).sum();
 
            // 系统时间
            long nowTime = System.currentTimeMillis();
            // 开窗时间
            long openTime = timeHolder.getTimestamp();
 
            long indexRaw = (nowTime - openTime) / smallWindow;
            int index;
            if (indexRaw - (DIV - 1) > DIV) {
                // 全部清空
                lockRedisTemplate.delete(key);
                lockRedisTemplate.opsForHash().put(key, TIMESTAMP_KEY, nowTime + "");
                lockRedisTemplate.expire(key, 12, TimeUnit.HOURS);
                sum = 0;
                index = 0;
            } else if (indexRaw > DIV - 1){
                int delta = (int)(indexRaw - (DIV - 1));
                // 将大窗滑动 & 并减去前 delta 个小窗的值
                for (int i = 0; i < DIV; i++) {
                    if (i < delta) {
                        String num = (String)entries.get("K" + i);
                        if (num != null) {
                            sum -= Integer.parseInt(num);
                        }
                    }
                    if (i + delta > DIV) {
                        entries.put("K" + i, "0");
                    } else {
                        Object o = entries.get("K" + (i + delta));
                        entries.put("K" + i, StringUtils.isEmpty(o) ? "0" : o);
                    }
                }
                entries.put(TIMESTAMP_KEY, nowTime + "");
                lockRedisTemplate.opsForHash().putAll(key, entries);
                index = DIV - 1;
            } else {
                index = (int) indexRaw;
            }
 
            if (sum >= httpMethod.rate()) {
                return false;
            }
            lockRedisTemplate.opsForHash().increment(key, "K" + index, 1l);
 
        }
        return true;
    }
 
    @Data
    private static class TimeHolder {
 
        /**
         * 默认开窗时间为0,也不会影响计数
         */
        private Long timestamp = 0L;
 
    }
 
}