diff --git a/pom.xml b/pom.xml index d42614a..69c9a07 100644 --- a/pom.xml +++ b/pom.xml @@ -173,7 +173,13 @@ redis.clients jedis - 2.8.0 + 3.4.0 + + + + org.springframework.boot + spring-boot-starter-data-redis + 2.5.2 diff --git a/src/main/java/io/github/ehlxr/redis/RedisDAO.java b/src/main/java/io/github/ehlxr/redis/RedisDAO.java new file mode 100644 index 0000000..7f73809 --- /dev/null +++ b/src/main/java/io/github/ehlxr/redis/RedisDAO.java @@ -0,0 +1,120 @@ +/* + * 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; + +/** + * 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); + } + +}