diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java new file mode 100644 index 0000000..09c1c9c --- /dev/null +++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/FrequencyControlConstant.java @@ -0,0 +1,10 @@ +package com.abin.mallchat.common; + +public interface FrequencyControlConstant { + + String TOTAL_COUNT_WITH_IN_FIX_TIME = "TotalCountWithInFixTime"; + + String SLIDING_WINDOW = "SlidingWindow"; + + String TOKEN_BUCKET = "TokenBucket"; +} diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/MDCKey.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/MDCKey.java new file mode 100644 index 0000000..ae0f6d2 --- /dev/null +++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/common/MDCKey.java @@ -0,0 +1,6 @@ +package com.abin.mallchat.common; + +public interface MDCKey { + String TID = "tid"; + String UID = "uid"; +} diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java new file mode 100644 index 0000000..c1cc848 --- /dev/null +++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/RedisUtils.java @@ -0,0 +1,1127 @@ +package com.abin.mallchat.utils; + +import cn.hutool.extra.spring.SpringUtil; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.RedisConnectionUtils; +import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.data.redis.core.script.DefaultRedisScript; +import org.springframework.data.redis.core.script.RedisScript; + +import java.util.*; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +@Slf4j +public class RedisUtils { + + private static StringRedisTemplate stringRedisTemplate; + + static { + RedisUtils.stringRedisTemplate = SpringUtil.getBean(StringRedisTemplate.class); + } + + private static final String LUA_INCR_EXPIRE = + "local key,ttl=KEYS[1],ARGV[1] \n" + + " \n" + + "if redis.call('EXISTS',key)==0 then \n" + + " redis.call('SETEX',key,ttl,1) \n" + + " return 1 \n" + + "else \n" + + " return tonumber(redis.call('INCR',key)) \n" + + "end "; + + public static Long inc(String key, int time, TimeUnit unit) { + RedisScript redisScript = new DefaultRedisScript<>(LUA_INCR_EXPIRE, Long.class); + return stringRedisTemplate.execute(redisScript, Collections.singletonList(key), String.valueOf(unit.toSeconds(time))); + } + + public static Long ZSetGet(String key) { + return stringRedisTemplate.opsForZSet().zCard(key); + } + + public static void ZSetAddAndExpire(String key, long startTime, long expireTime, long currentTime) { + stringRedisTemplate.opsForZSet().add(key, String.valueOf(currentTime), currentTime); + // 删除周期之前的数据 + stringRedisTemplate.opsForZSet().removeRangeByScore(key, 0, startTime); + // 过期时间窗口长度+时间间隔 + stringRedisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS); + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + */ + public static Boolean expire(String key, long time) { + try { + if (time > 0) { + stringRedisTemplate.expire(key, time, TimeUnit.SECONDS); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 指定缓存失效时间 + * + * @param key 键 + * @param time 时间(秒) + * @param timeUnit 单位 + */ + public static Boolean expire(String key, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + stringRedisTemplate.expire(key, time, timeUnit); + } + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + return true; + } + + /** + * 根据 key 获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public static Long getExpire(String key) { + return stringRedisTemplate.getExpire(key, TimeUnit.SECONDS); + } + + /** + * 根据 key 获取过期时间 + * + * @param key 键 不能为null + * @return 时间(秒) 返回0代表为永久有效 + */ + public static Long getExpire(String key, TimeUnit timeUnit) { + return stringRedisTemplate.getExpire(key, timeUnit); + } + + /** + * 查找匹配key + * + * @param pattern key + * @return / + */ + public static List scan(String pattern) { + ScanOptions options = ScanOptions.scanOptions().match(pattern).build(); + RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(); + while (cursor.hasNext()) { + result.add(new String(cursor.next())); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 分页查询 key + * + * @param patternKey key + * @param page 页码 + * @param size 每页数目 + * @return / + */ + public static List findKeysForPage(String patternKey, int page, int size) { + ScanOptions options = ScanOptions.scanOptions().match(patternKey).build(); + RedisConnectionFactory factory = stringRedisTemplate.getConnectionFactory(); + RedisConnection rc = Objects.requireNonNull(factory).getConnection(); + Cursor cursor = rc.scan(options); + List result = new ArrayList<>(size); + int tmpIndex = 0; + int fromIndex = page * size; + int toIndex = page * size + size; + while (cursor.hasNext()) { + if (tmpIndex >= fromIndex && tmpIndex < toIndex) { + result.add(new String(cursor.next())); + tmpIndex++; + continue; + } + // 获取到满足条件的数据后,就可以退出了 + if (tmpIndex >= toIndex) { + break; + } + tmpIndex++; + cursor.next(); + } + try { + RedisConnectionUtils.releaseConnection(rc, factory); + } catch (Exception e) { + log.error(e.getMessage(), e); + } + return result; + } + + /** + * 判断key是否存在 + * + * @param key 键 + * @return true 存在 false不存在 + */ + public static Boolean hasKey(String key) { + try { + return stringRedisTemplate.hasKey(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + + /** + * 删除缓存 + * + * @param keys + */ + public static void del(String... keys) { + if (keys != null && keys.length > 0) { + if (keys.length == 1) { + Boolean result = stringRedisTemplate.delete(keys[0]); + log.debug("--------------------------------------------"); + log.debug("删除缓存:" + keys[0] + ",结果:" + result); + } else { + Set keySet = new HashSet<>(); + for (String key : keys) { + Set stringSet = stringRedisTemplate.keys(key); + if (Objects.nonNull(stringSet) && !stringSet.isEmpty()) { + keySet.addAll(stringSet); + } + } + Long count = stringRedisTemplate.delete(keySet); + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keySet); + log.debug("缓存删除数量:" + count + "个"); + } + log.debug("--------------------------------------------"); + } + } + + public static void del(List keys) { + stringRedisTemplate.delete(keys); + } + + // ============================String============================= + + /** + * 普通缓存获取 + * + * @param key 键 + * @return 值 + */ + private static String get(String key) { + return key == null ? null : stringRedisTemplate.opsForValue().get(key); + } + + /** + * 普通缓存放入 + * + * @param key 键 + * @param value 值 + * @return true成功 false失败 + */ + public static Boolean set(String key, Object value) { + try { + stringRedisTemplate.opsForValue().set(key, objToStr(value)); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + public static String getStr(String key) { + return get(key, String.class); + } + + public static T get(String key, Class tClass) { + String s = get(key); + return toBeanOrNull(s, tClass); + } + + public static List mget(Collection keys, Class tClass) { + List list = stringRedisTemplate.opsForValue().multiGet(keys); + if (Objects.isNull(list)) { + return new ArrayList<>(); + } + return list.stream().map(o -> toBeanOrNull(o, tClass)).collect(Collectors.toList()); + } + + static T toBeanOrNull(String json, Class tClass) { + return json == null ? null : JsonUtils.toObj(json, tClass); + } + + public static String objToStr(Object o) { + return JsonUtils.toStr(o); + } + + public static void mset(Map map, long time) { + Map collect = map.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, (e) -> objToStr(e.getValue()))); + stringRedisTemplate.opsForValue().multiSet(collect); + map.forEach((key, value) -> { + expire(key, time); + }); + } + + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 + * @return true成功 false 失败 + */ + public static Boolean set(String key, Object value, long time) { + try { + if (time > 0) { + stringRedisTemplate.opsForValue().set(key, objToStr(value), time, TimeUnit.SECONDS); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 普通缓存放入并设置时间 + * + * @param key 键 + * @param value 值 + * @param time 时间 + * @param timeUnit 类型 + * @return true成功 false 失败 + */ + public static Boolean set(String key, Object value, long time, TimeUnit timeUnit) { + try { + if (time > 0) { + stringRedisTemplate.opsForValue().set(key, objToStr(value), time, timeUnit); + } else { + set(key, value); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + // ================================Map================================= + + /** + * HashGet + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return 值 + */ + public static Object hget(String key, String item) { + return stringRedisTemplate.opsForHash().get(key, item); + } + + /** + * 获取hashKey对应的所有键值 + * + * @param key 键 + * @return 对应的多个键值 + */ + public static Map hmget(String key) { + return stringRedisTemplate.opsForHash().entries(key); + + } + + /** + * HashSet + * + * @param key 键 + * @param map 对应多个键值 + * @return true 成功 false 失败 + */ + public static Boolean hmset(String key, Map map) { + try { + stringRedisTemplate.opsForHash().putAll(key, map); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * HashSet 并设置时间 + * + * @param key 键 + * @param map 对应多个键值 + * @param time 时间(秒) + * @return true成功 false失败 + */ + public static Boolean hmset(String key, Map map, long time) { + try { + stringRedisTemplate.opsForHash().putAll(key, map); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @return true 成功 false失败 + */ + public static Boolean hset(String key, String item, Object value) { + try { + stringRedisTemplate.opsForHash().put(key, item, value); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 向一张hash表中放入数据,如果不存在将创建 + * + * @param key 键 + * @param item 项 + * @param value 值 + * @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间 + * @return true 成功 false失败 + */ + public static Boolean hset(String key, String item, Object value, long time) { + try { + stringRedisTemplate.opsForHash().put(key, item, value); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 删除hash表中的值 + * + * @param key 键 不能为null + * @param item 项 可以使多个 不能为null + */ + public static void hdel(String key, Object... item) { + stringRedisTemplate.opsForHash().delete(key, item); + } + + /** + * 判断hash表中是否有该项的值 + * + * @param key 键 不能为null + * @param item 项 不能为null + * @return true 存在 false不存在 + */ + public static Boolean hHasKey(String key, String item) { + return stringRedisTemplate.opsForHash().hasKey(key, item); + } + + /** + * hash递增 如果不存在,就会创建一个 并把新增后的值返回 + * + * @param key 键 + * @param item 项 + * @param by 要增加几(大于0) + * @return + */ + public static Double hincr(String key, String item, double by) { + return stringRedisTemplate.opsForHash().increment(key, item, by); + } + + /** + * hash递减 + * + * @param key 键 + * @param item 项 + * @param by 要减少记(小于0) + * @return + */ + public static Double hdecr(String key, String item, double by) { + return stringRedisTemplate.opsForHash().increment(key, item, -by); + } + + // ============================set============================= + + /** + * 根据key获取Set中的所有值 + * + * @param key 键 + * @return + */ + public static Set sGet(String key) { + try { + return stringRedisTemplate.opsForSet().members(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 根据value从一个set中查询,是否存在 + * + * @param key 键 + * @param value 值 + * @return true 存在 false不存在 + */ + public static Boolean sHasKey(String key, Object value) { + try { + return stringRedisTemplate.opsForSet().isMember(key, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将数据放入set缓存 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 成功个数 + */ + public static Long sSet(String key, Object... values) { + try { + String[] s = new String[values.length]; + for (int i = 0; i < values.length; i++) { + s[i] = objToStr(values[i]); + } + return stringRedisTemplate.opsForSet().add(key, s); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + /** + * 将set数据放入缓存 + * + * @param key 键 + * @param time 时间(秒) + * @param values 值 可以是多个 + * @return 成功个数 + */ + public static Long sSetAndTime(String key, long time, Object... values) { + try { + String[] s = new String[values.length]; + for (int i = 0; i < values.length; i++) { + s[i] = objToStr(values[i]); + } + Long count = stringRedisTemplate.opsForSet().add(key, s); + if (time > 0) { + expire(key, time); + } + return count; + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + /** + * 获取set缓存的长度 + * + * @param key 键 + * @return + */ + public static Long sGetSetSize(String key) { + try { + return stringRedisTemplate.opsForSet().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + /** + * 移除值为value的 + * + * @param key 键 + * @param values 值 可以是多个 + * @return 移除的个数 + */ + public static Long setRemove(String key, Object... values) { + try { + return stringRedisTemplate.opsForSet().remove(key, values); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + // ===============================list================================= + + /** + * 获取list缓存的内容 + * + * @param key 键 + * @param start 开始 + * @param end 结束 0 到 -1代表所有值 + * @return + */ + public static List lGet(String key, long start, long end) { + try { + return stringRedisTemplate.opsForList().range(key, start, end); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 获取list缓存的长度 + * + * @param key 键 + * @return + */ + public static Long lGetListSize(String key) { + try { + return stringRedisTemplate.opsForList().size(key); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + /** + * 通过索引 获取list中的值 + * + * @param key 键 + * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推 + * @return + */ + public static String lGetIndex(String key, long index) { + try { + return stringRedisTemplate.opsForList().index(key, index); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public static Boolean lSet(String key, Object value) { + try { + stringRedisTemplate.opsForList().rightPush(key, objToStr(value)); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public static Boolean lSet(String key, Object value, long time) { + try { + stringRedisTemplate.opsForList().rightPush(key, objToStr(value)); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @return + */ + public static Boolean lSet(String key, List value) { + try { + List list = new ArrayList<>(); + for (Object item : value) { + list.add(objToStr(item)); + } + stringRedisTemplate.opsForList().rightPushAll(key, list); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 将list放入缓存 + * + * @param key 键 + * @param value 值 + * @param time 时间(秒) + * @return + */ + public static Boolean lSet(String key, List value, long time) { + try { + List list = new ArrayList<>(); + for (Object item : value) { + list.add(objToStr(item)); + } + stringRedisTemplate.opsForList().rightPushAll(key, list); + if (time > 0) { + expire(key, time); + } + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 根据索引修改list中的某条数据 + * + * @param key 键 + * @param index 索引 + * @param value 值 + * @return / + */ + public static Boolean lUpdateIndex(String key, long index, Object value) { + try { + stringRedisTemplate.opsForList().set(key, index, objToStr(value)); + return true; + } catch (Exception e) { + log.error(e.getMessage(), e); + return false; + } + } + + /** + * 移除N个值为value + * + * @param key 键 + * @param count 移除多少个 + * @param value 值 + * @return 移除的个数 + */ + public static Long lRemove(String key, long count, Object value) { + try { + return stringRedisTemplate.opsForList().remove(key, count, value); + } catch (Exception e) { + log.error(e.getMessage(), e); + return 0L; + } + } + + /** + * @param prefix 前缀 + * @param ids id + */ + public void delByKeys(String prefix, Set ids) { + Set keys = new HashSet<>(); + for (Long id : ids) { + Set stringSet = stringRedisTemplate.keys(prefix + id); + if (Objects.nonNull(stringSet) && !stringSet.isEmpty()) { + keys.addAll(stringSet); + } + } + Long count = stringRedisTemplate.delete(keys); + // 此处提示可自行删除 + log.debug("--------------------------------------------"); + log.debug("成功删除缓存:" + keys.toString()); + log.debug("缓存删除数量:" + count + "个"); + log.debug("--------------------------------------------"); + } + /**------------------zSet相关操作--------------------------------*/ + + /** + * 添加元素,有序集合是按照元素的score值由小到大排列 + * + * @param key + * @param value + * @param score + * @return + */ + public static Boolean zAdd(String key, String value, double score) { + return stringRedisTemplate.opsForZSet().add(key, value, score); + } + + public static Boolean zAdd(String key, Object value, double score) { + return zAdd(key, value.toString(), score); + } + + public static Boolean zIsMember(String key, Object value) { + return Objects.nonNull(stringRedisTemplate.opsForZSet().score(key, value.toString())); + } + + /** + * @param key + * @param values + * @return + */ + public Long zAdd(String key, Set> values) { + return stringRedisTemplate.opsForZSet().add(key, values); + } + + /** + * @param key + * @param values + * @return + */ + public static Long zRemove(String key, Object... values) { + return stringRedisTemplate.opsForZSet().remove(key, values); + } + + public static Long zRemove(String key, Object value) { + return zRemove(key, value.toString()); + } + + public static Long zRemove(String key, String value) { + return stringRedisTemplate.opsForZSet().remove(key, value); + } + + /** + * 增加元素的score值,并返回增加后的值 + * + * @param key + * @param value + * @param delta + * @return + */ + public static Double zIncrementScore(String key, String value, double delta) { + return stringRedisTemplate.opsForZSet().incrementScore(key, value, delta); + } + + /** + * 返回元素在集合的排名,有序集合是按照元素的score值由小到大排列 + * + * @param key + * @param value + * @return 0表示第一位 + */ + public static Long zRank(String key, Object value) { + return stringRedisTemplate.opsForZSet().rank(key, value); + } + + /** + * 返回元素在集合的排名,按元素的score值由大到小排列 + * + * @param key + * @param value + * @return + */ + public static Long zReverseRank(String key, Object value) { + return stringRedisTemplate.opsForZSet().reverseRank(key, value); + } + + /** + * 获取集合的元素, 从小到大排序 + * + * @param key + * @param start 开始位置 + * @param end 结束位置, -1查询所有 + * @return + */ + public static Set zRange(String key, long start, long end) { + return stringRedisTemplate.opsForZSet().range(key, start, end); + } + + public static Set zAll(String key) { + return stringRedisTemplate.opsForZSet().range(key, 0, -1); + } + + /** + * 获取集合元素, 并且把score值也获取 + * + * @param key + * @param start + * @param end + * @return + */ + public static Set> zRangeWithScores(String key, long start, + long end) { + return stringRedisTemplate.opsForZSet().rangeWithScores(key, start, end); + } + + /** + * 根据Score值查询集合元素 + * + * @param key + * @param min 最小值 + * @param max 最大值 + * @return + */ + public static Set zRangeByScore(String key, double min, double max) { + return stringRedisTemplate.opsForZSet().rangeByScore(key, min, max); + } + + /** + * 根据Score值查询集合元素, 从小到大排序 + * + * @param key + * @param min 最小值 + * @param max 最大值 + * @return + */ + public static Set> zRangeByScoreWithScores(String key, + Double min, Double max) { + if (Objects.isNull(min)) { + min = Double.MIN_VALUE; + } + if (Objects.isNull(max)) { + max = Double.MAX_VALUE; + } + return stringRedisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max); + } + + /** + * @param key + * @param min + * @param max + * @param start + * @param end + * @return + */ + public static Set> zRangeByScoreWithScores(String key, + double min, double max, long start, long end) { + return stringRedisTemplate.opsForZSet().rangeByScoreWithScores(key, min, max, + start, end); + } + + /** + * 获取集合的元素, 从大到小排序 + * + * @param key + * @param start + * @param end + * @return + */ + public static Set zReverseRange(String key, long start, long end) { + return stringRedisTemplate.opsForZSet().reverseRange(key, start, end); + } + +// /** +// * 获取集合的元素, 从大到小排序, 并返回score值 +// * +// * @param key +// * @param start +// * @param end +// * @return +// */ +// public Set> zReverseRangeWithScores(String key, +// long start, long end) { +// return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, +// end); +// } + + /** + * 获取集合的元素, 从大到小排序, 并返回score值 + * + * @param key + * @param pageSize + * @return + */ + public static Set> zReverseRangeWithScores(String key, + long pageSize) { + return stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, Double.MIN_VALUE, + Double.MAX_VALUE, 0, pageSize); + } + + /** + * @param key + * @param max + * @param pageSize + * @return + */ + public static Set> zReverseRangeByScoreWithScores(String key, + double max, long pageSize) { + return stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, Double.MIN_VALUE, max, + 1, pageSize); + } + +// /** +// * 根据Score值查询集合元素, 从大到小排序 +// * +// * @param key +// * @param min +// * @param max +// * @return +// */ +// public Set zReverseRangeByScore(String key, double min, +// double max) { +// return redisTemplate.opsForZSet().reverseRangeByScore(key, min, max); +// } + +// /** +// * 根据Score值查询集合元素, 从大到小排序 +// * +// * @param key +// * @param min +// * @param max +// * @return +// */ +// public Set> zReverseRangeByScoreWithScores( +// String key, double min, double max) { +// return redisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, +// min, max); +// } + + + /** + * 根据score值获取集合元素数量 + * + * @param key + * @param min + * @param max + * @return + */ + public static Long zCount(String key, double min, double max) { + return stringRedisTemplate.opsForZSet().count(key, min, max); + } + + /** + * 获取集合大小 + * + * @param key + * @return + */ + public static Long zSize(String key) { + return stringRedisTemplate.opsForZSet().size(key); + } + + /** + * 获取集合大小 + * + * @param key + * @return + */ + public static Long zCard(String key) { + return stringRedisTemplate.opsForZSet().zCard(key); + } + + /** + * 获取集合中value元素的score值 + * + * @param key + * @param value + * @return + */ + public static Double zScore(String key, Object value) { + return stringRedisTemplate.opsForZSet().score(key, value); + } + + /** + * 移除指定索引位置的成员 + * + * @param key + * @param start + * @param end + * @return + */ + public static Long zRemoveRange(String key, long start, long end) { + return stringRedisTemplate.opsForZSet().removeRange(key, start, end); + } + + /** + * 根据指定的score值的范围来移除成员 + * + * @param key + * @param min + * @param max + * @return + */ + public static Long zRemoveRangeByScore(String key, double min, double max) { + return stringRedisTemplate.opsForZSet().removeRangeByScore(key, min, max); + } + + /** + * 获取key和otherKey的并集并存储在destKey中 + * + * @param key + * @param otherKey + * @param destKey + * @return + */ + public static Long zUnionAndStore(String key, String otherKey, String destKey) { + return stringRedisTemplate.opsForZSet().unionAndStore(key, otherKey, destKey); + } + + /** + * @param key + * @param otherKeys + * @param destKey + * @return + */ + public static Long zUnionAndStore(String key, Collection otherKeys, + String destKey) { + return stringRedisTemplate.opsForZSet() + .unionAndStore(key, otherKeys, destKey); + } + + /** + * 交集 + * + * @param key + * @param otherKey + * @param destKey + * @return + */ + public static Long zIntersectAndStore(String key, String otherKey, + String destKey) { + return stringRedisTemplate.opsForZSet().intersectAndStore(key, otherKey, + destKey); + } + + /** + * 交集 + * + * @param key + * @param otherKeys + * @param destKey + * @return + */ + public static Long zIntersectAndStore(String key, Collection otherKeys, + String destKey) { + return stringRedisTemplate.opsForZSet().intersectAndStore(key, otherKeys, + destKey); + } + +} diff --git a/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/SpElUtils.java b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/SpElUtils.java new file mode 100644 index 0000000..02838bc --- /dev/null +++ b/mallchat-tools/mallchat-common-starter/src/main/java/com/abin/mallchat/utils/SpElUtils.java @@ -0,0 +1,30 @@ +package com.abin.mallchat.utils; + +import org.springframework.core.DefaultParameterNameDiscoverer; +import org.springframework.expression.EvaluationContext; +import org.springframework.expression.Expression; +import org.springframework.expression.ExpressionParser; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import java.lang.reflect.Method; +import java.util.Optional; + +public class SpElUtils { + private static final ExpressionParser parser = new SpelExpressionParser(); + private static final DefaultParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + + public static String parseSpEl(Method method, Object[] args, String spEl) { + String[] params = Optional.ofNullable(parameterNameDiscoverer.getParameterNames(method)).orElse(new String[]{});//解析参数名 + EvaluationContext context = new StandardEvaluationContext();//el解析需要的上下文对象 + for (int i = 0; i < params.length; i++) { + context.setVariable(params[i], args[i]);//所有参数都作为原材料扔进去 + } + Expression expression = parser.parseExpression(spEl); + return expression.getValue(context, String.class); + } + + public static String getMethodKey(Method method) { + return method.getDeclaringClass() + "#" + method.getName(); + } +} diff --git a/mallchat-tools/mallchat-frequency-control/pom.xml b/mallchat-tools/mallchat-frequency-control/pom.xml new file mode 100644 index 0000000..d7c6e85 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/pom.xml @@ -0,0 +1,47 @@ + + + + mallchat-tools + com.abin.mallchat + 1.0-SNAPSHOT + + 4.0.0 + + mallchat-frequency-control + + + + org.projectlombok + lombok + + + org.springframework.boot + spring-boot-starter-aop + + + org.redisson + redisson-spring-boot-starter + + + com.abin.mallchat + mallchat-common-starter + + + + + + + org.apache.maven.plugins + maven-resources-plugin + 2.4.3 + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java new file mode 100644 index 0000000..135a750 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControl.java @@ -0,0 +1,84 @@ +package com.abin.frequencycontrol.annotation; + +import com.abin.mallchat.common.FrequencyControlConstant; + +import java.lang.annotation.*; +import java.util.concurrent.TimeUnit; + + +/** + * 频控注解 + */ +@Repeatable(FrequencyControlContainer.class) // 可重复 +@Retention(RetentionPolicy.RUNTIME)// 运行时生效 +@Target(ElementType.METHOD)//作用在方法上 +public @interface FrequencyControl { + /** + * 策略 + */ + String strategy() default FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME; + + /** + * 窗口大小,默认 5 个 period + */ + int windowSize() default 5; + + /** + * 窗口最小周期 1s (窗口大小是 5s, 1s一个小格子,共10个格子) + */ + int period() default 1; + + + /** + * key的前缀,默认取方法全限定名,除非我们在不同方法上对同一个资源做频控,就自己指定 + * + * @return key的前缀 + */ + String prefixKey() default ""; + + /** + * 频控对象,默认el表达指定具体的频控对象 + * 对于ip 和uid模式,需要是http入口的对象,保证RequestHolder里有值 + * + * @return 对象 + */ + Target target() default Target.EL; + + /** + * springEl 表达式,target=EL必填 + * + * @return 表达式 + */ + String spEl() default ""; + + /** + * 频控时间范围,默认单位秒 + * + * @return 时间范围 + */ + int time() default 10; + + /** + * 频控时间单位,默认秒 + * + * @return 单位 + */ + TimeUnit unit() default TimeUnit.SECONDS; + + /** + * 单位时间内最大访问次数 + * + * @return 次数 + */ + int count() default 1; + + long capacity() default 3; // 令牌桶容量 + + double refillRate() default 0.5; // 每秒补充的令牌数 + + enum Target { + UID, + IP, + EL + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java new file mode 100644 index 0000000..8b251b0 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/annotation/FrequencyControlContainer.java @@ -0,0 +1,12 @@ +package com.abin.frequencycontrol.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME)// 运行时生效 +public @interface FrequencyControlContainer { + FrequencyControl[] value(); +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java new file mode 100644 index 0000000..269a34c --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/aspect/FrequencyControlAspect.java @@ -0,0 +1,120 @@ +package com.abin.frequencycontrol.aspect; + + +import cn.hutool.core.util.StrUtil; +import com.abin.frequencycontrol.util.RequestHolder; +import com.abin.mallchat.common.FrequencyControlConstant; +import com.abin.mallchat.utils.SpElUtils; +import com.abin.frequencycontrol.annotation.FrequencyControl; +import com.abin.frequencycontrol.domain.dto.FixedWindowDTO; +import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO; +import com.abin.frequencycontrol.domain.dto.SlidingWindowDTO; +import com.abin.frequencycontrol.domain.dto.TokenBucketDTO; +import com.abin.frequencycontrol.service.frequencycontrol.FrequencyControlUtil; +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.aspectj.lang.reflect.MethodSignature; +import org.springframework.stereotype.Component; + +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 频控实现 + */ +@Slf4j +@Aspect +@Component +public class FrequencyControlAspect { + @Around("@annotation(com.abin.frequencycontrol.annotation.FrequencyControl)||@annotation(com.abin.frequencycontrol.annotation.FrequencyControlContainer)") + public Object around(ProceedingJoinPoint joinPoint) throws Throwable { + Method method = ((MethodSignature) joinPoint.getSignature()).getMethod(); + FrequencyControl[] annotationsByType = method.getAnnotationsByType(FrequencyControl.class); + Map keyMap = new HashMap<>(); + String strategy = FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME; + for (int i = 0; i < annotationsByType.length; i++) { + // 获取频控注解 + FrequencyControl frequencyControl = annotationsByType[i]; + String prefix = StrUtil.isBlank(frequencyControl.prefixKey()) ? /* 默认方法限定名 + 注解排名(可能多个)*/method.toGenericString() + ":index:" + i : frequencyControl.prefixKey(); + String key = ""; + switch (frequencyControl.target()) { + case EL: + key = SpElUtils.parseSpEl(method, joinPoint.getArgs(), frequencyControl.spEl()); + break; + case IP: + key = RequestHolder.get().getIp(); + break; + case UID: + key = RequestHolder.get().getUid().toString(); + } + keyMap.put(prefix + ":" + key, frequencyControl); + strategy = frequencyControl.strategy(); + } + // 将注解的参数转换为编程式调用需要的参数 + if (FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME.equals(strategy)) { + // 调用编程式注解 固定窗口 + List frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildFixedWindowDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList()); + return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed); + + } else if (FrequencyControlConstant.TOKEN_BUCKET.equals(strategy)) { + // 调用编程式注解 令牌桶 + List frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildTokenBucketDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList()); + return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed); + } else { + // 调用编程式注解 滑动窗口 + List frequencyControlDTOS = keyMap.entrySet().stream().map(entrySet -> buildSlidingWindowFrequencyControlDTO(entrySet.getKey(), entrySet.getValue())).collect(Collectors.toList()); + return FrequencyControlUtil.executeWithFrequencyControlList(strategy, frequencyControlDTOS, joinPoint::proceed); + } + } + + /** + * 将注解参数转换为编程式调用所需要的参数 + * + * @param key 频率控制Key + * @param frequencyControl 注解 + * @return 编程式调用所需要的参数-FrequencyControlDTO + */ + private SlidingWindowDTO buildSlidingWindowFrequencyControlDTO(String key, FrequencyControl frequencyControl) { + SlidingWindowDTO frequencyControlDTO = new SlidingWindowDTO(); + frequencyControlDTO.setWindowSize(frequencyControl.windowSize()); + frequencyControlDTO.setPeriod(frequencyControl.period()); + frequencyControlDTO.setCount(frequencyControl.count()); + frequencyControlDTO.setUnit(frequencyControl.unit()); + frequencyControlDTO.setKey(key); + return frequencyControlDTO; + } + + /** + * 将注解参数转换为编程式调用所需要的参数 + * + * @param key 频率控制Key + * @param frequencyControl 注解 + * @return 编程式调用所需要的参数-FrequencyControlDTO + */ + private TokenBucketDTO buildTokenBucketDTO(String key, FrequencyControl frequencyControl) { + TokenBucketDTO tokenBucketDTO = new TokenBucketDTO(frequencyControl.capacity(), frequencyControl.refillRate()); + tokenBucketDTO.setKey(key); + return tokenBucketDTO; + } + + /** + * 将注解参数转换为编程式调用所需要的参数 + * + * @param key 频率控制Key + * @param frequencyControl 注解 + * @return 编程式调用所需要的参数-FrequencyControlDTO + */ + private FixedWindowDTO buildFixedWindowDTO(String key, FrequencyControl frequencyControl) { + FixedWindowDTO fixedWindowDTO = new FixedWindowDTO(); + fixedWindowDTO.setCount(frequencyControl.count()); + fixedWindowDTO.setTime(frequencyControl.time()); + fixedWindowDTO.setUnit(frequencyControl.unit()); + fixedWindowDTO.setKey(key); + return fixedWindowDTO; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/config/InterceptorConfig.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/config/InterceptorConfig.java new file mode 100644 index 0000000..365e15e --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/config/InterceptorConfig.java @@ -0,0 +1,29 @@ +package com.abin.frequencycontrol.config; + +import com.abin.frequencycontrol.intecepter.CollectorInterceptor; +import com.abin.frequencycontrol.intecepter.TokenInterceptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +/** + * 配置所有拦截器 + */ +@Configuration +public class InterceptorConfig implements WebMvcConfigurer { + + @Autowired + private TokenInterceptor tokenInterceptor; + @Autowired + private CollectorInterceptor collectorInterceptor; + + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tokenInterceptor) + .addPathPatterns("/capi/**"); + registry.addInterceptor(collectorInterceptor) + .addPathPatterns("/capi/**"); + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java new file mode 100644 index 0000000..6235d9e --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FixedWindowDTO.java @@ -0,0 +1,17 @@ +package com.abin.frequencycontrol.domain.dto; + +import lombok.Data; + +/** + * 限流策略定义 + */ +@Data +public class FixedWindowDTO extends FrequencyControlDTO { + + /** + * 频控时间范围,默认单位秒 + * + * @return 时间范围 + */ + private Integer time; +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java new file mode 100644 index 0000000..52ca8e9 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/FrequencyControlDTO.java @@ -0,0 +1,30 @@ +package com.abin.frequencycontrol.domain.dto; + +import lombok.Data; + +import java.util.concurrent.TimeUnit; + +/** + * 限流策略定义 + */ +@Data +public class FrequencyControlDTO { + /** + * 代表频控的Key 如果target为Key的话 这里要传值用于构建redis的Key target为Ip或者UID的话会从上下文取值 Key字段无需传值 + */ + private String key; + + /** + * 频控时间单位,默认秒 + * + * @return 单位 + */ + private TimeUnit unit; + + /** + * 单位时间内最大访问次数 + * + * @return 次数 + */ + private Integer count; +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java new file mode 100644 index 0000000..106db4e --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/RequestInfo.java @@ -0,0 +1,12 @@ +package com.abin.frequencycontrol.domain.dto; + +import lombok.Data; + +/** + * web请求信息收集类 + */ +@Data +public class RequestInfo { + private Long uid; + private String ip; +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java new file mode 100644 index 0000000..abf0ea0 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/SlidingWindowDTO.java @@ -0,0 +1,20 @@ +package com.abin.frequencycontrol.domain.dto; + +import lombok.Data; + +/** + * 限流策略定义 + */ +@Data +public class SlidingWindowDTO extends FrequencyControlDTO { + + /** + * 窗口大小,默认 10 s + */ + private int windowSize; + + /** + * 窗口最小周期 1s (窗口大小是 10s, 1s一个小格子,-共10个格子) + */ + private int period; +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java new file mode 100644 index 0000000..2ffaace --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/dto/TokenBucketDTO.java @@ -0,0 +1,67 @@ +package com.abin.frequencycontrol.domain.dto; + +import com.abin.frequencycontrol.exception.BusinessErrorEnum; +import com.abin.frequencycontrol.exception.BusinessException; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.locks.ReentrantLock; + +@Data +@Slf4j +public class TokenBucketDTO extends FrequencyControlDTO { + + private final long capacity; // 令牌桶容量 + private final double refillRate; // 每秒补充的令牌数 + private double tokens; // 当前令牌数量 + private long lastRefillTime; // 上次补充令牌的时间 + + private final ReentrantLock lock = new ReentrantLock(); + + public TokenBucketDTO(long capacity, double refillRate) { + if (capacity <= 0 || refillRate <= 0) { + throw new BusinessException(BusinessErrorEnum.CAPACITY_REFILL_ERROR); + } + this.capacity = capacity; + this.refillRate = refillRate; + this.tokens = capacity; + this.lastRefillTime = System.nanoTime(); + } + + public boolean tryAcquire(int permits) { + lock.lock(); + try { + refillTokens(); + if (tokens < permits) { + return true; + } + return false; + } finally { + lock.unlock(); + } + } + + public void deductionToken(int permits) { + lock.lock(); + try { + tokens -= permits; + } finally { + lock.unlock(); + } + } + + /** + * 补充令牌 + */ + private void refillTokens() { + long currentTime = System.nanoTime(); + // 转换为秒 + double elapsedTime = (currentTime - lastRefillTime) / 1e9; + double tokensToAdd = elapsedTime * refillRate; + log.info("tokensToAdd is {}", tokensToAdd); + // 令牌总数不能超过令牌桶容量 + tokens = Math.min(capacity, tokens + tokensToAdd); + log.info("current tokens is {}", tokens); + lastRefillTime = currentTime; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java new file mode 100644 index 0000000..75e3165 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/ApiResult.java @@ -0,0 +1,56 @@ +package com.abin.frequencycontrol.domain.vo.response; + +import com.abin.frequencycontrol.exception.ErrorEnum; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +/** + * 通用返回体 + */ +@Data +@ApiModel("基础返回体") +public class ApiResult { + @ApiModelProperty("成功标识true or false") + private Boolean success; + @ApiModelProperty("错误码") + private Integer errCode; + @ApiModelProperty("错误消息") + private String errMsg; + @ApiModelProperty("返回对象") + private T data; + + public static ApiResult success() { + ApiResult result = new ApiResult(); + result.setData(null); + result.setSuccess(Boolean.TRUE); + return result; + } + + public static ApiResult success(T data) { + ApiResult result = new ApiResult(); + result.setData(data); + result.setSuccess(Boolean.TRUE); + return result; + } + + public static ApiResult fail(Integer code, String msg) { + ApiResult result = new ApiResult(); + result.setSuccess(Boolean.FALSE); + result.setErrCode(code); + result.setErrMsg(msg); + return result; + } + + public static ApiResult fail(ErrorEnum errorEnum) { + ApiResult result = new ApiResult(); + result.setSuccess(Boolean.FALSE); + result.setErrCode(errorEnum.getErrorCode()); + result.setErrMsg(errorEnum.getErrorMsg()); + return result; + } + + public boolean isSuccess() { + return this.success; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java new file mode 100644 index 0000000..e88f988 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/domain/vo/response/PageBaseResp.java @@ -0,0 +1,75 @@ +package com.abin.frequencycontrol.domain.vo.response; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +@Data +@AllArgsConstructor +@NoArgsConstructor +@ApiModel("基础翻页返回") +public class PageBaseResp { + + @ApiModelProperty("当前页数") + private Integer pageNo; + + @ApiModelProperty("每页查询数量") + private Integer pageSize; + + @ApiModelProperty("总记录数") + private Long totalRecords; + + @ApiModelProperty("是否最后一页") + private Boolean isLast = Boolean.FALSE; + + @ApiModelProperty("数据列表") + private List list; + + + public static PageBaseResp empty() { + PageBaseResp r = new PageBaseResp<>(); + r.setPageNo(1); + r.setPageSize(0); + r.setIsLast(true); + r.setTotalRecords(0L); + r.setList(new ArrayList<>()); + return r; + } + + public static PageBaseResp init(Integer pageNo, Integer pageSize, Long totalRecords, Boolean isLast, List list) { + return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLast, list); + } + + public static PageBaseResp init(Integer pageNo, Integer pageSize, Long totalRecords, List list) { + return new PageBaseResp<>(pageNo, pageSize, totalRecords, isLastPage(totalRecords, pageNo, pageSize), list); + } + + public static PageBaseResp init(IPage page) { + return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), page.getRecords()); + } + + public static PageBaseResp init(IPage page, List list) { + return init((int) page.getCurrent(), (int) page.getSize(), page.getTotal(), list); + } + + public static PageBaseResp init(PageBaseResp resp, List list) { + return init(resp.getPageNo(), resp.getPageSize(), resp.getTotalRecords(), resp.getIsLast(), list); + } + + /** + * 是否是最后一页 + */ + public static Boolean isLastPage(long totalRecords, int pageNo, int pageSize) { + if (pageSize == 0) { + return false; + } + long pageTotal = totalRecords / pageSize + (totalRecords % pageSize == 0 ? 0 : 1); + return pageNo >= pageTotal ? true : false; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java new file mode 100644 index 0000000..4187d58 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessErrorEnum.java @@ -0,0 +1,30 @@ +package com.abin.frequencycontrol.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum BusinessErrorEnum implements ErrorEnum { + //==================================common================================== + BUSINESS_ERROR(1001, "{0}"), + //==================================user================================== + //==================================chat================================== + SYSTEM_ERROR(1001, "系统出小差了,请稍后再试哦~~"), + CAPACITY_REFILL_ERROR(1001, "Capacity and refill rate must be positive"), + + + ; + private Integer code; + private String msg; + + @Override + public Integer getErrorCode() { + return code; + } + + @Override + public String getErrorMsg() { + return msg; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java new file mode 100644 index 0000000..4675641 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/BusinessException.java @@ -0,0 +1,55 @@ +package com.abin.frequencycontrol.exception; + +import lombok.Data; + +@Data +public class BusinessException extends RuntimeException { + private static final long serialVersionUID = 1L; + + /** + *  错误码 + */ + protected Integer errorCode; + + /** + *  错误信息 + */ + protected String errorMsg; + + public BusinessException() { + super(); + } + + public BusinessException(String errorMsg) { + super(errorMsg); + this.errorMsg = errorMsg; + } + + public BusinessException(Integer errorCode, String errorMsg) { + super(errorMsg); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public BusinessException(Integer errorCode, String errorMsg, Throwable cause) { + super(errorMsg, cause); + this.errorCode = errorCode; + this.errorMsg = errorMsg; + } + + public BusinessException(ErrorEnum error) { + super(error.getErrorMsg()); + this.errorCode = error.getErrorCode(); + this.errorMsg = error.getErrorMsg(); + } + + @Override + public String getMessage() { + return errorMsg; + } + + @Override + public synchronized Throwable fillInStackTrace() { + return this; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java new file mode 100644 index 0000000..f7a1197 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/CommonErrorEnum.java @@ -0,0 +1,27 @@ +package com.abin.frequencycontrol.exception; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +@Getter +public enum CommonErrorEnum implements ErrorEnum { + + SYSTEM_ERROR(-1, "系统出小差了,请稍后再试哦~~"), + PARAM_VALID(-2, "参数校验失败{0}"), + FREQUENCY_LIMIT(-3, "请求太频繁了,请稍后再试哦~~"), + LOCK_LIMIT(-4, "请求太频繁了,请稍后再试哦~~"), + ; + private final Integer code; + private final String msg; + + @Override + public Integer getErrorCode() { + return this.code; + } + + @Override + public String getErrorMsg() { + return this.msg; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java new file mode 100644 index 0000000..fc3f2a9 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/ErrorEnum.java @@ -0,0 +1,8 @@ +package com.abin.frequencycontrol.exception; + +public interface ErrorEnum { + + Integer getErrorCode(); + + String getErrorMsg(); +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java new file mode 100644 index 0000000..9491dd9 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/FrequencyControlException.java @@ -0,0 +1,37 @@ +package com.abin.frequencycontrol.exception; + +import lombok.Data; + +/** + * 自定义限流异常 + */ +@Data +public class FrequencyControlException extends RuntimeException { + + private static final long serialVersionUID = 1L; + + /** + *  错误码 + */ + protected Integer errorCode; + + /** + *  错误信息 + */ + protected String errorMsg; + + public FrequencyControlException() { + super(); + } + + public FrequencyControlException(String errorMsg) { + super(errorMsg); + this.errorMsg = errorMsg; + } + + public FrequencyControlException(ErrorEnum error) { + super(error.getErrorMsg()); + this.errorCode = error.getErrorCode(); + this.errorMsg = error.getErrorMsg(); + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java new file mode 100644 index 0000000..61849f3 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/exception/HttpErrorEnum.java @@ -0,0 +1,40 @@ +package com.abin.frequencycontrol.exception; + +import cn.hutool.http.ContentType; +import cn.hutool.json.JSONUtil; +import com.abin.frequencycontrol.domain.vo.response.ApiResult; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.Charset; + +/** + * Description: 业务校验异常码6 + */ +@AllArgsConstructor +@Getter +public enum HttpErrorEnum implements ErrorEnum { + ACCESS_DENIED(401, "登录失效,请重新登录"), + ; + private Integer httpCode; + private String msg; + + @Override + public Integer getErrorCode() { + return httpCode; + } + + @Override + public String getErrorMsg() { + return msg; + } + + public void sendHttpError(HttpServletResponse response) throws IOException { + response.setStatus(this.getErrorCode()); + ApiResult responseData = ApiResult.fail(this); + response.setContentType(ContentType.JSON.toString(Charset.forName("UTF-8"))); + response.getWriter().write(JSONUtil.toJsonStr(responseData)); + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/CollectorInterceptor.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/CollectorInterceptor.java new file mode 100644 index 0000000..7882534 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/CollectorInterceptor.java @@ -0,0 +1,37 @@ +package com.abin.frequencycontrol.intecepter; + +import cn.hutool.extra.servlet.ServletUtil; +import com.abin.frequencycontrol.domain.dto.RequestInfo; +import com.abin.frequencycontrol.util.RequestHolder; +import lombok.extern.slf4j.Slf4j; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Optional; + +/** + * 信息收集的拦截器 + */ +@Order(1) +@Slf4j +@Component +public class CollectorInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + RequestInfo info = new RequestInfo(); + info.setUid(Optional.ofNullable(request.getAttribute(TokenInterceptor.ATTRIBUTE_UID)).map(Object::toString).map(Long::parseLong).orElse(null)); + info.setIp(ServletUtil.getClientIP(request)); + RequestHolder.set(info); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + RequestHolder.remove(); + } + +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/TokenInterceptor.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/TokenInterceptor.java new file mode 100644 index 0000000..f326a59 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/intecepter/TokenInterceptor.java @@ -0,0 +1,69 @@ +package com.abin.frequencycontrol.intecepter; + +import com.abin.frequencycontrol.exception.HttpErrorEnum; +import com.abin.mallchat.common.MDCKey; +import lombok.extern.slf4j.Slf4j; +import org.slf4j.MDC; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Objects; +import java.util.Optional; + +@Order(-2) +@Slf4j +@Component +public class TokenInterceptor implements HandlerInterceptor { + + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String AUTHORIZATION_SCHEMA = "Bearer "; + public static final String ATTRIBUTE_UID = "uid"; +// +// @Autowired +// private LoginService loginService; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + //获取用户登录token + String token = getToken(request); +// Long validUid = loginService.getValidUid(token); + Long validUid = 1L; + if (Objects.nonNull(validUid)) {//有登录态 + request.setAttribute(ATTRIBUTE_UID, validUid); + } else { + boolean isPublicURI = isPublicURI(request.getRequestURI()); + if (!isPublicURI) {//又没有登录态,又不是公开路径,直接401 + HttpErrorEnum.ACCESS_DENIED.sendHttpError(response); + return false; + } + } + MDC.put(MDCKey.UID, String.valueOf(validUid)); + return true; + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + MDC.remove(MDCKey.UID); + } + + /** + * 判断是不是公共方法,可以未登录访问的 + * + * @param requestURI + */ + private boolean isPublicURI(String requestURI) { + String[] split = requestURI.split("/"); + return split.length > 2 && "public".equals(split[3]); + } + + private String getToken(HttpServletRequest request) { + String header = request.getHeader(AUTHORIZATION_HEADER); + return Optional.ofNullable(header) + .filter(h -> h.startsWith(AUTHORIZATION_SCHEMA)) + .map(h -> h.substring(AUTHORIZATION_SCHEMA.length())) + .orElse(null); + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java new file mode 100644 index 0000000..5fd990c --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/mannager/TokenBucketManager.java @@ -0,0 +1,51 @@ +package com.abin.frequencycontrol.mannager; + +import com.abin.frequencycontrol.domain.dto.TokenBucketDTO; +import org.springframework.stereotype.Component; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.ReentrantLock; + +@Component +public class TokenBucketManager { + + private final Map tokenBucketMap = new ConcurrentHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + + public void createTokenBucket(String key, long capacity, double refillRate) { + lock.lock(); + try { + if (!tokenBucketMap.containsKey(key)) { + TokenBucketDTO tokenBucket = new TokenBucketDTO(capacity, refillRate); + tokenBucketMap.put(key, tokenBucket); + } + } finally { + lock.unlock(); + } + } + + public void removeTokenBucket(String key) { + lock.lock(); + try { + tokenBucketMap.remove(key); + } finally { + lock.unlock(); + } + } + + public boolean tryAcquire(String key, int permits) { + TokenBucketDTO tokenBucket = tokenBucketMap.get(key); + if (tokenBucket != null) { + return tokenBucket.tryAcquire(permits); + } + return false; + } + + public void deductionToken(String key, int permits) { + TokenBucketDTO tokenBucket = tokenBucketMap.get(key); + if (tokenBucket != null) { + tokenBucket.deductionToken(permits); + } + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java new file mode 100644 index 0000000..14cfdab --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/AbstractFrequencyControlService.java @@ -0,0 +1,121 @@ +package com.abin.frequencycontrol.service.frequencycontrol; + +import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO; +import com.abin.frequencycontrol.exception.CommonErrorEnum; +import com.abin.frequencycontrol.exception.FrequencyControlException; +import com.abin.frequencycontrol.util.AssertUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; + +import javax.annotation.PostConstruct; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * 抽象类频控服务 其他类如果要实现限流服务 直接注入使用通用限流类 后期会通过继承此类实现令牌桶等算法 + * + * @param + */ +@Slf4j +public abstract class AbstractFrequencyControlService { + + @PostConstruct + protected void registerMyselfToFactory() { + FrequencyControlStrategyFactory.registerFrequencyController(getStrategyName(), this); + } + + /** + * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value + * @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑 + * @return 业务方法执行的返回值 + * @throws Throwable + */ + private T executeWithFrequencyControlMap(Map frequencyControlMap, SupplierThrowWithoutParam supplier) throws Throwable { + if (reachRateLimit(frequencyControlMap)) { + throw new FrequencyControlException(CommonErrorEnum.FREQUENCY_LIMIT); + } + try { + return supplier.get(); + } finally { + //不管成功还是失败,都增加次数 + addFrequencyControlStatisticsCount(frequencyControlMap); + } + } + + + /** + * 多限流策略的编程式调用方法 无参的调用方法 + * + * @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序 + * @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑 + * @return 业务方法执行的返回值 + * @throws Throwable 被限流或者限流策略定义错误 + */ + @SuppressWarnings("unchecked") + public T executeWithFrequencyControlList(List frequencyControlList, SupplierThrowWithoutParam supplier) throws Throwable { + boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey())); + AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值"); + Map frequencyControlDTOMap = frequencyControlList.stream().collect(Collectors.groupingBy(K::getKey, Collectors.collectingAndThen(Collectors.toList(), list -> list.get(0)))); + return executeWithFrequencyControlMap(frequencyControlDTOMap, supplier); + } + + /** + * 单限流策略的调用方法-编程式调用 + * + * @param frequencyControl 单个频控对象 + * @param supplier 服务提供着 + * @return 业务方法执行结果 + * @throws Throwable + */ + public T executeWithFrequencyControl(K frequencyControl, SupplierThrowWithoutParam supplier) throws Throwable { + return executeWithFrequencyControlList(Collections.singletonList(frequencyControl), supplier); + } + + + @FunctionalInterface + public interface SupplierThrowWithoutParam { + + /** + * Gets a result. + * + * @return a result + */ + T get() throws Throwable; + } + + @FunctionalInterface + public interface Executor { + + /** + * Gets a result. + * + * @return a result + */ + void execute() throws Throwable; + } + + /** + * 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断 + * + * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value + * @return true-方法被限流 false-方法没有被限流 + */ + protected abstract boolean reachRateLimit(Map frequencyControlMap); + + /** + * 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑 + * + * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value + */ + protected abstract void addFrequencyControlStatisticsCount(Map frequencyControlMap); + + /** + * 获取策略名称 + * + * @return 策略名称 + */ + protected abstract String getStrategyName(); + +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java new file mode 100644 index 0000000..6e14dff --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlStrategyFactory.java @@ -0,0 +1,46 @@ +package com.abin.frequencycontrol.service.frequencycontrol; + + +import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 限流策略工厂 + */ +public class FrequencyControlStrategyFactory { + + /** + * 限流策略集合 + */ + static Map> frequencyControlServiceStrategyMap = new ConcurrentHashMap<>(8); + + /** + * 将策略类放入工厂 + * + * @param strategyName 策略名称 + * @param abstractFrequencyControlService 策略类 + */ + public static void registerFrequencyController(String strategyName, AbstractFrequencyControlService abstractFrequencyControlService) { + frequencyControlServiceStrategyMap.put(strategyName, abstractFrequencyControlService); + } + + /** + * 根据名称获取策略类 + * + * @param strategyName 策略名称 + * @return 对应的限流策略类 + */ + @SuppressWarnings("unchecked") + public static AbstractFrequencyControlService getFrequencyControllerByName(String strategyName) { + return (AbstractFrequencyControlService) frequencyControlServiceStrategyMap.get(strategyName); + } + + /** + * 构造器私有 + */ + private FrequencyControlStrategyFactory() { + + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java new file mode 100644 index 0000000..cba9c52 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/FrequencyControlUtil.java @@ -0,0 +1,60 @@ +package com.abin.frequencycontrol.service.frequencycontrol; + +import com.abin.frequencycontrol.domain.dto.FrequencyControlDTO; +import com.abin.frequencycontrol.util.AssertUtil; +import org.apache.commons.lang3.ObjectUtils; + +import java.util.List; + +/** + * 限流工具类 提供编程式的限流调用方法 + */ +public class FrequencyControlUtil { + + /** + * 单限流策略的调用方法-编程式调用 + * + * @param strategyName 策略名称 + * @param frequencyControl 单个频控对象 + * @param supplier 服务提供着 + * @return 业务方法执行结果 + * @throws Throwable + */ + public static T executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.SupplierThrowWithoutParam supplier) throws Throwable { + AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName); + return frequencyController.executeWithFrequencyControl(frequencyControl, supplier); + } + + public static void executeWithFrequencyControl(String strategyName, K frequencyControl, AbstractFrequencyControlService.Executor executor) throws Throwable { + AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName); + frequencyController.executeWithFrequencyControl(frequencyControl, () -> { + executor.execute(); + return null; + }); + } + + + /** + * 多限流策略的编程式调用方法调用方法 + * + * @param strategyName 策略名称 + * @param frequencyControlList 频控列表 包含每一个频率控制的定义以及顺序 + * @param supplier 函数式入参-代表每个频控方法执行的不同的业务逻辑 + * @return 业务方法执行的返回值 + * @throws Throwable 被限流或者限流策略定义错误 + */ + public static T executeWithFrequencyControlList(String strategyName, List frequencyControlList, AbstractFrequencyControlService.SupplierThrowWithoutParam supplier) throws Throwable { + boolean existsFrequencyControlHasNullKey = frequencyControlList.stream().anyMatch(frequencyControl -> ObjectUtils.isEmpty(frequencyControl.getKey())); + AssertUtil.isFalse(existsFrequencyControlHasNullKey, "限流策略的Key字段不允许出现空值"); + AbstractFrequencyControlService frequencyController = FrequencyControlStrategyFactory.getFrequencyControllerByName(strategyName); + return frequencyController.executeWithFrequencyControlList(frequencyControlList, supplier); + } + + /** + * 构造器私有 + */ + private FrequencyControlUtil() { + + } + +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java new file mode 100644 index 0000000..63a59ed --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/SlidingWindowFrequencyController.java @@ -0,0 +1,64 @@ +package com.abin.frequencycontrol.service.frequencycontrol.strategy; + +import com.abin.mallchat.common.FrequencyControlConstant; +import com.abin.mallchat.utils.RedisUtils; +import com.abin.frequencycontrol.domain.dto.SlidingWindowDTO; +import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + + +/** + * 抽象类频控服务 -使用redis实现 滑动窗口是一种更加灵活的频率控制策略,它在一个滑动的时间窗口内限制操作的发生次数 + */ +@Slf4j +@Service +public class SlidingWindowFrequencyController extends AbstractFrequencyControlService { + @Override + protected boolean reachRateLimit(Map frequencyControlMap) { + // 批量获取redis统计的值 + List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet()); + for (int i = 0; i < frequencyKeys.size(); i++) { + String key = frequencyKeys.get(i); + SlidingWindowDTO controlDTO = frequencyControlMap.get(key); + // 获取窗口时间内计数 + Long count = RedisUtils.ZSetGet(key); + int frequencyControlCount = controlDTO.getCount(); + if (Objects.nonNull(count) && count >= frequencyControlCount) { + //频率超过了 + log.warn("frequencyControl limit key:{},count:{}", key, count); + return true; + } + } + return false; + } + + @Override + protected void addFrequencyControlStatisticsCount(Map frequencyControlMap) { + List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet()); + for (int i = 0; i < frequencyKeys.size(); i++) { + String key = frequencyKeys.get(i); + SlidingWindowDTO controlDTO = frequencyControlMap.get(key); + // 窗口最小周期转秒 + long period = controlDTO.getUnit().toMillis(controlDTO.getPeriod()); + long current = System.currentTimeMillis(); + // 窗口大小 单位 秒 + long length = period * controlDTO.getWindowSize(); + long start = current - length; +// long expireTime = length + period; + RedisUtils.ZSetAddAndExpire(key, start, length, current); + } + } + + @Override + protected String getStrategyName() { + return FrequencyControlConstant.SLIDING_WINDOW; + + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java new file mode 100644 index 0000000..a65fd40 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TokenBucketFrequencyController.java @@ -0,0 +1,54 @@ +package com.abin.frequencycontrol.service.frequencycontrol.strategy; + +import com.abin.mallchat.common.FrequencyControlConstant; +import com.abin.frequencycontrol.domain.dto.TokenBucketDTO; +import com.abin.frequencycontrol.mannager.TokenBucketManager; +import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + + +/** + * 抽象类频控服务 -使用redis实现 维护一个令牌桶来限制操作的发生次数 + */ +@Slf4j +@Service +public class TokenBucketFrequencyController extends AbstractFrequencyControlService { + + @Autowired + private TokenBucketManager tokenBucketManager; + + @Override + protected boolean reachRateLimit(Map frequencyControlMap) { + // 批量获取redis统计的值 + List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet()); + for (int i = 0; i < frequencyKeys.size(); i++) { + String key = frequencyKeys.get(i); + // 获取 1 个令牌 + return tokenBucketManager.tryAcquire(key, 1); + } + return false; + } + + @Override + protected void addFrequencyControlStatisticsCount(Map frequencyControlMap) { + List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet()); + for (int i = 0; i < frequencyKeys.size(); i++) { + String key = frequencyKeys.get(i); + TokenBucketDTO tokenBucketDTO = frequencyControlMap.get(key); + tokenBucketManager.createTokenBucket(key, tokenBucketDTO.getCapacity(), tokenBucketDTO.getRefillRate()); + // 扣减 1 个令牌 + tokenBucketManager.deductionToken(key, 1); + } + } + + @Override + protected String getStrategyName() { + return FrequencyControlConstant.TOKEN_BUCKET; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java new file mode 100644 index 0000000..d25f142 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/service/frequencycontrol/strategy/TotalCountWithInFixTimeFrequencyController.java @@ -0,0 +1,63 @@ +package com.abin.frequencycontrol.service.frequencycontrol.strategy; + + +import com.abin.mallchat.common.FrequencyControlConstant; +import com.abin.mallchat.utils.RedisUtils; +import com.abin.frequencycontrol.domain.dto.FixedWindowDTO; +import com.abin.frequencycontrol.service.frequencycontrol.AbstractFrequencyControlService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Objects; + + +/** + * 抽象类频控服务 -使用redis实现 固定时间内不超过固定次数的限流类 + */ +@Slf4j +@Service +public class TotalCountWithInFixTimeFrequencyController extends AbstractFrequencyControlService { + + + /** + * 是否达到限流阈值 子类实现 每个子类都可以自定义自己的限流逻辑判断 + * + * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value + * @return true-方法被限流 false-方法没有被限流 + */ + @Override + protected boolean reachRateLimit(Map frequencyControlMap) { + //批量获取redis统计的值 + List frequencyKeys = new ArrayList<>(frequencyControlMap.keySet()); + List countList = RedisUtils.mget(frequencyKeys, Integer.class); + for (int i = 0; i < frequencyKeys.size(); i++) { + String key = frequencyKeys.get(i); + Integer count = countList.get(i); + int frequencyControlCount = frequencyControlMap.get(key).getCount(); + if (Objects.nonNull(count) && count >= frequencyControlCount) { + //频率超过了 + log.warn("frequencyControl limit key:{},count:{}", key, count); + return true; + } + } + return false; + } + + /** + * 增加限流统计次数 子类实现 每个子类都可以自定义自己的限流统计信息增加的逻辑 + * + * @param frequencyControlMap 定义的注解频控 Map中的Key-对应redis的单个频控的Key Map中的Value-对应redis的单个频控的Key限制的Value + */ + @Override + protected void addFrequencyControlStatisticsCount(Map frequencyControlMap) { + frequencyControlMap.forEach((k, v) -> RedisUtils.inc(k, v.getTime(), v.getUnit())); + } + + @Override + protected String getStrategyName() { + return FrequencyControlConstant.TOTAL_COUNT_WITH_IN_FIX_TIME; + } +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java new file mode 100644 index 0000000..c4da534 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/AssertUtil.java @@ -0,0 +1,159 @@ +package com.abin.frequencycontrol.util; + +import cn.hutool.core.util.ObjectUtil; +import com.abin.frequencycontrol.exception.BusinessErrorEnum; +import com.abin.frequencycontrol.exception.BusinessException; +import com.abin.frequencycontrol.exception.CommonErrorEnum; +import com.abin.frequencycontrol.exception.ErrorEnum; +import org.hibernate.validator.HibernateValidator; + +import javax.validation.ConstraintViolation; +import javax.validation.Validation; +import javax.validation.Validator; +import java.text.MessageFormat; +import java.util.*; + +/** + * 校验工具类 + */ +public class AssertUtil { + + /** + * 校验到失败就结束 + */ + private static Validator failFastValidator = Validation.byProvider(HibernateValidator.class) + .configure() + .failFast(true) + .buildValidatorFactory().getValidator(); + + /** + * 全部校验 + */ + private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator(); + + /** + * 注解验证参数(校验到失败就结束) + * @param obj + */ + public static void fastFailValidate(T obj) { + Set> constraintViolations = failFastValidator.validate(obj); + if (constraintViolations.size() > 0) { + throwException(CommonErrorEnum.PARAM_VALID,constraintViolations.iterator().next().getMessage()); + } + } + + /** + * 注解验证参数(全部校验,抛出异常) + * @param obj + */ + public static void allCheckValidateThrow(T obj) { + Set> constraintViolations = validator.validate(obj); + if (constraintViolations.size() > 0) { + StringBuilder errorMsg = new StringBuilder(); + Iterator> iterator = constraintViolations.iterator(); + while (iterator.hasNext()) { + ConstraintViolation violation = iterator.next(); + //拼接异常信息 + errorMsg.append(violation.getPropertyPath().toString()).append(":").append(violation.getMessage()).append(","); + } + //去掉最后一个逗号 + throwException(CommonErrorEnum.PARAM_VALID, errorMsg.toString().substring(0, errorMsg.length() - 1)); + } + } + + + /** + * 注解验证参数(全部校验,返回异常信息集合) + * @param obj + */ + public static Map allCheckValidate(T obj) { + Set> constraintViolations = validator.validate(obj); + if (constraintViolations.size() > 0) { + Map errorMessages= new HashMap<>(); + Iterator> iterator = constraintViolations.iterator(); + while (iterator.hasNext()) { + ConstraintViolation violation = iterator.next(); + errorMessages.put(violation.getPropertyPath().toString(),violation.getMessage()); + } + return errorMessages; + } + return new HashMap<>(); + } + + //如果不是true,则抛异常 + public static void isTrue(boolean expression, String msg) { + if (!expression) { + throwException(msg); + } + } + + public static void isTrue(boolean expression, ErrorEnum errorEnum, Object... args) { + if (!expression) { + throwException(errorEnum, args); + } + } + + //如果是true,则抛异常 + public static void isFalse(boolean expression, String msg) { + if (expression) { + throwException(msg); + } + } + + //如果是true,则抛异常 + public static void isFalse(boolean expression, ErrorEnum errorEnum, Object... args) { + if (expression) { + throwException(errorEnum, args); + } + } + + //如果不是非空对象,则抛异常 + public static void isNotEmpty(Object obj, String msg) { + if (isEmpty(obj)) { + throwException(msg); + } + } + + //如果不是非空对象,则抛异常 + public static void isNotEmpty(Object obj, ErrorEnum errorEnum, Object... args) { + if (isEmpty(obj)) { + throwException(errorEnum, args); + } + } + + //如果不是非空对象,则抛异常 + public static void isEmpty(Object obj, String msg) { + if (!isEmpty(obj)) { + throwException(msg); + } + } + + public static void equal(Object o1, Object o2, String msg) { + if (!ObjectUtil.equal(o1, o2)) { + throwException(msg); + } + } + + public static void notEqual(Object o1, Object o2, String msg) { + if (ObjectUtil.equal(o1, o2)) { + throwException(msg); + } + } + + private static boolean isEmpty(Object obj) { + return ObjectUtil.isEmpty(obj); + } + + private static void throwException(String msg) { + throwException(null, msg); + } + + private static void throwException(ErrorEnum errorEnum, Object... arg) { + if (Objects.isNull(errorEnum)) { + errorEnum = BusinessErrorEnum.BUSINESS_ERROR; + } + throw new BusinessException(errorEnum.getErrorCode(), MessageFormat.format(errorEnum.getErrorMsg(), arg)); + } + + +} diff --git a/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java new file mode 100644 index 0000000..fb8ec10 --- /dev/null +++ b/mallchat-tools/mallchat-frequency-control/src/main/java/com/abin/frequencycontrol/util/RequestHolder.java @@ -0,0 +1,24 @@ +package com.abin.frequencycontrol.util; + + +import com.abin.frequencycontrol.domain.dto.RequestInfo; + +/** + * 请求上下文 + */ +public class RequestHolder { + + private static final ThreadLocal threadLocal = new ThreadLocal<>(); + + public static void set(RequestInfo requestInfo) { + threadLocal.set(requestInfo); + } + + public static RequestInfo get() { + return threadLocal.get(); + } + + public static void remove() { + threadLocal.remove(); + } +} diff --git a/mallchat-tools/pom.xml b/mallchat-tools/pom.xml index 1a81e3b..0094600 100644 --- a/mallchat-tools/pom.xml +++ b/mallchat-tools/pom.xml @@ -15,7 +15,8 @@ mallchat-redis mallchat-transaction mallchat-common-starter + mallchat-frequency-control - \ No newline at end of file +