This commit is contained in:
2025-11-28 16:23:32 +08:00
commit a9e0e16c29
826 changed files with 89805 additions and 0 deletions

View File

@@ -0,0 +1,848 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.cache;
import lombok.Getter;
import org.springblade.core.tool.utils.CollectionUtil;
import org.springblade.core.tool.utils.NumberUtil;
import org.springframework.data.redis.core.*;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
/**
* redis 工具
*
* @author L.cm
*/
@Getter
@SuppressWarnings("unchecked")
public class BladeRedis {
private final RedisTemplate<String, Object> redisTemplate;
private final StringRedisTemplate stringRedisTemplate;
private final ValueOperations<String, Object> valueOps;
private final HashOperations<String, Object, Object> hashOps;
private final ListOperations<String, Object> listOps;
private final SetOperations<String, Object> setOps;
private final ZSetOperations<String, Object> zSetOps;
public BladeRedis(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
this.redisTemplate = redisTemplate;
this.stringRedisTemplate = stringRedisTemplate;
Assert.notNull(redisTemplate, "redisTemplate is null");
valueOps = redisTemplate.opsForValue();
hashOps = redisTemplate.opsForHash();
listOps = redisTemplate.opsForList();
setOps = redisTemplate.opsForSet();
zSetOps = redisTemplate.opsForZSet();
}
/**
* 设置缓存
*
* @param cacheKey 缓存key
* @param value 缓存value
*/
public void set(CacheKey cacheKey, Object value) {
String key = cacheKey.getKey();
Duration expire = cacheKey.getExpire();
if (expire == null) {
set(key, value);
} else {
setEx(key, value, expire);
}
}
/**
* 存放 key value 对到 redis。
*/
public void set(String key, Object value) {
valueOps.set(key, value);
}
/**
* 存放 key value 对到 redis并将 key 的生存时间设为 seconds (以秒为单位)。
* 如果 key 已经存在, SETEX 命令将覆写旧值。
*/
public void setEx(String key, Object value, Duration timeout) {
valueOps.set(key, value, timeout);
}
/**
* 存放 key value 对到 redis并将 key 的生存时间设为 seconds (以秒为单位)。
* 如果 key 已经存在, SETEX 命令将覆写旧值。
*/
public void setEx(String key, Object value, Long seconds) {
valueOps.set(key, value, seconds, TimeUnit.SECONDS);
}
/**
* 返回 key 所关联的 value 值
* 如果 key 不存在那么返回特殊值 nil 。
*/
@Nullable
public <T> T get(String key) {
return (T) valueOps.get(key);
}
/**
* 获取cache 为 null 时使用加载器,然后设置缓存
*
* @param key cacheKey
* @param loader cache loader
* @param <T> 泛型
* @return 结果
*/
@Nullable
public <T> T get(String key, Supplier<T> loader) {
T value = this.get(key);
if (value != null) {
return value;
}
value = loader.get();
if (value == null) {
return null;
}
this.set(key, value);
return value;
}
/**
* 返回 key 所关联的 value 值
* 如果 key 不存在那么返回特殊值 nil 。
*/
@Nullable
public <T> T get(CacheKey cacheKey) {
return (T) valueOps.get(cacheKey.getKey());
}
/**
* 获取cache 为 null 时使用加载器,然后设置缓存
*
* @param cacheKey cacheKey
* @param loader cache loader
* @param <T> 泛型
* @return 结果
*/
@Nullable
public <T> T get(CacheKey cacheKey, Supplier<T> loader) {
String key = cacheKey.getKey();
T value = this.get(key);
if (value != null) {
return value;
}
value = loader.get();
if (value == null) {
return null;
}
this.set(cacheKey, value);
return value;
}
/**
* 删除给定的一个 key
* 不存在的 key 会被忽略。
*/
public Boolean del(String key) {
return redisTemplate.delete(key);
}
/**
* 删除给定的一个 key
* 不存在的 key 会被忽略。
*/
public Boolean del(CacheKey key) {
return redisTemplate.delete(key.getKey());
}
/**
* 删除给定的多个 key
* 不存在的 key 会被忽略。
*/
public Long del(String... keys) {
return del(Arrays.asList(keys));
}
/**
* 删除给定的多个 key
* 不存在的 key 会被忽略。
*/
public Long del(Collection<String> keys) {
return redisTemplate.delete(keys);
}
/**
* 查找所有符合给定模式 pattern 的 key 。
* KEYS * 匹配数据库中所有 key 。
* KEYS h?llo 匹配 hello hallo 和 hxllo 等。
* KEYS h*llo 匹配 hllo 和 heeeeello 等。
* KEYS h[ae]llo 匹配 hello 和 hallo ,但不匹配 hillo 。
* 特殊符号用 \ 隔开
*/
public Set<String> keys(String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 同时设置一个或多个 key-value 对。
* 如果某个给定 key 已经存在,那么 MSET 会用新值覆盖原来的旧值,如果这不是你所希望的效果,请考虑使用 MSETNX 命令:它只会在所有给定 key 都不存在的情况下进行设置操作。
* MSET 是一个原子性(atomic)操作,所有给定 key 都会在同一时间内被设置,某些给定 key 被更新而另一些给定 key 没有改变的情况,不可能发生。
* <pre>
* 例子:
* Cache cache = RedisKit.use(); // 使用 Redis 的 cache
* cache.mset("k1", "v1", "k2", "v2"); // 放入多个 key value 键值对
* List list = cache.mget("k1", "k2"); // 利用多个键值得到上面代码放入的值
* </pre>
*/
public void mSet(Object... keysValues) {
valueOps.multiSet(CollectionUtil.toMap(keysValues));
}
/**
* 返回所有(一个或多个)给定 key 的值。
* 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。
*/
public List<Object> mGet(String... keys) {
return mGet(Arrays.asList(keys));
}
/**
* 返回所有(一个或多个)给定 key 的值。
* 如果给定的 key 里面,有某个 key 不存在,那么这个 key 返回特殊值 nil 。因此,该命令永不失败。
*/
public List<Object> mGet(Collection<String> keys) {
return valueOps.multiGet(keys);
}
/**
* 将 key 中储存的数字值减一。
* 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECR 操作。
* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
* 本操作的值限制在 64 位(bit)有符号数字表示之内。
* 关于递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。
*/
public Long decr(String key) {
return stringRedisTemplate.opsForValue().decrement(key);
}
/**
* 将 key 所储存的值减去减量 decrement 。
* 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 DECRBY 操作。
* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
* 本操作的值限制在 64 位(bit)有符号数字表示之内。
* 关于更多递增(increment) / 递减(decrement)操作的更多信息,请参见 INCR 命令。
*/
public Long decrBy(String key, long longValue) {
return stringRedisTemplate.opsForValue().decrement(key, longValue);
}
/**
* 将 key 中储存的数字值增一。
* 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。
* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
* 本操作的值限制在 64 位(bit)有符号数字表示之内。
*/
public Long incr(String key) {
return stringRedisTemplate.opsForValue().increment(key);
}
/**
* 将 key 所储存的值加上增量 increment 。
* 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。
* 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。
* 本操作的值限制在 64 位(bit)有符号数字表示之内。
* 关于递增(increment) / 递减(decrement)操作的更多信息,参见 INCR 命令。
*/
public Long incrBy(String key, long longValue) {
return stringRedisTemplate.opsForValue().increment(key, longValue);
}
/**
* 根据 key 获取递减的参数值
*/
public Long getDecr(String key) {
return NumberUtil.toLong(stringRedisTemplate.opsForValue().get(key));
}
/**
* 根据 key 获取递增的参数值
*/
public Long getIncr(String key) {
return NumberUtil.toLong(stringRedisTemplate.opsForValue().get(key));
}
/**
* 获取记数器的值
*/
public Long getCounter(String key) {
return Long.valueOf(String.valueOf(valueOps.get(key)));
}
/**
* 检查给定 key 是否存在。
*/
public Boolean exists(String key) {
return redisTemplate.hasKey(key);
}
/**
* 从当前数据库中随机返回(不删除)一个 key 。
*/
public String randomKey() {
return redisTemplate.randomKey();
}
/**
* 将 key 改名为 newkey 。
* 当 key 和 newkey 相同,或者 key 不存在时,返回一个错误。
* 当 newkey 已经存在时, RENAME 命令将覆盖旧值。
*/
public void rename(String oldkey, String newkey) {
redisTemplate.rename(oldkey, newkey);
}
/**
* 将当前数据库的 key 移动到给定的数据库 db 当中。
* 如果当前数据库(源数据库)和给定数据库(目标数据库)有相同名字的给定 key ,或者 key 不存在于当前数据库,那么 MOVE 没有任何效果。
* 因此,也可以利用这一特性,将 MOVE 当作锁(locking)原语(primitive)。
*/
public Boolean move(String key, int dbIndex) {
return redisTemplate.move(key, dbIndex);
}
/**
* 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。
* 在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。
*/
public Boolean expire(String key, long seconds) {
return redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
}
/**
* 为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。
* 在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。
*/
public Boolean expire(String key, Duration timeout) {
return expire(key, timeout.getSeconds());
}
/**
* EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
*/
public Boolean expireAt(String key, Date date) {
return redisTemplate.expireAt(key, date);
}
/**
* EXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。
*/
public Boolean expireAt(String key, long unixTime) {
return expireAt(key, new Date(unixTime));
}
/**
* 这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。
*/
public Boolean pexpire(String key, long milliseconds) {
return redisTemplate.expire(key, milliseconds, TimeUnit.MILLISECONDS);
}
/**
* 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。
* 当 key 存在但不是字符串类型时,返回一个错误。
*/
public <T> T getSet(String key, Object value) {
return (T) valueOps.getAndSet(key, value);
}
/**
* 移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。
*/
public Boolean persist(String key) {
return redisTemplate.persist(key);
}
/**
* 返回 key 所储存的值的类型。
*/
public String type(String key) {
return redisTemplate.type(key).code();
}
/**
* 以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。
*/
public Long ttl(String key) {
return redisTemplate.getExpire(key);
}
/**
* 这个命令类似于 TTL 命令,但它以毫秒为单位返回 key 的剩余生存时间,而不是像 TTL 命令那样,以秒为单位。
*/
public Long pttl(String key) {
return redisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
}
/**
* 将哈希表 key 中的域 field 的值设为 value 。
* 如果 key 不存在,一个新的哈希表被创建并进行 HSET 操作。
* 如果域 field 已经存在于哈希表中,旧值将被覆盖。
*/
public void hSet(String key, Object field, Object value) {
hashOps.put(key, field, value);
}
/**
* 同时将多个 field-value (域-值)对设置到哈希表 key 中。
* 此命令会覆盖哈希表中已存在的域。
* 如果 key 不存在,一个空哈希表被创建并执行 HMSET 操作。
*/
public void hMset(String key, Map<Object, Object> hash) {
hashOps.putAll(key, hash);
}
/**
* 返回哈希表 key 中给定域 field 的值。
*/
public <T> T hGet(String key, Object field) {
return (T) hashOps.get(key, field);
}
/**
* 返回哈希表 key 中,一个或多个给定域的值。
* 如果给定的域不存在于哈希表,那么返回一个 nil 值。
* 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
*/
public List hmGet(String key, Object... fields) {
return hmGet(key, Arrays.asList(fields));
}
/**
* 返回哈希表 key 中,一个或多个给定域的值。
* 如果给定的域不存在于哈希表,那么返回一个 nil 值。
* 因为不存在的 key 被当作一个空哈希表来处理,所以对一个不存在的 key 进行 HMGET 操作将返回一个只带有 nil 值的表。
*/
public List hmGet(String key, Collection<Object> hashKeys) {
return hashOps.multiGet(key, hashKeys);
}
/**
* 删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
*/
public Long hDel(String key, Object... fields) {
return hashOps.delete(key, fields);
}
/**
* 查看哈希表 key 中,给定域 field 是否存在。
*/
public Boolean hExists(String key, Object field) {
return hashOps.hasKey(key, field);
}
/**
* 返回哈希表 key 中,所有的域和值。
* 在返回值里,紧跟每个域名(field name)之后是域的值(value),所以返回值的长度是哈希表大小的两倍。
*/
public Map hGetAll(String key) {
return hashOps.entries(key);
}
/**
* 返回哈希表 key 中所有域的值。
*/
public List hVals(String key) {
return hashOps.values(key);
}
/**
* 返回哈希表 key 中的所有域。
* 底层实现此方法取名为 hfields 更为合适,在此仅为与底层保持一致
*/
public Set<Object> hKeys(String key) {
return hashOps.keys(key);
}
/**
* 返回哈希表 key 中域的数量。
*/
public Long hLen(String key) {
return hashOps.size(key);
}
/**
* 为哈希表 key 中的域 field 的值加上增量 increment 。
* 增量也可以为负数,相当于对给定域进行减法操作。
* 如果 key 不存在,一个新的哈希表被创建并执行 HINCRBY 命令。
* 如果域 field 不存在,那么在执行命令前,域的值被初始化为 0 。
* 对一个储存字符串值的域 field 执行 HINCRBY 命令将造成一个错误。
* 本操作的值被限制在 64 位(bit)有符号数字表示之内。
*/
public Long hIncrBy(String key, Object field, long value) {
return hashOps.increment(key, field, value);
}
/**
* 为哈希表 key 中的域 field 加上浮点数增量 increment 。
* 如果哈希表中没有域 field ,那么 HINCRBYFLOAT 会先将域 field 的值设为 0 ,然后再执行加法操作。
* 如果键 key 不存在,那么 HINCRBYFLOAT 会先创建一个哈希表,再创建域 field ,最后再执行加法操作。
* 当以下任意一个条件发生时,返回一个错误:
* 1:域 field 的值不是字符串类型(因为 redis 中的数字和浮点数都以字符串的形式保存,所以它们都属于字符串类型)
* 2:域 field 当前的值或给定的增量 increment 不能解释(parse)为双精度浮点数(double precision floating point number)
* HINCRBYFLOAT 命令的详细功能和 INCRBYFLOAT 命令类似,请查看 INCRBYFLOAT 命令获取更多相关信息。
*/
public Double hIncrByFloat(String key, Object field, double value) {
return hashOps.increment(key, field, value);
}
/**
* 返回列表 key 中,下标为 index 的元素。
* 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,
* 以 1 表示列表的第二个元素,以此类推。
* 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
* 如果 key 不是列表类型,返回一个错误。
*/
public <T> T lIndex(String key, long index) {
return (T) listOps.index(key, index);
}
/**
* 返回列表 key 的长度。
* 如果 key 不存在,则 key 被解释为一个空列表,返回 0 .
* 如果 key 不是列表类型,返回一个错误。
*/
public Long lLen(String key) {
return listOps.size(key);
}
/**
* 移除并返回列表 key 的头元素。
*/
public <T> T lPop(String key) {
return (T) listOps.leftPop(key);
}
/**
* 将一个或多个值 value 插入到列表 key 的表头
* 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表头: 比如说,
* 对空列表 mylist 执行命令 LPUSH mylist a b c ,列表的值将是 c b a
* 这等同于原子性地执行 LPUSH mylist a 、 LPUSH mylist b 和 LPUSH mylist c 三个命令。
* 如果 key 不存在,一个空列表会被创建并执行 LPUSH 操作。
* 当 key 存在但不是列表类型时,返回一个错误。
*/
public Long lPush(String key, Object... values) {
return listOps.leftPush(key, values);
}
/**
* 将列表 key 下标为 index 的元素的值设置为 value 。
* 当 index 参数超出范围,或对一个空列表( key 不存在)进行 LSET 时,返回一个错误。
* 关于列表下标的更多信息,请参考 LINDEX 命令。
*/
public void lSet(String key, long index, Object value) {
listOps.set(key, index, value);
}
/**
* 根据参数 count 的值,移除列表中与参数 value 相等的元素。
* count 的值可以是以下几种:
* count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
* count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
* count = 0 : 移除表中所有与 value 相等的值。
*/
public Long lRem(String key, long count, Object value) {
return listOps.remove(key, count, value);
}
/**
* 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定。
* 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
* 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
* <pre>
* 例子:
* 获取 list 中所有数据cache.lrange(listKey, 0, -1);
* 获取 list 中下标 1 到 3 的数据: cache.lrange(listKey, 1, 3);
* </pre>
*/
public List lRange(String key, long start, long end) {
return listOps.range(key, start, end);
}
/**
* 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
* 举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。
* 下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
* 你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
* 当 key 不是列表类型时,返回一个错误。
*/
public void lTrim(String key, long start, long end) {
listOps.trim(key, start, end);
}
/**
* 移除并返回列表 key 的尾元素。
*/
public <T> T rPop(String key) {
return (T) listOps.rightPop(key);
}
/**
* 将一个或多个值 value 插入到列表 key 的表尾(最右边)。
* 如果有多个 value 值,那么各个 value 值按从左到右的顺序依次插入到表尾:比如
* 对一个空列表 mylist 执行 RPUSH mylist a b c ,得出的结果列表为 a b c
* 等同于执行命令 RPUSH mylist a 、 RPUSH mylist b 、 RPUSH mylist c 。
* 如果 key 不存在,一个空列表会被创建并执行 RPUSH 操作。
* 当 key 存在但不是列表类型时,返回一个错误。
*/
public Long rPush(String key, Object... values) {
return listOps.rightPushAll(key, values);
}
/**
* 命令 RPOPLPUSH 在一个原子时间内,执行以下两个动作:
* 将列表 source 中的最后一个元素(尾元素)弹出,并返回给客户端。
* 将 source 弹出的元素插入到列表 destination ,作为 destination 列表的的头元素。
*/
public <T> T rPopLPush(String srcKey, String dstKey) {
return (T) listOps.rightPopAndLeftPush(srcKey, dstKey);
}
/**
* 将一个或多个 member 元素加入到集合 key 当中,已经存在于集合的 member 元素将被忽略。
* 假如 key 不存在,则创建一个只包含 member 元素作成员的集合。
* 当 key 不是集合类型时,返回一个错误。
*/
public Long sAdd(String key, Object... members) {
return setOps.add(key, members);
}
/**
* 移除并返回集合中的一个随机元素。
* 如果只想获取一个随机元素,但不想该元素从集合中被移除的话,可以使用 SRANDMEMBER 命令。
*/
public <T> T sPop(String key) {
return (T) setOps.pop(key);
}
/**
* 返回集合 key 中的所有成员。
* 不存在的 key 被视为空集合。
*/
public Set sMembers(String key) {
return setOps.members(key);
}
/**
* 判断 member 元素是否集合 key 的成员。
*/
public boolean sIsMember(String key, Object member) {
return setOps.isMember(key, member);
}
/**
* 返回多个集合的交集,多个集合由 keys 指定
*/
public Set sInter(String key, String otherKey) {
return setOps.intersect(key, otherKey);
}
/**
* 返回多个集合的交集,多个集合由 keys 指定
*/
public Set sInter(String key, Collection<String> otherKeys) {
return setOps.intersect(key, otherKeys);
}
/**
* 返回集合中的一个随机元素。
*/
public <T> T sRandMember(String key) {
return (T) setOps.randomMember(key);
}
/**
* 返回集合中的 count 个随机元素。
* 从 Redis 2.6 版本开始, SRANDMEMBER 命令接受可选的 count 参数:
* 如果 count 为正数,且小于集合基数,那么命令返回一个包含 count 个元素的数组,数组中的元素各不相同。
* 如果 count 大于等于集合基数,那么返回整个集合。
* 如果 count 为负数,那么命令返回一个数组,数组中的元素可能会重复出现多次,而数组的长度为 count 的绝对值。
* 该操作和 SPOP 相似,但 SPOP 将随机元素从集合中移除并返回,而 SRANDMEMBER 则仅仅返回随机元素,而不对集合进行任何改动。
*/
public List sRandMember(String key, int count) {
return setOps.randomMembers(key, count);
}
/**
* 移除集合 key 中的一个或多个 member 元素,不存在的 member 元素会被忽略。
*/
public Long sRem(String key, Object... members) {
return setOps.remove(key, members);
}
/**
* 返回多个集合的并集,多个集合由 keys 指定
* 不存在的 key 被视为空集。
*/
public Set sUnion(String key, String otherKey) {
return setOps.union(key, otherKey);
}
/**
* 返回多个集合的并集,多个集合由 keys 指定
* 不存在的 key 被视为空集。
*/
public Set sUnion(String key, Collection<String> otherKeys) {
return setOps.union(key, otherKeys);
}
/**
* 返回一个集合的全部成员,该集合是所有给定集合之间的差集。
* 不存在的 key 被视为空集。
*/
public Set sDiff(String key, String otherKey) {
return setOps.difference(key, otherKey);
}
/**
* 返回一个集合的全部成员,该集合是所有给定集合之间的差集。
* 不存在的 key 被视为空集。
*/
public Set sDiff(String key, Collection<String> otherKeys) {
return setOps.difference(key, otherKeys);
}
/**
* 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
* 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值,
* 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。
*/
public Boolean zAdd(String key, Object member, double score) {
return zSetOps.add(key, member, score);
}
/**
* 将一个或多个 member 元素及其 score 值加入到有序集 key 当中。
* 如果某个 member 已经是有序集的成员,那么更新这个 member 的 score 值,
* 并通过重新插入这个 member 元素,来保证该 member 在正确的位置上。
*/
public Long zAdd(String key, Map<Object, Double> scoreMembers) {
Set<ZSetOperations.TypedTuple<Object>> tuples = new HashSet<>();
scoreMembers.forEach((k, v) -> {
tuples.add(new DefaultTypedTuple<>(k, v));
});
return zSetOps.add(key, tuples);
}
/**
* 返回有序集 key 的基数。
*/
public Long zCard(String key) {
return zSetOps.zCard(key);
}
/**
* 返回有序集 key 中, score 值在 min 和 max 之间(默认包括 score 值等于 min 或 max )的成员的数量。
* 关于参数 min 和 max 的详细使用方法,请参考 ZRANGEBYSCORE 命令。
*/
public Long zCount(String key, double min, double max) {
return zSetOps.count(key, min, max);
}
/**
* 为有序集 key 的成员 member 的 score 值加上增量 increment 。
*/
public Double zIncrBy(String key, Object member, double score) {
return zSetOps.incrementScore(key, member, score);
}
/**
* 返回有序集 key 中,指定区间内的成员。
* 其中成员的位置按 score 值递增(从小到大)来排序。
* 具有相同 score 值的成员按字典序(lexicographical order )来排列。
* 如果你需要成员按 score 值递减(从大到小)来排列,请使用 ZREVRANGE 命令。
*/
public Set zRange(String key, long start, long end) {
return zSetOps.range(key, start, end);
}
/**
* 返回有序集 key 中,指定区间内的成员。
* 其中成员的位置按 score 值递减(从大到小)来排列。
* 具有相同 score 值的成员按字典序的逆序(reverse lexicographical order)排列。
* 除了成员按 score 值递减的次序排列这一点外, ZREVRANGE 命令的其他方面和 ZRANGE 命令一样。
*/
public Set zRevrange(String key, long start, long end) {
return zSetOps.reverseRange(key, start, end);
}
/**
* 返回有序集 key 中,所有 score 值介于 min 和 max 之间(包括等于 min 或 max )的成员。
* 有序集成员按 score 值递增(从小到大)次序排列。
*/
public Set zRangeByScore(String key, double min, double max) {
return zSetOps.rangeByScore(key, min, max);
}
/**
* 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递增(从小到大)顺序排列。
* 排名以 0 为底,也就是说, score 值最小的成员排名为 0 。
* 使用 ZREVRANK 命令可以获得成员按 score 值递减(从大到小)排列的排名。
*/
public Long zRank(String key, Object member) {
return zSetOps.rank(key, member);
}
/**
* 返回有序集 key 中成员 member 的排名。其中有序集成员按 score 值递减(从大到小)排序。
* 排名以 0 为底,也就是说, score 值最大的成员排名为 0 。
* 使用 ZRANK 命令可以获得成员按 score 值递增(从小到大)排列的排名。
*/
public Long zRevrank(String key, Object member) {
return zSetOps.reverseRank(key, member);
}
/**
* 移除有序集 key 中的一个或多个成员,不存在的成员将被忽略。
* 当 key 存在但不是有序集类型时,返回一个错误。
*/
public Long zRem(String key, Object... members) {
return zSetOps.remove(key, members);
}
/**
* 返回有序集 key 中,成员 member 的 score 值。
* 如果 member 元素不是有序集 key 的成员,或 key 不存在,返回 nil 。
*/
public Double zScore(String key, Object member) {
return zSetOps.score(key, member);
}
}

View File

@@ -0,0 +1,59 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.cache;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* cache key 封装
*
* @author L.cm
*/
@Getter
@ToString
@AllArgsConstructor
public class CacheKey {
/**
* redis key
*/
private final String key;
/**
* 超时时间 秒
*/
@Nullable
private Duration expire;
public CacheKey(String key) {
this.key = key;
}
}

View File

@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.cache;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.lang.Nullable;
import java.time.Duration;
/**
* cache key
*
* @author L.cm
*/
public interface ICacheKey {
/**
* 获取前缀
*
* @return key 前缀
*/
String getPrefix();
/**
* 超时时间
*
* @return 超时时间
*/
@Nullable
default Duration getExpire() {
return null;
}
/**
* 组装 cache key
*
* @param suffix 参数
* @return cache key
*/
default CacheKey getKey(Object... suffix) {
String prefix = this.getPrefix();
// 拼接参数
String key;
if (ObjectUtil.isEmpty(suffix)) {
key = prefix;
} else {
key = prefix.concat(StringUtil.join(suffix, StringPool.COLON));
}
Duration expire = this.getExpire();
return expire == null ? new CacheKey(key) : new CacheKey(key, expire);
}
}

View File

@@ -0,0 +1,123 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.jwt.config.JwtRedisConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.lang.Nullable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 扩展redis-cache支持注解cacheName添加超时时间
* <p>
*
* @author L.cm
*/
@AutoConfiguration(before = JwtRedisConfiguration.class)
@EnableConfigurationProperties(CacheProperties.class)
public class BladeRedisCacheAutoConfiguration {
/**
* 序列化方式
*/
private final RedisSerializer<Object> redisSerializer;
private final CacheProperties cacheProperties;
private final CacheManagerCustomizers customizerInvoker;
@Nullable
private final RedisCacheConfiguration redisCacheConfiguration;
BladeRedisCacheAutoConfiguration(RedisSerializer<Object> redisSerializer,
CacheProperties cacheProperties,
CacheManagerCustomizers customizerInvoker,
ObjectProvider<RedisCacheConfiguration> redisCacheConfiguration) {
this.redisSerializer = redisSerializer;
this.cacheProperties = cacheProperties;
this.customizerInvoker = customizerInvoker;
this.redisCacheConfiguration = redisCacheConfiguration.getIfAvailable();
}
@Primary
@Bean("redisCacheManager")
public RedisCacheManager redisCacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(connectionFactory);
RedisCacheConfiguration cacheConfiguration = this.determineConfiguration();
List<String> cacheNames = this.cacheProperties.getCacheNames();
Map<String, RedisCacheConfiguration> initialCaches = new LinkedHashMap<>();
if (!cacheNames.isEmpty()) {
Map<String, RedisCacheConfiguration> cacheConfigMap = new LinkedHashMap<>(cacheNames.size());
cacheNames.forEach(it -> cacheConfigMap.put(it, cacheConfiguration));
initialCaches.putAll(cacheConfigMap);
}
boolean allowInFlightCacheCreation = true;
boolean enableTransactions = false;
RedisAutoCacheManager cacheManager = new RedisAutoCacheManager(redisCacheWriter, cacheConfiguration, initialCaches, allowInFlightCacheCreation);
cacheManager.setTransactionAware(enableTransactions);
return this.customizerInvoker.customize(cacheManager);
}
private RedisCacheConfiguration determineConfiguration() {
if (this.redisCacheConfiguration != null) {
return this.redisCacheConfiguration;
} else {
CacheProperties.Redis redisProperties = this.cacheProperties.getRedis();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
}

View File

@@ -0,0 +1,95 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.time.Duration;
/**
* redis 配置
*
* @author L.cm
*/
@Getter
@Setter
@ConfigurationProperties(BladeRedisProperties.PREFIX)
public class BladeRedisProperties {
public static final String PREFIX = "blade.redis";
/**
* 序列化方式
*/
private SerializerType serializerType = SerializerType.ProtoStuff;
/**
* stream
*/
private Stream stream = new Stream();
public enum SerializerType {
/**
* 默认:ProtoStuff 序列化
*/
ProtoStuff,
/**
* json 序列化
*/
JSON,
/**
* jdk 序列化
*/
JDK
}
@Getter
@Setter
public static class Stream {
public static final String PREFIX = BladeRedisProperties.PREFIX + ".stream";
/**
* 是否开启 stream
*/
boolean enable = false;
/**
* consumer group默认服务名 + 环境
*/
String consumerGroup;
/**
* 消费者名称默认ip + 端口
*/
String consumerName;
/**
* poll 批量大小
*/
Integer pollBatchSize;
/**
* poll 超时时间
*/
Duration pollTimeout;
}
}

View File

@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* redis 序列化
*
* @author L.cm
*/
public interface BladeRedisSerializerConfigAble {
/**
* JSON序列化类型字段
*/
String TYPE_NAME = "@class";
/**
* 序列化接口
*
* @param properties 配置
* @return RedisSerializer
*/
RedisSerializer<Object> redisSerializer(BladeRedisProperties properties);
/**
* 默认的序列化方式
*
* @param properties 配置
* @return RedisSerializer
*/
default RedisSerializer<Object> defaultRedisSerializer(BladeRedisProperties properties) {
BladeRedisProperties.SerializerType serializerType = properties.getSerializerType();
if (BladeRedisProperties.SerializerType.JDK == serializerType) {
/**
* SpringBoot扩展了ClassLoader进行分离打包的时候使用到JdkSerializationRedisSerializer的地方
* 会因为ClassLoader的不同导致加载不到Class
* 指定使用项目的ClassLoader
*
* JdkSerializationRedisSerializer默认使用{@link sun.misc.Launcher.AppClassLoader}
* SpringBoot默认使用{@link org.springframework.boot.loader.LaunchedURLClassLoader}
*/
ClassLoader classLoader = this.getClass().getClassLoader();
return new JdkSerializationRedisSerializer(classLoader);
}
return new GenericJackson2JsonRedisSerializer(TYPE_NAME);
}
}

View File

@@ -0,0 +1,55 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.serializer.ProtoStuffSerializer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* ProtoStuff 序列化配置
*
* @author L.cm
*/
@AutoConfiguration(before = RedisTemplateConfiguration.class)
@ConditionalOnClass(name = "io.protostuff.Schema")
public class ProtoStuffSerializerConfiguration implements BladeRedisSerializerConfigAble {
@Bean
@ConditionalOnMissingBean
@Override
public RedisSerializer<Object> redisSerializer(BladeRedisProperties properties) {
if (BladeRedisProperties.SerializerType.ProtoStuff == properties.getSerializerType()) {
return new ProtoStuffSerializer();
}
return defaultRedisSerializer(properties);
}
}

View File

@@ -0,0 +1,73 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.ratelimiter.RedisRateLimiterAspect;
import org.springblade.core.redis.ratelimiter.RedisRateLimiterClient;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.scripting.support.ResourceScriptSource;
import java.util.List;
/**
* 基于 redis 的分布式限流自动配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnProperty(value = "blade.redis.rate-limiter.enabled", havingValue = "true")
public class RateLimiterAutoConfiguration {
private RedisScript<Long> redisRateLimiterScript() {
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/blade_rate_limiter.lua")));
redisScript.setResultType(Long.class);
return redisScript;
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterClient redisRateLimiter(StringRedisTemplate redisTemplate,
Environment environment) {
RedisScript<Long> redisRateLimiterScript = redisRateLimiterScript();
return new RedisRateLimiterClient(redisTemplate, redisRateLimiterScript, environment);
}
@Bean
@ConditionalOnMissingBean
public RedisRateLimiterAspect redisRateLimiterAspect(RedisRateLimiterClient rateLimiterClient) {
return new RedisRateLimiterAspect(rateLimiterClient);
}
}

View File

@@ -0,0 +1,73 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.tool.utils.StringPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.convert.DurationStyle;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
import java.time.Duration;
import java.time.temporal.ChronoUnit;
import java.util.Map;
/**
* redis cache 扩展cache name自动化配置
*
* @author L.cm
*/
public class RedisAutoCacheManager extends RedisCacheManager {
public RedisAutoCacheManager(RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration,
Map<String, RedisCacheConfiguration> initialCacheConfigurations, boolean allowInFlightCacheCreation) {
super(cacheWriter, defaultCacheConfiguration, initialCacheConfigurations, allowInFlightCacheCreation);
}
@NonNull
@Override
protected RedisCache createRedisCache(@NonNull String name, @Nullable RedisCacheConfiguration cacheConfig) {
if (StringUtil.isBlank(name) || !name.contains(StringPool.HASH)) {
return super.createRedisCache(name, cacheConfig);
}
String[] cacheArray = name.split(StringPool.HASH);
if (cacheArray.length < 2) {
return super.createRedisCache(name, cacheConfig);
}
String cacheName = cacheArray[0];
if (cacheConfig != null) {
Duration cacheAge = DurationStyle.detectAndParse(cacheArray[1], ChronoUnit.SECONDS);;
cacheConfig = cacheConfig.entryTtl(cacheAge);
}
return super.createRedisCache(cacheName, cacheConfig);
}
}

View File

@@ -0,0 +1,52 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizer;
import org.springframework.boot.autoconfigure.cache.CacheManagerCustomizers;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import java.util.List;
/**
* CacheManagerCustomizers配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnMissingBean(CacheManagerCustomizers.class)
public class RedisCacheManagerConfig {
@Bean
public CacheManagerCustomizers cacheManagerCustomizers(
ObjectProvider<List<CacheManagerCustomizer<?>>> customizers) {
return new CacheManagerCustomizers(customizers.getIfAvailable());
}
}

View File

@@ -0,0 +1,140 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.launch.utils.INetUtil;
import org.springblade.core.redis.stream.DefaultRStreamTemplate;
import org.springblade.core.redis.stream.RStreamListenerDetector;
import org.springblade.core.redis.stream.RStreamTemplate;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.data.redis.stream.StreamMessageListenerContainer.StreamMessageListenerContainerOptions;
import org.springframework.util.ErrorHandler;
import java.time.Duration;
/**
* redis Stream 配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnProperty(
prefix = BladeRedisProperties.Stream.PREFIX,
name = "enable",
havingValue = "true"
)
public class RedisStreamConfiguration {
/**
* Spring 应用名 prop key
*/
private static final String SPRING_APP_NAME_KEY = "spring.application.name";
/**
* The "active profiles" property name.
*/
private static final String ACTIVE_PROFILES_PROPERTY = "spring.profiles.active";
@Bean
@ConditionalOnMissingBean
public StreamMessageListenerContainerOptions<String, MapRecord<String, String, byte[]>> streamMessageListenerContainerOptions(BladeRedisProperties properties,
ObjectProvider<ErrorHandler> errorHandlerObjectProvider) {
StreamMessageListenerContainer.StreamMessageListenerContainerOptionsBuilder<String, MapRecord<String, String, byte[]>> builder = StreamMessageListenerContainerOptions
.builder()
.keySerializer(RedisSerializer.string())
.hashKeySerializer(RedisSerializer.string())
.hashValueSerializer(RedisSerializer.byteArray());
BladeRedisProperties.Stream streamProperties = properties.getStream();
// 批量大小
Integer pollBatchSize = streamProperties.getPollBatchSize();
if (pollBatchSize != null && pollBatchSize > 0) {
builder.batchSize(pollBatchSize);
}
// poll 超时时间
Duration pollTimeout = streamProperties.getPollTimeout();
if (pollTimeout != null && !pollTimeout.isNegative()) {
builder.pollTimeout(pollTimeout);
}
// errorHandler
errorHandlerObjectProvider.ifAvailable((builder::errorHandler));
// TODO L.cm executor
return builder.build();
}
@Bean
@ConditionalOnMissingBean
public StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer(RedisConnectionFactory redisConnectionFactory,
StreamMessageListenerContainerOptions<String, MapRecord<String, String, byte[]>> streamMessageListenerContainerOptions) {
// 根据配置对象创建监听容器
return StreamMessageListenerContainer.create(redisConnectionFactory, streamMessageListenerContainerOptions);
}
@Bean
@ConditionalOnMissingBean
public RStreamListenerDetector streamListenerDetector(StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer,
RedisTemplate<String, Object> redisTemplate,
ObjectProvider<ServerProperties> serverPropertiesObjectProvider,
BladeRedisProperties properties,
Environment environment) {
BladeRedisProperties.Stream streamProperties = properties.getStream();
// 消费组名称
String consumerGroup = streamProperties.getConsumerGroup();
if (StringUtil.isBlank(consumerGroup)) {
String appName = environment.getRequiredProperty(SPRING_APP_NAME_KEY);
String profile = environment.getProperty(ACTIVE_PROFILES_PROPERTY);
consumerGroup = StringUtil.isBlank(profile) ? appName : appName + CharPool.COLON + profile;
}
// 消费者名称
String consumerName = streamProperties.getConsumerName();
if (StringUtil.isBlank(consumerName)) {
final StringBuilder consumerNameBuilder = new StringBuilder(INetUtil.getHostIp());
serverPropertiesObjectProvider.ifAvailable(serverProperties -> {
consumerNameBuilder.append(CharPool.COLON).append(serverProperties.getPort());
});
consumerName = consumerNameBuilder.toString();
}
return new RStreamListenerDetector(streamMessageListenerContainer, redisTemplate, consumerGroup, consumerName);
}
@Bean
public RStreamTemplate streamTemplate(RedisTemplate<String, Object> redisTemplate) {
return new DefaultRStreamTemplate(redisTemplate);
}
}

View File

@@ -0,0 +1,92 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.config;
import org.springblade.core.jwt.config.JwtRedisConfiguration;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.redis.serializer.RedisKeySerializer;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* RedisTemplate 配置
*
* @author L.cm
*/
@EnableCaching
@AutoConfiguration(before = {JwtRedisConfiguration.class, RedisAutoConfiguration.class})
@EnableConfigurationProperties(BladeRedisProperties.class)
public class RedisTemplateConfiguration implements BladeRedisSerializerConfigAble {
/**
* value 值 序列化
*
* @return RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(RedisSerializer.class)
@Override
public RedisSerializer<Object> redisSerializer(BladeRedisProperties properties) {
return defaultRedisSerializer(properties);
}
@Bean(name = "redisTemplate")
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory, RedisSerializer<Object> redisSerializer) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// key 序列化
RedisKeySerializer keySerializer = new RedisKeySerializer();
redisTemplate.setKeySerializer(keySerializer);
redisTemplate.setHashKeySerializer(keySerializer);
// value 序列化
redisTemplate.setValueSerializer(redisSerializer);
redisTemplate.setHashValueSerializer(redisSerializer);
redisTemplate.setConnectionFactory(redisConnectionFactory);
return redisTemplate;
}
@Bean
@ConditionalOnMissingBean(ValueOperations.class)
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public BladeRedis bladeRedis(RedisTemplate<String, Object> redisTemplate, StringRedisTemplate stringRedisTemplate) {
return new BladeRedis(redisTemplate, stringRedisTemplate);
}
}

View File

@@ -0,0 +1,161 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.*;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
/**
* 分布式锁自动化配置
*
* @author L.cm
*/
@AutoConfiguration
@ConditionalOnClass(RedissonClient.class)
@EnableConfigurationProperties(BladeLockProperties.class)
@ConditionalOnProperty(value = "blade.lock.enabled", havingValue = "true")
public class BladeLockAutoConfiguration {
private static Config singleConfig(BladeLockProperties properties) {
Config config = new Config();
SingleServerConfig serversConfig = config.useSingleServer();
serversConfig.setAddress(properties.getAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setConnectionPoolSize(properties.getPoolSize());
serversConfig.setConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config masterSlaveConfig(BladeLockProperties properties) {
Config config = new Config();
MasterSlaveServersConfig serversConfig = config.useMasterSlaveServers();
serversConfig.setMasterAddress(properties.getMasterAddress());
serversConfig.addSlaveAddress(properties.getSlaveAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config sentinelConfig(BladeLockProperties properties) {
Config config = new Config();
SentinelServersConfig serversConfig = config.useSentinelServers();
serversConfig.setMasterName(properties.getMasterName());
serversConfig.addSentinelAddress(properties.getSentinelAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setDatabase(properties.getDatabase());
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
private static Config clusterConfig(BladeLockProperties properties) {
Config config = new Config();
ClusterServersConfig serversConfig = config.useClusterServers();
serversConfig.addNodeAddress(properties.getNodeAddress());
String password = properties.getPassword();
if (StringUtil.isNotBlank(password)) {
serversConfig.setPassword(password);
}
serversConfig.setMasterConnectionPoolSize(properties.getPoolSize());
serversConfig.setMasterConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setSlaveConnectionPoolSize(properties.getPoolSize());
serversConfig.setSlaveConnectionMinimumIdleSize(properties.getIdleSize());
serversConfig.setIdleConnectionTimeout(properties.getConnectionTimeout());
serversConfig.setConnectTimeout(properties.getConnectionTimeout());
serversConfig.setTimeout(properties.getTimeout());
return config;
}
@Bean
@ConditionalOnMissingBean
public RedisLockClient redisLockClient(BladeLockProperties properties) {
return new RedisLockClientImpl(redissonClient(properties));
}
@Bean
@ConditionalOnMissingBean
public RedisLockAspect redisLockAspect(RedisLockClient redisLockClient) {
return new RedisLockAspect(redisLockClient);
}
private static RedissonClient redissonClient(BladeLockProperties properties) {
BladeLockProperties.Mode mode = properties.getMode();
Config config;
switch (mode) {
case sentinel:
config = sentinelConfig(properties);
break;
case cluster:
config = clusterConfig(properties);
break;
case master:
config = masterSlaveConfig(properties);
break;
case single:
config = singleConfig(properties);
break;
default:
config = new Config();
break;
}
return Redisson.create(config);
}
}

View File

@@ -0,0 +1,114 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
/**
* 分布式锁配置
*
* @author L.cm
*/
@Getter
@Setter
@ConfigurationProperties(BladeLockProperties.PREFIX)
public class BladeLockProperties {
public static final String PREFIX = "blade.lock";
/**
* 是否开启默认为false便于生成配置提示。
*/
private Boolean enabled = Boolean.FALSE;
/**
* 单机配置redis 服务地址
*/
private String address = "redis://127.0.0.1:6379";
/**
* 密码配置
*/
private String password;
/**
* db
*/
private Integer database = 0;
/**
* 连接池大小
*/
private Integer poolSize = 20;
/**
* 最小空闲连接数
*/
private Integer idleSize = 5;
/**
* 连接空闲超时,单位:毫秒
*/
private Integer idleTimeout = 60000;
/**
* 连接超时,单位:毫秒
*/
private Integer connectionTimeout = 3000;
/**
* 命令等待超时,单位:毫秒
*/
private Integer timeout = 10000;
/**
* 集群模式单机single主从master哨兵模式sentinel集群模式cluster
*/
private Mode mode = Mode.single;
/**
* 主从模式,主地址
*/
private String masterAddress;
/**
* 主从模式,从地址
*/
private String[] slaveAddress;
/**
* 哨兵模式:主名称
*/
private String masterName;
/**
* 哨兵模式地址
*/
private String[] sentinelAddress;
/**
* 集群模式节点地址
*/
private String[] nodeAddress;
public enum Mode {
/**
* 集群模式单机single主从master哨兵模式sentinel集群模式cluster
*/
single,
master,
sentinel,
cluster
}
}

View File

@@ -0,0 +1,43 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
/**
* 锁类型
*
* @author lcm
*/
public enum LockType {
/**
* 重入锁
*/
REENTRANT,
/**
* 公平锁
*/
FAIR
}

View File

@@ -0,0 +1,91 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式锁注解redisson支持的锁的种类有很多适合注解形式的只有重入锁、公平锁
*
* <p>
* 1. 可重入锁Reentrant Lock
* 2. 公平锁Fair Lock
* 3. 联锁MultiLock
* 4. 红锁RedLock
* 5. 读写锁ReadWriteLock
* </p>
*
* @author L.cm
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RedisLock {
/**
* 分布式锁的 key必须请保持唯一性
*
* @return key
*/
String value();
/**
* 分布式锁参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 等待锁超时时间默认30
*
* @return int
*/
long waitTime() default 30;
/**
* 自动解锁时间自动解锁时间一定得大于方法执行时间否则会导致锁提前释放默认100
*
* @return int
*/
long leaseTime() default 100;
/**
* 时间单位,默认为秒
*
* @return 时间单位
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 默认公平锁
*
* @return LockType
*/
LockType type() default LockType.FAIR;
}

View File

@@ -0,0 +1,112 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.RequiredArgsConstructor;
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.springblade.core.tool.spel.BladeExpressionEvaluator;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* redis 分布式锁
*
* @author L.cm
*/
@Aspect
@RequiredArgsConstructor
public class RedisLockAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private static final BladeExpressionEvaluator EVALUATOR = new BladeExpressionEvaluator();
/**
* redis 限流服务
*/
private final RedisLockClient redisLockClient;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RedisLock
*/
@Around("@annotation(redisLock)")
public Object aroundRedisLock(ProceedingJoinPoint point, RedisLock redisLock) {
String lockName = redisLock.value();
Assert.hasText(lockName, "@RedisLock value must have length; it must not be null or empty");
// el 表达式
String lockParam = redisLock.param();
// 表达式不为空
String lockKey;
if (StringUtil.isNotBlank(lockParam)) {
String evalAsText = evalLockParam(point, lockParam);
lockKey = lockName + CharPool.COLON + evalAsText;
} else {
lockKey = lockName;
}
LockType lockType = redisLock.type();
long waitTime = redisLock.waitTime();
long leaseTime = redisLock.leaseTime();
TimeUnit timeUnit = redisLock.timeUnit();
return redisLockClient.lock(lockKey, lockType, waitTime, leaseTime, timeUnit, point::proceed);
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param lockParam lockParam
* @return 结果
*/
private String evalLockParam(ProceedingJoinPoint point, String lockParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = EVALUATOR.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return EVALUATOR.evalAsText(lockParam, elementKey, context);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View File

@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import org.springblade.core.tool.function.CheckedSupplier;
import java.util.concurrent.TimeUnit;
/**
* 锁客户端
*
* @author L.cm
*/
public interface RedisLockClient {
/**
* 尝试获取锁
*
* @param lockName 锁名
* @param lockType 锁类型
* @param waitTime 等待时间
* @param leaseTime 自动解锁时间,自动解锁时间一定得大于方法执行时间
* @param timeUnit 时间参数
* @return 是否成功
* @throws InterruptedException InterruptedException
*/
boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException;
/**
* 解锁
*
* @param lockName 锁名
* @param lockType 锁类型
*/
void unLock(String lockName, LockType lockType);
/**
* 自定获取锁后执行方法
*
* @param lockName 锁名
* @param lockType 锁类型
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间自动解锁时间一定得大于方法执行时间否则会导致锁提前释放默认100
* @param timeUnit 时间单位
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
<T> T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier<T> supplier);
/**
* 公平锁
*
* @param lockName 锁名
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间自动解锁时间一定得大于方法执行时间否则会导致锁提前释放默认100
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
default <T> T lockFair(String lockName, long waitTime, long leaseTime, CheckedSupplier<T> supplier) {
return lock(lockName, LockType.FAIR, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
}
/**
* 可重入锁
*
* @param lockName 锁名
* @param waitTime 等待锁超时时间
* @param leaseTime 自动解锁时间自动解锁时间一定得大于方法执行时间否则会导致锁提前释放默认100
* @param supplier 获取锁后的回调
* @return 返回的数据
*/
default <T> T lockReentrant(String lockName, long waitTime, long leaseTime, CheckedSupplier<T> supplier) {
return lock(lockName, LockType.REENTRANT, waitTime, leaseTime, TimeUnit.SECONDS, supplier);
}
}

View File

@@ -0,0 +1,88 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.lock;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springblade.core.tool.function.CheckedSupplier;
import org.springblade.core.tool.utils.Exceptions;
import java.util.concurrent.TimeUnit;
/**
* 锁客户端
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RedisLockClientImpl implements RedisLockClient {
private final RedissonClient redissonClient;
@Override
public boolean tryLock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit) throws InterruptedException {
RLock lock = getLock(lockName, lockType);
return lock.tryLock(waitTime, leaseTime, timeUnit);
}
@Override
public void unLock(String lockName, LockType lockType) {
RLock lock = getLock(lockName, lockType);
// 仅仅在已经锁定和当前线程持有锁时解锁
if (lock.isLocked() && lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
private RLock getLock(String lockName, LockType lockType) {
RLock lock;
if (LockType.REENTRANT == lockType) {
lock = redissonClient.getLock(lockName);
} else {
lock = redissonClient.getFairLock(lockName);
}
return lock;
}
@Override
public <T> T lock(String lockName, LockType lockType, long waitTime, long leaseTime, TimeUnit timeUnit, CheckedSupplier<T> supplier) {
try {
boolean result = this.tryLock(lockName, lockType, waitTime, leaseTime, timeUnit);
if (result) {
return supplier.get();
}
} catch (Throwable e) {
throw Exceptions.unchecked(e);
} finally {
this.unLock(lockName, lockType);
}
return null;
}
}

View File

@@ -0,0 +1,76 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
/**
* 分布式 限流注解,默认速率为 600/ms
*
* @author L.cm
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface RateLimiter {
/**
* 限流的 key 支持,必须:请保持唯一性
*
* @return key
*/
String value();
/**
* 限流的参数,可选,支持 spring el # 读取方法参数和 @ 读取 spring bean
*
* @return param
*/
String param() default "";
/**
* 支持的最大请求,默认: 100
*
* @return 请求数
*/
long max() default 100L;
/**
* 持续时间,默认: 3600
*
* @return 持续时间
*/
long ttl() default 1L;
/**
* 时间单位,默认为分
*
* @return TimeUnit
*/
TimeUnit timeUnit() default TimeUnit.MINUTES;
}

View File

@@ -0,0 +1,100 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import org.springblade.core.tool.function.CheckedSupplier;
import org.springblade.core.tool.utils.Exceptions;
import java.util.concurrent.TimeUnit;
/**
* RateLimiter 限流 Client
*
* @author L.cm
*/
public interface RateLimiterClient {
/**
* 服务是否被限流
*
* @param key 自定义的key请保证唯一
* @param max 支持的最大请求
* @param ttl 时间,单位默认为秒seconds
* @return 是否允许
*/
default boolean isAllowed(String key, long max, long ttl) {
return this.isAllowed(key, max, ttl, TimeUnit.SECONDS);
}
/**
* 服务是否被限流
*
* @param key 自定义的key请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @return 是否允许
*/
boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit);
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param supplier Supplier 函数式
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, CheckedSupplier<T> supplier) {
return allow(key, max, ttl, TimeUnit.SECONDS, supplier);
}
/**
* 服务限流,被限制时抛出 RateLimiterException 异常,需要自行处理异常
*
* @param key 自定义的key请保证唯一
* @param max 支持的最大请求
* @param ttl 时间
* @param timeUnit 时间单位
* @param supplier Supplier 函数式
* @param <T>
* @return 函数执行结果
*/
default <T> T allow(String key, long max, long ttl, TimeUnit timeUnit, CheckedSupplier<T> supplier) {
boolean isAllowed = this.isAllowed(key, max, ttl, timeUnit);
if (isAllowed) {
try {
return supplier.get();
} catch (Throwable e) {
throw Exceptions.unchecked(e);
}
}
throw new RateLimiterException(key, max, ttl, timeUnit);
}
}

View File

@@ -0,0 +1,52 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.Getter;
import java.util.concurrent.TimeUnit;
/**
* 限流异常
*
* @author L.cm
*/
@Getter
public class RateLimiterException extends RuntimeException {
private final String key;
private final long max;
private final long ttl;
private final TimeUnit timeUnit;
public RateLimiterException(String key, long max, long ttl, TimeUnit timeUnit) {
super(String.format("您的访问次数已超限:%s速率%d/%ds", key, max, timeUnit.toSeconds(ttl)));
this.key = key;
this.max = max;
this.ttl = ttl;
this.timeUnit = timeUnit;
}
}

View File

@@ -0,0 +1,111 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
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.springblade.core.tool.spel.BladeExpressionEvaluator;
import org.springblade.core.tool.utils.CharPool;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;
/**
* redis 限流
*
* @author L.cm
*/
@Aspect
@RequiredArgsConstructor
public class RedisRateLimiterAspect implements ApplicationContextAware {
/**
* 表达式处理
*/
private final BladeExpressionEvaluator evaluator = new BladeExpressionEvaluator();
/**
* redis 限流服务
*/
private final RedisRateLimiterClient rateLimiterClient;
private ApplicationContext applicationContext;
/**
* AOP 环切 注解 @RateLimiter
*/
@Around("@annotation(limiter)")
public Object aroundRateLimiter(ProceedingJoinPoint point, RateLimiter limiter) throws Throwable {
String limitKey = limiter.value();
Assert.hasText(limitKey, "@RateLimiter value must have length; it must not be null or empty");
// el 表达式
String limitParam = limiter.param();
// 表达式不为空
String rateKey;
if (StringUtil.isNotBlank(limitParam)) {
String evalAsText = evalLimitParam(point, limitParam);
rateKey = limitKey + CharPool.COLON + evalAsText;
} else {
rateKey = limitKey;
}
long max = limiter.max();
long ttl = limiter.ttl();
TimeUnit timeUnit = limiter.timeUnit();
return rateLimiterClient.allow(rateKey, max, ttl, timeUnit, point::proceed);
}
/**
* 计算参数表达式
*
* @param point ProceedingJoinPoint
* @param limitParam limitParam
* @return 结果
*/
private String evalLimitParam(ProceedingJoinPoint point, String limitParam) {
MethodSignature ms = (MethodSignature) point.getSignature();
Method method = ms.getMethod();
Object[] args = point.getArgs();
Object target = point.getTarget();
Class<?> targetClass = target.getClass();
EvaluationContext context = evaluator.createContext(method, args, target, targetClass, applicationContext);
AnnotatedElementKey elementKey = new AnnotatedElementKey(method, targetClass);
return evaluator.evalAsText(limitParam, elementKey, context);
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}

View File

@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.ratelimiter;
import lombok.RequiredArgsConstructor;
import org.springblade.core.tool.utils.CharPool;
import org.springframework.core.env.Environment;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
/**
* redis 限流服务
*
* @author dream.lu
*/
@RequiredArgsConstructor
public class RedisRateLimiterClient implements RateLimiterClient {
/**
* redis 限流 key 前缀
*/
private static final String REDIS_KEY_PREFIX = "limiter:";
/**
* 失败的默认返回值
*/
private static final long FAIL_CODE = 0;
/**
* redisTemplate
*/
private final StringRedisTemplate redisTemplate;
/**
* redisScript
*/
private final RedisScript<Long> script;
/**
* env
*/
private final Environment environment;
@Override
public boolean isAllowed(String key, long max, long ttl, TimeUnit timeUnit) {
// redis key
String redisKeyBuilder = REDIS_KEY_PREFIX +
getApplicationName(environment) + CharPool.COLON + key;
List<String> keys = Collections.singletonList(redisKeyBuilder);
// 转为毫秒pexpire
long ttlMillis = timeUnit.toMillis(ttl);
// 执行命令
Long result = this.redisTemplate.execute(this.script, keys, max + "", ttlMillis + "");
return result != null && result != FAIL_CODE;
}
private static String getApplicationName(Environment environment) {
return environment.getProperty("spring.application.name", "");
}
}

View File

@@ -0,0 +1,61 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
/**
* redis序列化辅助类.单纯的泛型无法定义通用schema原因是无法通过泛型T得到Class
*
* @author L.cm
*/
public class BytesWrapper<T> implements Cloneable {
private T value;
public BytesWrapper() {
}
public BytesWrapper(T value) {
this.value = value;
}
public void setValue(T value) {
this.value = value;
}
public T getValue() {
return value;
}
@Override
@SuppressWarnings("unchecked")
public BytesWrapper<T> clone() {
try {
return (BytesWrapper) super.clone();
} catch (CloneNotSupportedException e) {
return new BytesWrapper<>();
}
}
}

View File

@@ -0,0 +1,71 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
import io.protostuff.LinkedBuffer;
import io.protostuff.ProtobufIOUtil;
import io.protostuff.Schema;
import io.protostuff.runtime.RuntimeSchema;
import org.springblade.core.tool.utils.ObjectUtil;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
/**
* ProtoStuff 序列化
*
* @author L.cm
*/
public class ProtoStuffSerializer implements RedisSerializer<Object> {
private final Schema<BytesWrapper> schema;
public ProtoStuffSerializer() {
this.schema = RuntimeSchema.getSchema(BytesWrapper.class);
}
@Override
public byte[] serialize(Object object) throws SerializationException {
if (object == null) {
return null;
}
LinkedBuffer buffer = LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE);
try {
return ProtobufIOUtil.toByteArray(new BytesWrapper<>(object), schema, buffer);
} finally {
buffer.clear();
}
}
@Override
public Object deserialize(byte[] bytes) throws SerializationException {
if (ObjectUtil.isEmpty(bytes)) {
return null;
}
BytesWrapper<Object> wrapper = new BytesWrapper<>();
ProtobufIOUtil.mergeFrom(bytes, wrapper, schema);
return wrapper.getValue();
}
}

View File

@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.serializer;
import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Objects;
/**
* 将redis key序列化为字符串
*
* <p>
* spring cache中的简单基本类型直接使用 StringRedisSerializer 会有问题
* </p>
*
* @author L.cm
*/
public class RedisKeySerializer implements RedisSerializer<Object> {
private final Charset charset;
private final ConversionService converter;
public RedisKeySerializer() {
this(StandardCharsets.UTF_8);
}
public RedisKeySerializer(Charset charset) {
Objects.requireNonNull(charset, "Charset must not be null");
this.charset = charset;
this.converter = DefaultConversionService.getSharedInstance();
}
@Override
public Object deserialize(byte[] bytes) {
// redis keys 会用到反序列化
if (bytes == null) {
return null;
}
return new String(bytes, charset);
}
@Override
public byte[] serialize(Object object) {
Objects.requireNonNull(object, "redis key is null");
String key;
if (object instanceof SimpleKey) {
key = "";
} else if (object instanceof String) {
key = (String) object;
} else {
key = converter.convert(object, String.class);
}
return key.getBytes(this.charset);
}
}

View File

@@ -0,0 +1,122 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.core.convert.RedisCustomConversions;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
/**
* 默认的 RStreamTemplate
*
* @author L.cm
*/
public class DefaultRStreamTemplate implements RStreamTemplate {
private static final RedisCustomConversions CUSTOM_CONVERSIONS = new RedisCustomConversions();
private final RedisTemplate<String, Object> redisTemplate;
private final StreamOperations<String, String, Object> streamOperations;
public DefaultRStreamTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
this.streamOperations = redisTemplate.opsForStream();
}
@Override
public RecordId send(Record<String, ?> record) {
// 1. MapRecord
if (record instanceof MapRecord) {
return streamOperations.add(record);
}
String stream = Objects.requireNonNull(record.getStream(), "RStreamTemplate send stream name is null.");
Object recordValue = Objects.requireNonNull(record.getValue(), "RStreamTemplate send stream: " + stream + " value is null.");
Class<?> valueClass = recordValue.getClass();
// 2. 普通类型的 ObjectRecord
if (CUSTOM_CONVERSIONS.isSimpleType(valueClass)) {
return streamOperations.add(record);
}
// 3. 自定义类型处理
Map<String, Object> payload = new HashMap<>();
payload.put(RStreamTemplate.OBJECT_PAYLOAD_KEY, recordValue);
MapRecord<String, String, Object> mapRecord = MapRecord.create(stream, payload);
return streamOperations.add(mapRecord);
}
@Override
public RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options) {
RedisSerializer<String> stringSerializer = StringRedisSerializer.UTF_8;
byte[] nameBytes = Objects.requireNonNull(stringSerializer.serialize(name), "redis stream name is null.");
byte[] keyBytes = Objects.requireNonNull(stringSerializer.serialize(key), "redis stream key is null.");
Map<byte[], byte[]> mapDate = Collections.singletonMap(keyBytes, data);
return redisTemplate.execute((RedisCallback<RecordId>) redis -> {
RedisStreamCommands streamCommands = redis.streamCommands();
return streamCommands.xAdd(MapRecord.create(nameBytes, mapDate), options);
});
}
@Override
public Long delete(String name, String... recordIds) {
return streamOperations.delete(name, recordIds);
}
@Override
public Long delete(String name, RecordId... recordIds) {
return streamOperations.delete(name, recordIds);
}
@Override
public Long trim(String name, long count, boolean approximateTrimming) {
return streamOperations.trim(name, count, approximateTrimming);
}
@Override
public Long acknowledge(String name, String group, String... recordIds) {
return streamOperations.acknowledge(name, group, recordIds);
}
@Override
public Long acknowledge(String name, String group, RecordId... recordIds) {
return streamOperations.acknowledge(name, group, recordIds);
}
@Override
public Long acknowledge(String group, Record<String, ?> record) {
return streamOperations.acknowledge(group, record);
}
}

View File

@@ -0,0 +1,46 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
/**
* 消息类型
*
* @author L.cm
*/
public enum MessageModel {
/**
* 广播
*/
BROADCASTING,
/**
* 集群消息
*/
CLUSTERING;
}

View File

@@ -0,0 +1,90 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import java.lang.annotation.*;
/**
* 基于 redis 的 stream 监听
*
* @author L.cm
*/
@Documented
@Inherited
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RStreamListener {
/**
* Queue name
*
* @return String
*/
String name();
/**
* consumer group默认为服务名 + 环境
*
* @return String
*/
String group() default "";
/**
* 消息方式,集群模式和广播模式,如果想让所有订阅者收到所有消息,广播是一个不错的选择。
*
* @return MessageModel
*/
MessageModel messageModel() default MessageModel.CLUSTERING;
/**
* offsetModel默认LAST_CONSUMED
*
* <p>
* 0-0 : 从开始的地方读。
* $ :表示从尾部开始消费,只接受新消息,当前 Stream 消息会全部忽略。
* > : 读取所有新到达的元素这些元素的id大于消费组使用的最后一个元素。
* </p>
*
* @return ReadOffsetModel
*/
ReadOffsetModel offsetModel() default ReadOffsetModel.LAST_CONSUMED;
/**
* 自动 ack
*
* @return boolean
*/
boolean autoAcknowledge() default false;
/**
* 读取原始的 bytes 数据
*
* @return boolean
*/
boolean readRawBytes() default false;
}

View File

@@ -0,0 +1,169 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.ReflectUtil;
import org.springblade.core.tool.utils.StringUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.RedisSystemException;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StreamOperations;
import org.springframework.data.redis.stream.StreamMessageListenerContainer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
import java.util.Collections;
import java.util.Map;
/**
* Redisson 监听器
*
* @author L.cm
*/
@Slf4j
public class RStreamListenerDetector implements BeanPostProcessor, InitializingBean {
private final StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer;
private final RedisTemplate<String, Object> redisTemplate;
private final String consumerGroup;
private final String consumerName;
public RStreamListenerDetector(StreamMessageListenerContainer<String, MapRecord<String, String, byte[]>> streamMessageListenerContainer,
RedisTemplate<String, Object> redisTemplate, String consumerGroup, String consumerName) {
this.streamMessageListenerContainer = streamMessageListenerContainer;
this.redisTemplate = redisTemplate;
this.consumerGroup = consumerGroup;
this.consumerName = consumerName;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> userClass = ClassUtils.getUserClass(bean);
ReflectionUtils.doWithMethods(userClass, method -> {
RStreamListener listener = AnnotationUtils.findAnnotation(method, RStreamListener.class);
if (listener != null) {
String streamKey = listener.name();
Assert.hasText(streamKey, "@RStreamListener name must not be empty.");
log.info("Found @RStreamListener on bean:{} method:{}", beanName, method);
// 校验 methodmethod 入参数大于等于1
int paramCount = method.getParameterCount();
if (paramCount > 1) {
throw new IllegalArgumentException("@RStreamListener on method " + method + " parameter count must less or equal to 1.");
}
// streamOffset
ReadOffset readOffset = listener.offsetModel().getReadOffset();
StreamOffset<String> streamOffset = StreamOffset.create(streamKey, readOffset);
// 消费模式
MessageModel messageModel = listener.messageModel();
if (MessageModel.BROADCASTING == messageModel) {
broadCast(streamOffset, bean, method, listener.readRawBytes());
} else {
String groupId = StringUtil.isNotBlank(listener.group()) ? listener.group() : consumerGroup;
Consumer consumer = Consumer.from(groupId, consumerName);
// 如果需要,创建 group
createGroupIfNeed(redisTemplate, streamKey, readOffset, groupId);
cluster(consumer, streamOffset, listener, bean, method);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
return bean;
}
private void broadCast(StreamOffset<String> streamOffset, Object bean, Method method, boolean isReadRawBytes) {
streamMessageListenerContainer.receive(streamOffset, (message) -> {
// MapBackedRecord
invokeMethod(bean, method, message, isReadRawBytes);
});
}
private void cluster(Consumer consumer, StreamOffset<String> streamOffset, RStreamListener listener, Object bean, Method method) {
boolean autoAcknowledge = listener.autoAcknowledge();
StreamMessageListenerContainer.ConsumerStreamReadRequest<String> readRequest = StreamMessageListenerContainer.StreamReadRequest.builder(streamOffset).consumer(consumer).autoAcknowledge(autoAcknowledge).build();
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
streamMessageListenerContainer.register(readRequest, (message) -> {
// MapBackedRecord
invokeMethod(bean, method, message, listener.readRawBytes());
// ack
if (!autoAcknowledge) {
opsForStream.acknowledge(consumer.getGroup(), message);
}
});
}
private static void createGroupIfNeed(RedisTemplate<String, Object> redisTemplate, String streamKey, ReadOffset readOffset, String group) {
StreamOperations<String, Object, Object> opsForStream = redisTemplate.opsForStream();
try {
StreamInfo.XInfoGroups groups = opsForStream.groups(streamKey);
if (groups.stream().noneMatch((x) -> group.equals(x.groupName()))) {
opsForStream.createGroup(streamKey, readOffset, group);
}
} catch (RedisSystemException e) {
// RedisCommandExecutionException: ERR no such key
opsForStream.createGroup(streamKey, group);
}
}
private void invokeMethod(Object bean, Method method, MapRecord<String, String, byte[]> mapRecord, boolean isReadRawBytes) {
// 支持没有参数的方法
if (method.getParameterCount() == 0) {
ReflectUtil.invokeMethod(method, bean);
return;
}
if (isReadRawBytes) {
ReflectUtil.invokeMethod(method, bean, mapRecord);
} else {
ReflectUtil.invokeMethod(method, bean, getRecordValue(mapRecord));
}
}
private Object getRecordValue(MapRecord<String, String, byte[]> mapRecord) {
Map<String, byte[]> messageValue = mapRecord.getValue();
if (messageValue.containsKey(RStreamTemplate.OBJECT_PAYLOAD_KEY)) {
byte[] payloads = messageValue.get(RStreamTemplate.OBJECT_PAYLOAD_KEY);
Object deserialize = redisTemplate.getValueSerializer().deserialize(payloads);
return ObjectRecord.create(mapRecord.getStream(), deserialize).withId(mapRecord.getId());
} else {
return mapRecord.mapEntries(entry -> {
String key = entry.getKey();
Object value = redisTemplate.getValueSerializer().deserialize(entry.getValue());
return Collections.singletonMap(key, value).entrySet().iterator().next();
});
}
}
@Override
public void afterPropertiesSet() throws Exception {
streamMessageListenerContainer.start();
}
}

View File

@@ -0,0 +1,270 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import org.springframework.data.redis.connection.RedisStreamCommands;
import org.springframework.data.redis.connection.stream.MapRecord;
import org.springframework.data.redis.connection.stream.ObjectRecord;
import org.springframework.data.redis.connection.stream.Record;
import org.springframework.data.redis.connection.stream.RecordId;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.lang.Nullable;
import java.util.Collections;
import java.util.Map;
import java.util.function.Function;
/**
* 基于 redis Stream 的消息发布器
*
* @author L.cm
*/
public interface RStreamTemplate {
/**
* 自定义 pojo 类型 key
*/
String OBJECT_PAYLOAD_KEY = "@payload";
/**
* 方便多 redis 数据源使用
*
* @param redisTemplate RedisTemplate
* @return MicaRedisCache
*/
static RStreamTemplate use(RedisTemplate<String, Object> redisTemplate) {
return new DefaultRStreamTemplate(redisTemplate);
}
/**
* 发布消息
*
* @param name 队列名
* @param value 消息
* @return 消息id
*/
default RecordId send(String name, Object value) {
return send(ObjectRecord.create(name, value));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param value 消息
* @return 消息id
*/
default RecordId send(String name, String key, Object value) {
return send(name, Collections.singletonMap(key, value));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @return 消息id
*/
default RecordId send(String name, String key, byte[] data) {
return send(name, key, data, RedisStreamCommands.XAddOptions.none());
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param maxLen 限制 stream 最大长度
* @return 消息id
*/
default RecordId send(String name, String key, byte[] data, long maxLen) {
return send(name, key, data, RedisStreamCommands.XAddOptions.maxlen(maxLen));
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param options XAddOptions
* @return 消息id
*/
RecordId send(String name, String key, byte[] data, RedisStreamCommands.XAddOptions options);
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper mapper
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper, long maxLen) {
return send(name, key, mapper.apply(data), maxLen);
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper mapper
* @param options XAddOptions
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper, RedisStreamCommands.XAddOptions options) {
return send(name, key, mapper.apply(data), options);
}
/**
* 发布消息
*
* @param name 队列名
* @param key 消息key
* @param data 消息
* @param mapper 消息转换
* @param <T> 泛型
* @return 消息id
*/
default <T> RecordId send(String name, String key, T data, Function<T, byte[]> mapper) {
return send(name, key, mapper.apply(data));
}
/**
* 批量发布
*
* @param name 队列名
* @param messages 消息
* @return 消息id
*/
default RecordId send(String name, Map<String, Object> messages) {
return send(MapRecord.create(name, messages));
}
/**
* 发送消息
*
* @param record Record
* @return 消息id
*/
RecordId send(Record<String, ?> record);
/**
* 删除消息
*
* @param name stream name
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long delete(String name, String... recordIds);
/**
* 删除消息
*
* @param name stream name
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long delete(String name, RecordId... recordIds);
/**
* 删除消息
*
* @param record Record
* @return Long
*/
@Nullable
default Long delete(Record<String, ?> record) {
return delete(record.getStream(), record.getId());
}
/**
* 对流进行修剪,限制长度
*
* @param name name
* @param count count
* @return Long
*/
@Nullable
default Long trim(String name, long count) {
return trim(name, count, false);
}
/**
* 对流进行修剪,限制长度
*
* @param name name
* @param count count
* @param approximateTrimming approximateTrimming
* @return Long
*/
@Nullable
Long trim(String name, long count, boolean approximateTrimming);
/**
* 手动 ack
*
* @param name name
* @param group group
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long acknowledge(String name, String group, String... recordIds);
/**
* 手动 ack
*
* @param name name
* @param group group
* @param recordIds recordIds
* @return Long
*/
@Nullable
Long acknowledge(String name, String group, RecordId... recordIds);
/**
* 手动 ack
*
* @param group group
* @param record record
* @return Long
*/
@Nullable
Long acknowledge(String group, Record<String, ?> record);
}

View File

@@ -0,0 +1,60 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY. The author is
* not liable for any claims arising from secondary or illegal development.
* <p>
* Author: DreamLu (596392912@qq.com)
*/
package org.springblade.core.redis.stream;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.connection.stream.ReadOffset;
/**
* stream read offset model
*
* @author L.cm
*/
@Getter
@RequiredArgsConstructor
public enum ReadOffsetModel {
/**
* 从开始的地方读
*/
START(ReadOffset.from("0-0")),
/**
* 从最近的偏移量读取。
*/
LATEST(ReadOffset.latest()),
/**
* 读取所有新到达的元素这些元素的id大于最后一个消费组的id。
*/
LAST_CONSUMED(ReadOffset.lastConsumed());
/**
* readOffset
*/
private final ReadOffset readOffset;
}

View File

@@ -0,0 +1,26 @@
-- 开启单命令复制模式
redis.replicate_commands()
-- lua 下标从 1 开始
-- 限流大小
local max = tonumber(ARGV[1])
-- 超时时间
local ttl = tonumber(ARGV[2])
local now = redis.call('TIME')[1]
-- 已经过期的时间点
local expired = now - ttl
-- 清除过期的数据,移除指定分数score区间内的所有成员
redis.call('zremrangebyscore', KEYS[1], 0, expired)
-- 获取当前流量大小
local currentLimit = tonumber(redis.call('zcard', KEYS[1]))
local nextLimit = currentLimit + 1
if nextLimit > max then
-- 达到限流大小 返回 0
return 0;
else
-- 没有达到阈值 value + 1
redis.call("zadd", KEYS[1], now, now)
-- 秒为单位设置 key 的生存时间
redis.call("pexpire", KEYS[1], ttl)
return nextLimit
end

View File

@@ -0,0 +1,11 @@
{
"properties": [
{
"name": "blade.redis.rate-limiter.enabled",
"type": "java.lang.Boolean",
"description": "是否开启 redis 分布式限流.",
"defaultValue": "false"
}
]
}