目录
测试环境:
springboot 3.1.5
maven:3.6.1
java:17
依赖
<!--集成redis 不需要导入spring-boot-starter-cache,即可同时使用cache-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置类
redis、cache序列化以及@cacheable的key策略
package com.muyi.common.core.redis;
import com.muyi.common.core.cache.CacheKeys;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.util.Arrays;
/**
* Redis配置
* <pre>
*
* redis 自定义前缀:CacheKeys.getAppMark() ;实现参见see
* cache 自定义前缀:CacheKeys.getCachePrefix();在本类中实现
* cache-@Cacheable key自动生成器
* </pre>
* <pre>
* 需要在引用此类的项目或模块中的某个application-base.yml中添加如下配置
* spring:
* data:
* # redis缓存
* redis:
* database: ${muyi.system.redis.database}
* host: ${muyi.system.redis.host}
* #Redis服务器连接端口
* port: ${muyi.system.redis.port}
* #Redis服务器连接密码(默认为空)
* password: ${muyi.system.redis.password}
* #连接超时时间(毫秒)
* timeout: ${muyi.system.redis.timeout}
* client-type: LETTUCE
* #连接池
* lettuce:
* pool:
* #连接池最大连接数(使用负值表示没有限制)
* max-active: 20
* #连接池最大阻塞等待时间(使用负值表示没有限制)
* max-wait: -1
* #连接池中的最大空闲连接
* max-idle: 8
* #连接池中的最小空闲连接
* min-idle: 0
* cache:
* redis:
* key-prefix: ${muyi.system.appMark}
* time-to-live: ${muyi.system.cache.timeToLive}
* use-key-prefix: true
* cache-null-values: true
* ${muyi.system.redis.host}是yml文件间的引用,例如其配置在application-muyi.yml(包含本项目相关的可变参数)中,
* 而在application-base.yml(包含项目的基本不变参数,以及引用参数)中引用。
* 在application.yml中添加如下配置,完成对-muyi、-base的引用
* spring:
* profiles:
* include:
* - muyi
* - base
* </pre>
* @see RedisKeyPrefixSerializer
* @see org.springframework.cache.annotation.Cacheable
* @author MuYi
* @version 1.0
* @date 2023/11/24 9:36
**/
@EnableConfigurationProperties(CacheProperties.class)
@EnableCaching
@Configuration
public class RedisConfig {
public static final String GROUP = ":";
/**
* 使用Jackson2JsonRedisSerialize、RedisKeyPrefixSerializer、StringRedisSerializer 替换默认序列化
*/
@Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setKeySerializer(new RedisKeyPrefixSerializer());
// 设置hash的key的序列化方式
template.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化方式json,使用GenericJackson2JsonRedisSerializer替换默认序列化,RedisSerializer.json() 等价于 new GenericJackson2JsonRedisSerializer()
template.setValueSerializer(RedisSerializer.json());
// 设置hash的value的序列化方式
template.setHashValueSerializer(RedisSerializer.json());
template.setConnectionFactory(connectionFactory);
// 缓存支持回滚(事务管理)
template.setEnableTransactionSupport(true);
// 使配置生效
template.afterPropertiesSet();
return template;
}
/**
* 配置事务管理器
*/
@Bean public PlatformTransactionManager transactionManager(DataSource dataSource) throws SQLException {
return new DataSourceTransactionManager(dataSource);
}
/**
* 配置自动化缓存使用的序列化方式以及过期时间
*/
@Bean public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.string()))
.serializeValuesWith(RedisSerializationContext.SerializationPair .fromSerializer(RedisSerializer.json()))
//注意:此处在key前加入自定义前缀
.prefixCacheNameWith(CacheKeys.getCachePrefix() + GROUP);
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
/**
* Cache的 keys生成策略
* <pre>
* @Cacheable(value = "dept" ,keyGenerator ="myKeyGenerator",sync = true)
* </pre>
*/
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return (target, method, params) -> method.getName() + "[" + Arrays.asList(params).toString() + "]";
}
}
redis 自动增加key前缀
注意:
cache的key(cacheName)前缀可以调用本类,亦可以同过自定义RedisCacheConfiguration.prefixCacheNameWith(CacheKeys.getCachePrefix() + GROUP)
实现。本说明采用后者。
package com.muyi.common.core.redis;
import com.muyi.common.core.cache.CacheKeys;
import jakarta.annotation.Nullable;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets;
/**
* RedisSerializer<String> 字符串key自动增加前缀
* <pre>
* </pre>
*
* @author MuYi
* @version 1.0
* @date 2023/12/2 8:35
**/
@Component
public class RedisKeyPrefixSerializer implements RedisSerializer<String> {
public static final String GROUP = ":";
@Override public byte[] serialize(@Nullable String bytes) {
return (bytes == null) ? null:
(CacheKeys.getAppMark() + GROUP + bytes).getBytes(StandardCharsets.UTF_8);
}
@Override public String deserialize(@Nullable byte[] bytes) {
return (bytes==null)?null:
(new String(bytes, StandardCharsets.UTF_8)).replaceFirst(CacheKeys.getAppMark()+ GROUP, "");
}
}
Rediscache工具类
package com.muyi.common.core.redis;
import com.muyi.common.core.cache.CacheKeys;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* spring redis 工具类
*
* @author MuYi
* @version 1.0
* @date 2022/4/8 15:36
**/
@SuppressWarnings(value = {"unchecked", "rawtypes"})
@Component
public class RedisCache {
protected RedisTemplate<String, Object> redisTemplate;
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
@Autowired
protected void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
*/
public <T> void setCacheObject(final String key, final T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存基本的对象,Integer、String、实体类等
*
* @param key 缓存的键值
* @param value 缓存的值
* @param timeout 时间
* @param timeUnit 时间颗粒度
*/
public <T> void setCacheObject(final String key, final T value, final Integer timeout, final TimeUnit timeUnit) {
redisTemplate.delete(key);
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout) {
return expire(key, timeout, TimeUnit.SECONDS);
}
/**
* 设置有效时间
*
* @param key Redis键
* @param timeout 超时时间
* @param unit 时间单位
* @return true=设置成功;false=设置失败
*/
public boolean expire(final String key, final long timeout, final TimeUnit unit) {
if (StringUtils.isBlank(key)) {
return false;
}
if (Boolean.FALSE.equals(redisTemplate.hasKey(key))) {
return false;
}
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, unit));
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key) {
ValueOperations<String, T> operation = (ValueOperations<String, T>) redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 是否存在键值
*
* @param key 键值
* @return 是否
*/
public Boolean exists(final String key) {
return redisTemplate.hasKey(key);
}
/**
* 是否存在键值
*
* @param key 键值
* @param hKey Hash键值
* @return 是否
*/
public Boolean exists(final String key, String hKey) {
return redisTemplate.opsForHash().hasKey(key, hKey);
}
/**
* 删除单个对象
*
* @param key 缓存的键值
*/
public boolean deleteObject(final String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
/**
* 删除所有前缀为指定内容的key
* <pre>
* 注意:如果Redis、Cache进行了自动增加Key处理,则实际处理的匹配字符串为:
* 自定义前缀+'*'+keyPrefix+'*'
* </pre>
* @param keyPrefix key前缀,为空不执行
* @return 被删除的键的数量。在管道/事务中使用时为空
*/
public Long deleteObjects(final String keyPrefix) {
if(StringUtils.isBlank(keyPrefix)) {
return 0L;
}
Set<String> keys = redisTemplate.keys(keyPrefix + "*");
return (keys == null || keys.isEmpty())?0L: redisTemplate.delete(keys);
}
/**
* 删除所有包含指定内容的key
* <pre>
* 注意:如果Redis、Cache进行了自动增加Key处理,则实际处理的匹配字符串为:
* 自定义前缀+'*'+keyPrefix+'*'
* </pre>
*
* @param keyInclude key中包含字符串,为空不执行
* @return 被删除的键的数量。在管道/事务中使用时为空
*/
public Long deleteObjectsAsBlurry(final String keyInclude){
if(StringUtils.isBlank(keyInclude)) {
return 0L;
}
Set<String> keys = redisTemplate.keys("*"+keyInclude + "*");
return (keys == null || keys.isEmpty())?0L: redisTemplate.delete(keys);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return 删除数
*/
public long deleteObject(Collection collection) {
if (collection != null) {
return redisTemplate.delete(collection);
} else {
return 0L;
}
}
/**
* 缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long setCacheList(final String key, final List<T> dataList) {
Long count = redisTemplate.opsForList().rightPushAll(key, dataList);
return count == null ? 0 : count;
}
/**
* 获得缓存的list对象
*
* @param key 缓存的键值
* @return 缓存键值对应的数据
*/
public <T> List<T> getCacheList(final String key) {
return (List<T>) redisTemplate.opsForList().range(key, 0, -1);
}
/**
* 缓存Set
*
* @param key 缓存键值
* @param dataSet 缓存的数据
* @return 缓存数据的对象
*/
public <T> BoundSetOperations<String, T> setCacheSet(final String key, final Set<T> dataSet) {
BoundSetOperations<String, T> setOperation = (BoundSetOperations<String, T>) redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext()) {
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key 缓存的键值
* @return Set<T>
*/
public <T> Set<T> getCacheSet(final String key) {
return (Set<T>) redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key 缓存的键值
* @param dataMap Map<String, T>数据
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap) {
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key 缓存的键值
* @return Map<String, T>
*/
public Map<Object, Object> getCacheMap(final String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 往Hash中存入数据
*
* @param key Redis键
* @param hKey Hash键
* @param value 值
*/
public <T> void setCacheMapValue(final String key, final String hKey, final T value) {
redisTemplate.opsForHash().put(key, hKey, value);
}
/**
* 获取Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
* @return Hash中的对象
*/
public <T> T getCacheMapValue(final String key, final String hKey) {
HashOperations<String, String, T> opsForHash = redisTemplate.opsForHash();
return opsForHash.get(key, hKey);
}
/**
* 删除Hash中的数据
*
* @param key Redis键
* @param hKey Hash键
*/
public void delCacheMapValue(final String key, final String hKey) {
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.delete(key, hKey);
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys) {
return (List<T>) redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern) {
return redisTemplate.keys(pattern);
}
/**
* 获取使用的CacheKeys#getAppMark()
*
* @return 字符串
* @see CacheKeys#getAppMark()
*/
public String getAppMark() {
return CacheKeys.getAppMark();
}
/**
* 获取使用的CacheKeys#getCachePrefix()
*
* @return 字符串
* @see CacheKeys#getCachePrefix()
*/
public String getCachePrefix() {
return CacheKeys.getCachePrefix();
}
}
外部数据访问使用cache
- 一般在controller中查询使缓存,在新增/更新/删除使失效
/**
* 新建或更新记录
*
* @param entity id为null为新建否则为更新
* @return 是否成功
*/
@Operation(summary = "新建或更新记录", description = "id为null为新建否则为更新")
@ModelViewExceptionProcess
@PreAuthorize("hasAnyAuthority('system:dept:add','system:dept:edit')")
@PutMapping("/addOrUpdate")
@OperaLog(title = MODEL_TITLE, operaType = OperaType.UPDATE, isSaveRequestData = false, isSaveResponseData = true)
@ResponseBody@Cacheable(value = "dept")
public Result<Boolean> addOrUpdate(SysDept entity) {
return deptService.addOrUpdate(entity);
}
/**
* 获得全部信息,可能含关联信息
*
* @param conditions 条件
* @param basic 基本内容
* @return Result<List < SysDept>>
*/
@Operation(summary = "获得全部信息,含关联信息")
@PreAuthorize("hasAuthority('system:dept:list')")
@ModelViewExceptionProcess
@GetMapping("/list")
@ResponseBody@Cacheable(value = "dept", keyGenerator = "myKeyGenerator", sync = true)
public Result<List<SysDept>> list(SysDept conditions, boolean basic) {
PageUtils.startPage();
Result<List<SysDept>> result = deptService.selectByCondition(conditions, basic);
PageUtils.setTotal(result);
return result;
}
程序内任意调用
@Autowired
private RedisCache redisCache;
@Test
void test() {
SysUser user = new SysUser();
user.setId(1L);
user.setUsername("admin");
user.setPassword("admin");
redisCache.setCacheObject(":user", user);
redisCache.setCacheObject(":test", "testvalue测试数据");
redisCache.setCacheMapValue("map","user",user);
SysUser user1 = redisCache.<SysUser>getCacheObject(":user");
System.out.println(user1);
String test = redisCache.<String>getCacheObject(":test");
System.out.println(test);
SysDept dept = new SysDept();
dept.setId(1L);
dept.setDeptName("测试部门");
redisCache.setCacheObject(":dept", dept);
SysDept dept1 = redisCache.<SysDept>getCacheObject(":dept");
System.out.println(dept1);
}
注意事项
key的层级构成
可以使用“:”进行层级分割
例如:key1=“GPHRM:user”、key2=“GPHRM:test”
通过设置database来区分不同项目使用不同数据表,避免项目间的键名冲突
#Redis数据库索引(一般为0-15,可通过redis命令行 database n修改个数),多app时应有所区别
spring.data.redisk.database: 2
如果已无空白数据表可用,使用下面方法
通过设置key-prefix来区同一张数据表内的key分组,避免键名冲突
#本例中未起作用
spring.cache.redis.key-prefix: 你的键名前缀
spring.cache.redis.use-key-prefix: true
设置合理的过期/存活时间
# 存活时间
# 格式参见Duration 采用ISO-8601时间格式。格式为:PnYnMnDTnHnMnS (n为个数)
# P=开始标记,T=日期和时间的分割标记;日期后缀Y=年,M=月,D=天;时间后缀: M=分钟,H=小时,D=天,S=秒
spring.cache.redis.time-to-live: PT1H