+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package io.github.ehlxr.redis;
+
+/**
+ * redis lock/滑动时间窗口限流
+ *
+ * @author ehlxr
+ * @since 2021-07-15 22:42.
+ */
+public interface RedisDAO {
+
+ /**
+ * 释放分布式锁时使用的 lua 脚本,保证原子性
+ *
+ * if (redis.call('get', KEYS[1]) == ARGV[1])
+ * then
+ * return redis.call('del', KEYS[1])
+ * else
+ * return 0
+ * end
+ */
+ String RELEASE_LOCK_LUA = "if (redis.call('get', KEYS[1]) == ARGV[1]) then return redis.call('del', KEYS[1]) else return 0 end";
+
+ /**
+ * 滑动窗口限流使用的 lua 脚本,保证原子性
+ *
+ * local key = KEYS[1];
+ * local index = tonumber(ARGV[1]);
+ * local time_window = tonumber(ARGV[2]);
+ * local now_time = tonumber(ARGV[3]);
+ * local far_time = redis.call('lindex', key, index);
+ * if (not far_time)
+ * then
+ * redis.call('lpush', key, now_time);
+ * redis.call('pexpire', key, time_window+1000);
+ * return 1;
+ * end
+ * if (now_time - far_time > time_window)
+ * then
+ * redis.call('rpop', key);
+ * redis.call('lpush', key, now_time);
+ * redis.call('pexpire', key, time_window+1000);
+ * return 1;
+ * else
+ * return 0;
+ * end
+ */
+ String SLIDE_WINDOW_LUA = "local key = KEYS[1];\n" + "local index = tonumber(ARGV[1]);\n" + "local time_window = tonumber(ARGV[2]);\n" + "local now_time = tonumber(ARGV[3]);\n" + "local far_time = redis.call('lindex', key, index);\n" + "if (not far_time)\n" + "then\n" + " redis.call('lpush', key, now_time);\n" + " redis.call('pexpire', key, time_window+1000);\n" + " return 1;\n" + "end\n" + "\n" + "if (now_time - far_time > time_window)\n" + "then\n" + " redis.call('rpop', key);\n" + " redis.call('lpush', key, now_time);\n" + " redis.call('pexpire', key, time_window+1000);\n" + " return 1;\n" + "else\n" + " return 0;\n" + "end";
+
+ /**
+ * 获取分布式锁
+ *
+ * @param logId 日志 id
+ * @param key key
+ * @param value value,需要保证全局唯一,用来删除分布式锁时判断身份使用
+ * @param expireTime 锁过期时间,毫秒,防止业务崩溃未删除锁,导致死锁
+ * @return 是否获取成功锁
+ */
+ Boolean getDistributedLock(String key, String value, long expireTime);
+
+ /**
+ * 释放分布式锁
+ *
+ * @param logId 日志 id
+ * @param key key
+ * @param value value,需要和获取锁时传入的一致
+ * @return 是否释放成功锁
+ */
+ Boolean releaseDistributedLock(String key, String value);
+
+ /**
+ * 分布式限流队列,在时间窗口内(包含该时间点),判断是否达到限流的阀值
+ * 本接口实现的方法通过加锁避免并发问题,性能不高。只是为了说明限流逻辑如何实现
+ *
+ * @param logId 日志 id
+ * @param key key
+ * @param count 限流阀值
+ * @param timeWindow 限流时间窗口
+ * @return 是否允许通过(通过即不进行限流)
+ */
+ Boolean slideWindow(String key, int count, long timeWindow);
+
+ /**
+ * 分布式限流队列,在时间窗口内(包含该时间点),判断是否达到限流的阀值
+ * 本接口实现的方法通过 Lua 脚本避免并发问题,性能较高。
+ *
+ * @param logId 日志 id
+ * @param key key
+ * @param count 限流阀值
+ * @param timeWindow 限流时间窗口
+ * @return 是否允许通过(通过即不进行限流)
+ */
+ Boolean slideWindowLua(String key, int count, long timeWindow);
+
+}
+
+
diff --git a/src/main/java/io/github/ehlxr/redis/impl/JedisDAOImpl.java b/src/main/java/io/github/ehlxr/redis/impl/JedisDAOImpl.java
new file mode 100644
index 0000000..5a6249f
--- /dev/null
+++ b/src/main/java/io/github/ehlxr/redis/impl/JedisDAOImpl.java
@@ -0,0 +1,129 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright © 2020 xrv
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package io.github.ehlxr.redis.impl;
+
+import io.github.ehlxr.redis.RedisDAO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import redis.clients.jedis.JedisCluster;
+import redis.clients.jedis.params.SetParams;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * redis lock/滑动时间窗口限流
+ *
+ * @author ehlxr
+ * @since 2021-07-15 22:44.
+ */
+public class JedisDAOImpl implements RedisDAO {
+ private final Logger log = LoggerFactory.getLogger(JedisDAOImpl.class);
+
+ @Autowired
+ private JedisCluster jc;
+
+ @Override
+ public Boolean getDistributedLock(String key, String value, long expireTime) {
+ String set = null;
+ try {
+ set = jc.set(key, value, SetParams.setParams().nx().px(expireTime));
+ log.debug("getLock redis key: {}, value: {}, expireTime: {}, result: {}", key, value, expireTime, set);
+ } catch (Exception e) {
+ log.error("getLock redis key: {}, value: {}, expireTime: {}", key, value, expireTime, e);
+ }
+ return "OK".equals(set);
+ }
+
+ @Override
+ public Boolean releaseDistributedLock(String key, String value) {
+ Object eval = null;
+ try {
+ eval = jc.eval(RELEASE_LOCK_LUA, Collections.singletonList(key), Collections.singletonList(value));
+ log.debug("releaseLock redis key: {}, value: {}, result: {}", key, value, eval);
+ } catch (Exception e) {
+ log.error("releaseLock redis key: {}, value: {}", key, value, e);
+ }
+ return Long.valueOf(1L).equals(eval);
+ }
+
+ @Override
+ public synchronized Boolean slideWindow(String key, int count, long timeWindow) {
+ if (count <= 0 || timeWindow <= 0) {
+ return false;
+ }
+ try {
+ // 获取当前时间
+ long nowTime = System.currentTimeMillis();
+ // 获取队列中,达到限流数量的位置,存储的时间戳
+ String farTime = jc.lindex(key, count - 1);
+ // 如果为空,说明限流队列还没满,则允许通过,并添加当前时间戳到队列开始位置
+ if (farTime == null) {
+ jc.lpush(key, String.valueOf(nowTime));
+ // 给限流队列增加过期时间,防止长时间不用导致内存一直占用
+ jc.pexpire(key, timeWindow + 1000L);
+ return true;
+ }
+
+ // 队列已满(达到限制次数),用当前时间戳 减去 最早添加的时间戳
+ if (nowTime - Long.parseLong(farTime) > timeWindow) {
+ // 若结果大于 timeWindow,则说明在 timeWindow 内,通过的次数小于等于 count
+ // 允许通过,并删除最早添加的时间戳,将当前时间添加到队列开始位置
+ jc.rpop(key);
+ jc.lpush(key, String.valueOf(nowTime));
+ // 给限流队列增加过期时间,防止长时间不用导致内存一直占用
+ jc.pexpire(key, timeWindow + 1000L);
+ return true;
+ }
+ // 若结果小于等于 timeWindow,则说明在 timeWindow 内,通过的次数大于 count
+ // 不允许通过
+ return false;
+ } catch (Exception e) {
+ log.error("[logId:{}]", e);
+ return false;
+ }
+ }
+
+ @Override
+ public Boolean slideWindowLua(String key, int count, long timeWindow) {
+ if (count <= 0 || timeWindow <= 0) {
+ return false;
+ }
+ Object eval = null;
+ try {
+ List argvList = new ArrayList<>();
+ argvList.add(String.valueOf(count - 1));
+ argvList.add(String.valueOf(timeWindow));
+ argvList.add(String.valueOf(System.currentTimeMillis()));
+ eval = jc.eval(SLIDE_WINDOW_LUA, Collections.singletonList(key), argvList);
+ log.debug("slideWindowLua redis key: {}, count: {}, timeWindow: {}, result: {}", key, count, timeWindow, eval);
+ } catch (Exception e) {
+ log.error("slideWindowLua redis key: {}, count: {}, timeWindow: {}", key, count, timeWindow, e);
+ }
+ return Long.valueOf(1L).equals(eval);
+ }
+}
diff --git a/src/main/java/io/github/ehlxr/redis/impl/LettuceDAOImpl.java b/src/main/java/io/github/ehlxr/redis/impl/LettuceDAOImpl.java
new file mode 100644
index 0000000..bf5f5b6
--- /dev/null
+++ b/src/main/java/io/github/ehlxr/redis/impl/LettuceDAOImpl.java
@@ -0,0 +1,114 @@
+/*
+ * The MIT License (MIT)
+ *
+ * Copyright © 2020 xrv
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package io.github.ehlxr.redis.impl;
+
+import io.github.ehlxr.redis.RedisDAO;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.ListOperations;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.core.script.RedisScript;
+
+import java.util.Collections;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * @author ehlxr
+ * @since 2021-07-15 22:51.
+ */
+public class LettuceDAOImpl implements RedisDAO {
+ private final Logger log = LoggerFactory.getLogger(LettuceDAOImpl.class);
+
+ @Autowired
+ private RedisTemplate rt;
+
+ @Override
+ public Boolean getDistributedLock(String key, String value, long expireTime) {
+ Boolean set = false;
+ try {
+ set = rt.opsForValue().setIfAbsent(key, value, expireTime, TimeUnit.MILLISECONDS);
+ log.debug("getLock redis key: {}, value: {}, expireTime: {}, result: {}", key, value, expireTime, set);
+ } catch (Exception e) {
+ log.error("getLock redis key: {}, value: {}, expireTime: {}", key, value, expireTime, e);
+ }
+ return set;
+ }
+
+ @Override
+ public Boolean releaseDistributedLock(String key, String value) {
+ Long execute = null;
+ try {
+ RedisScript redisScript = new DefaultRedisScript<>(RELEASE_LOCK_LUA, Long.class);
+ execute = rt.execute(redisScript, Collections.singletonList(key), value);
+ log.debug("releaseLock redis key: {}, value: {}, result: {}", key, value, execute);
+ } catch (Exception e) {
+ log.error("releaseLock redis key: {}, value: {}", key, value, e);
+ }
+ return Long.valueOf(1L).equals(execute);
+ }
+
+ @Override
+ public synchronized Boolean slideWindow(String key, int count, long timeWindow) {
+ try {
+ long nowTime = System.currentTimeMillis();
+ ListOperations list = rt.opsForList();
+ String farTime = list.index(key, count - 1);
+ if (farTime == null) {
+ list.leftPush(key, String.valueOf(nowTime));
+ rt.expire(key, timeWindow + 1000L, TimeUnit.MILLISECONDS);
+ return true;
+ }
+ if (nowTime - Long.parseLong(farTime) > timeWindow) {
+ list.rightPop(key);
+ list.leftPush(key, String.valueOf(nowTime));
+ rt.expire(key, timeWindow + 1000L, TimeUnit.MILLISECONDS);
+ return true;
+ }
+ return false;
+ } catch (Exception e) {
+ log.error("[logId:{}]", e);
+ return false;
+ }
+ }
+
+ @Override
+ public Boolean slideWindowLua(String key, int count, long timeWindow) {
+ if (count <= 0 || timeWindow <= 0) {
+ return false;
+ }
+ Long execute = null;
+ try {
+ RedisScript redisScript = new DefaultRedisScript<>(SLIDE_WINDOW_LUA, Long.class);
+ execute = rt.execute(redisScript, Collections.singletonList(key), String.valueOf(count - 1), String.valueOf(timeWindow), String.valueOf(System.currentTimeMillis()));
+ log.debug("slideWindowLua redis key: {}, count: {}, timeWindow: {}, result: {}", key, count, timeWindow, execute);
+ } catch (Exception e) {
+ log.error("slideWindowLua redis key: {}, count: {}, timeWindow: {}", key, count, timeWindow, e);
+ }
+ return Long.valueOf(1L).equals(execute);
+ }
+
+}