🎉 新增redis模块,提供BladeRedis新版工具,支持redis pub/sub 发布

This commit is contained in:
smallchill 2024-12-10 18:58:40 +08:00
parent ae90973dd4
commit 9d7400f381
25 changed files with 2016 additions and 18 deletions

View File

@ -67,6 +67,10 @@
<groupId>org.springblade</groupId>
<artifactId>blade-starter-tenant</artifactId>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-redis</artifactId>
</dependency>
<!--MyBatis-->
<dependency>
<groupId>org.springblade</groupId>

View File

@ -22,19 +22,13 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
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.core.RedisTemplate;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import java.time.Duration;
/**
* RedisTemplate 配置
*
@ -43,7 +37,7 @@ import java.time.Duration;
@EnableCaching
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfiguration {
public class RedisConfiguration {
/**
* value 序列化
@ -71,15 +65,6 @@ public class RedisTemplateConfiguration {
return redisTemplate;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1));
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
@Bean(name = "redisUtil")
@ConditionalOnBean(RedisTemplate.class)
public RedisUtil redisUtils(RedisTemplate<String, Object> redisTemplate) {

View File

@ -25,6 +25,7 @@ import java.util.concurrent.Callable;
* 缓存工具类
*
* @author Chill
* @deprecated in favor of {org.springblade.core.cache.utils.CacheUtil}.
*/
@Deprecated
public class CacheUtil {

View File

@ -20,6 +20,7 @@ import org.springframework.util.CollectionUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
/**
@ -79,4 +80,26 @@ public class CollectionUtil extends org.springframework.util.CollectionUtils {
return !CollectionUtils.isEmpty(map);
}
/**
* 将key value 数组转为 map
*
* @param keysValues key value 数组
* @param <K> key
* @param <V> value
* @return map 集合
*/
public static <K, V> Map<K, V> toMap(Object... keysValues) {
int kvLength = keysValues.length;
if (kvLength % 2 != 0) {
throw new IllegalArgumentException("wrong number of arguments for met, keysValues length can not be odd");
}
Map<K, V> keyValueMap = new HashMap<>(kvLength);
for (int i = kvLength - 2; i >= 0; i -= 2) {
Object key = keysValues[i];
Object value = keysValues[i + 1];
keyValueMap.put((K) key, (V) value);
}
return keyValueMap;
}
}

View File

@ -13,7 +13,9 @@ import java.util.concurrent.TimeUnit;
* Redis工具类
*
* @author Chill
* @deprecated in favor of {org.springblade.core.redis.cache.BladeRedis}.
*/
@Deprecated
@AllArgsConstructor
public class RedisUtil {

View File

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>blade-tool</artifactId>
<groupId>org.springblade</groupId>
<version>${revision}</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>blade-starter-redis</artifactId>
<name>${project.artifactId}</name>
<packaging>jar</packaging>
<properties>
<module.name>org.springblade.blade.starter.redis</module.name>
</properties>
<dependencies>
<!--Blade-->
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-core-tool</artifactId>
</dependency>
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
<!-- protostuff -->
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
</dependency>
<dependency>
<groupId>io.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,876 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.data.redis.serializer.RedisSerializer;
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.Function;
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 对到 redis并将 key 的生存时间设为自定义单位
* 如果 key 已经存在 SETEX 命令将覆写旧值
*/
public void setEx(String key, Object value, long timeout, TimeUnit unit) {
valueOps.set(key, value, timeout, unit);
}
/**
* 返回 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);
}
/**
* redis publish
*
* @param channel channel
* @param message message
* @param mapper mapper
* @param <T> 泛型标记
* @return Long
*/
@Nullable
public <T> Long publish(String channel, T message, Function<T, byte[]> mapper) {
return redisTemplate.execute((RedisCallback<Long>) redis -> {
byte[] channelBytes = keySerialize(channel);
return redis.publish(channelBytes, mapper.apply(message));
});
}
/**
* redisKey 序列化
*
* @param redisKey redisKey
* @return byte array
*/
public byte[] keySerialize(String redisKey) {
RedisSerializer<String> keySerializer = (RedisSerializer<String>) this.redisTemplate.getKeySerializer();
return Objects.requireNonNull(keySerializer.serialize(redisKey), "Redis key is null.");
}
}

View File

@ -0,0 +1,49 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,70 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,113 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.config;
import org.springblade.core.tool.config.RedisConfiguration;
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 = RedisConfiguration.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,41 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 {
@Bean
@ConditionalOnMissingBean
public RedisSerializer<Object> redisSerializer() {
return new ProtoStuffSerializer();
}
}

View File

@ -0,0 +1,63 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,42 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,61 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.redis.pubsub.RPubSubListenerDetector;
import org.springblade.core.redis.pubsub.RPubSubPublisher;
import org.springblade.core.redis.pubsub.RedisPubSubPublisher;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redisson pub/sub 发布配置
*
* @author L.cm
*/
@AutoConfiguration
public class RedisPubSubConfiguration {
@Bean
@ConditionalOnMissingBean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
@Bean
public RPubSubPublisher topicEventPublisher(BladeRedis bladeRedis,
RedisSerializer<Object> redisSerializer) {
return new RedisPubSubPublisher(bladeRedis, redisSerializer);
}
@Bean
@ConditionalOnBean(RedisSerializer.class)
public RPubSubListenerDetector topicListenerDetector(RedisMessageListenerContainer redisMessageListenerContainer,
RedisSerializer<Object> redisSerializer) {
return new RPubSubListenerDetector(redisMessageListenerContainer, redisSerializer);
}
}

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.config;
import org.springblade.core.redis.cache.BladeRedis;
import org.springblade.core.redis.serializer.ProtoStuffSerializer;
import org.springblade.core.redis.serializer.RedisKeySerializer;
import org.springblade.core.tool.config.RedisConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
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 = {RedisConfiguration.class, RedisAutoConfiguration.class})
public class RedisTemplateConfiguration {
/**
* value 序列化
*
* @return RedisSerializer
*/
@Bean
@ConditionalOnMissingBean(RedisSerializer.class)
public RedisSerializer<Object> redisSerializer() {
return new ProtoStuffSerializer();
}
@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,99 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
import lombok.experimental.UtilityClass;
import org.springblade.core.tool.utils.CharPool;
import org.springframework.data.redis.listener.ChannelTopic;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.Topic;
/**
* channel 工具类
*
* @author L.cm
*/
@UtilityClass
class ChannelUtil {
/**
* 获取 pub sub topic
*
* @param channel channel
* @return Topic
*/
public static Topic getTopic(String channel) {
return isPattern(channel) ? new PatternTopic(channel) : new ChannelTopic(channel);
}
/**
* 判断是否为模糊话题*? [...]
*
* @param channel 话题名
* @return 是否模糊话题
*/
public static boolean isPattern(String channel) {
int length = channel.length();
boolean isRightSqBracket = false;
// 倒序因为表达式一般在尾部
for (int i = length - 1; i > 0; i--) {
char charAt = channel.charAt(i);
switch (charAt) {
case CharPool.ASTERISK:
case CharPool.QUESTION_MARK:
if (isEscapeChars(channel, i)) {
break;
}
return true;
case CharPool.RIGHT_SQ_BRACKET:
if (isEscapeChars(channel, i)) {
break;
}
isRightSqBracket = true;
break;
case CharPool.LEFT_SQ_BRACKET:
if (isEscapeChars(channel, i)) {
break;
}
if (isRightSqBracket) {
return true;
}
break;
default:
break;
}
}
return false;
}
/**
* 判断是否为转义字符
*
* @param name 话题名
* @param index 索引
* @return 是否为转义字符
*/
private static boolean isEscapeChars(String name, int index) {
if (index < 1) {
return false;
}
// 预读一位判断是否为转义符 /
char charAt = name.charAt(index - 1);
return CharPool.BACK_SLASH == charAt;
}
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
/**
* 基于 redis pub sub 事件对象
*
* @author L.cm
*/
@Getter
@ToString
@RequiredArgsConstructor
public class RPubSubEvent<M> {
/**
* 匹配模式时的正则
*/
private final CharSequence pattern;
/**
* channel
*/
private final CharSequence channel;
/**
* pub 的消息对象
*/
private final M msg;
}

View File

@ -0,0 +1,39 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
import java.lang.annotation.*;
/**
* 基于 Redisson 的消息监听器
*
* @author L.cm
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface RPubSubListener {
/**
* topic name支持通配符 *? [...]
*
* @return String
*/
String value();
}

View File

@ -0,0 +1,81 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.tool.utils.ReflectUtil;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.Topic;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import java.lang.reflect.Method;
/**
* Redisson 监听器
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RPubSubListenerDetector implements BeanPostProcessor {
private final RedisMessageListenerContainer redisMessageListenerContainer;
private final RedisSerializer<Object> redisSerializer;
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Class<?> userClass = ClassUtils.getUserClass(bean);
ReflectionUtils.doWithMethods(userClass, method -> {
RPubSubListener listener = AnnotationUtils.findAnnotation(method, RPubSubListener.class);
if (listener != null) {
String channel = listener.value();
Assert.hasText(channel, "@RPubSubListener value channel must not be empty.");
log.info("Found @RPubSubListener on bean:{} method:{}", beanName, method);
// 校验 methodmethod 入参数大于等于1
int paramCount = method.getParameterCount();
if (paramCount > 1) {
throw new IllegalArgumentException("@RPubSubListener on method " + method + " parameter count must less or equal to 1.");
}
// 精准模式和模糊匹配模式
Topic topic = ChannelUtil.getTopic(channel);
redisMessageListenerContainer.addMessageListener((message, pattern) -> {
String messageChannel = new String(message.getChannel());
Object body = redisSerializer.deserialize(message.getBody());
invokeMethod(bean, method, paramCount, new RPubSubEvent<>(channel, messageChannel, body));
}, topic);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
return bean;
}
private static void invokeMethod(Object bean, Method method, int paramCount, RPubSubEvent<Object> topicEvent) {
// 支持没有参数的方法
if (paramCount == 0) {
ReflectUtil.invokeMethod(method, bean);
} else {
ReflectUtil.invokeMethod(method, bean, topicEvent);
}
}
}

View File

@ -0,0 +1,35 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
/**
* 基于 Redisson 的消息发布器
*
* @author L.cm
*/
public interface RPubSubPublisher {
/**
* 发布消息
*
* @param channel 队列名
* @param message 消息
* @return 收到消息的客户数量
*/
<T> Long publish(String channel, T message);
}

View File

@ -0,0 +1,45 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springblade.core.redis.pubsub;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.redis.cache.BladeRedis;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* Redisson pub/sub 发布器
*
* @author L.cm
*/
@Slf4j
@RequiredArgsConstructor
public class RedisPubSubPublisher implements InitializingBean, RPubSubPublisher {
private final BladeRedis bladeRedis;
private final RedisSerializer<Object> redisSerializer;
@Override
public <T> Long publish(String channel, T message) {
return bladeRedis.publish(channel, message, redisSerializer::serialize);
}
@Override
public void afterPropertiesSet() throws Exception {
log.info("RPubSubPublisher init success.");
}
}

View File

@ -0,0 +1,51 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,61 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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,74 @@
/**
* Copyright (c) 2018-2099, DreamLu 卢春梦 (qq596392912@gmail.com).
* <p>
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* <p>
* http://www.gnu.org/licenses/lgpl.html
* <p>
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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);
}
}

16
pom.xml
View File

@ -61,8 +61,8 @@
<mica.auto.version>3.1.4</mica.auto.version>
<druid.version>1.2.23</druid.version>
<spring.version>6.1.14</spring.version>
<spring.boot.version>3.2.10</spring.boot.version>
<spring.version>6.1.15</spring.version>
<spring.boot.version>3.2.12</spring.boot.version>
<spring.boot.admin.version>3.2.3</spring.boot.admin.version>
<spring.cloud.version>2023.0.3</spring.cloud.version>
@ -85,6 +85,7 @@
<module>blade-starter-log</module>
<module>blade-starter-mybatis</module>
<module>blade-starter-oss</module>
<module>blade-starter-redis</module>
<module>blade-starter-report</module>
<module>blade-starter-social</module>
<module>blade-starter-swagger</module>
@ -197,6 +198,11 @@
<artifactId>blade-starter-oss</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-redis</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.springblade</groupId>
<artifactId>blade-starter-tenant</artifactId>
@ -364,6 +370,12 @@
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
<version>2.3.0</version>
</dependency>
<!-- redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.17.7</version>
</dependency>
<!-- protostuff -->
<dependency>
<groupId>io.protostuff</groupId>