目录
JDK Serialization (Java 默认序列化)
SpringBoot使用Redis的核心逻辑
关于Redis相关的组件主要由Reids连接池(RedisConnectionFactory),对象模板(RedisTemplate)、缓存管理器(RedisManager),根据不同的缓存需求场景构造对象模板、缓存管理器对象时指定不同的序列化器,并将其注册到连接池中。
其中可以根据不同项目、场景对于序列化、CacheManger的需求,定义自定义的序列化器和缓存管理器进行扩展。
SpringBoot的Redis配置类
引入Redis的依赖:
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
进行yml文件相关配置
spring:
redis:
host: 127.0.0.1 # Redis 服务器的主机地址
port: 6379 # Redis 服务器的端口号
password: XXX # Redis 服务器的密码(如果存在)
database: 0 # 使用的数据库索引,默认为 0
timeout: 5000 # 连接超时时间(毫秒),默认为 2000 毫秒
lettuce:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(负值表示不限制)
max-idle: ¾ # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲连接
进行configuration类编写,进行对象模板的构造,Spring Boot 提供了原生的Start来适配Redistribution,原则上不需要额外构建RedisConnectionFactory 连接池的Bean, RedisTemplate 和 StringRedisTemplate 两种对象模板是Spring框架自带的操作 Redis的对象模板。原则上是不需要进行额外的操作,这里主要是对对象模板的序列化方式进行指定。
部分SpringBoot版本直接引用spring-boot-starter-data-redis可能不会自动化配置生成RedisConnectionFactory ,此种情况需要进行手动创建连接池的bean
Redis连接池Bean配置
@Configuration
public class RedisConfigure {
@Autowired
RedisProperty reidis;
@Bean
public RedisConnectionFactory getRedisConnectionFactory(){
RedisStandaloneConfiguration configuration = new RedisStandaloneConfiguration();
configuration.setHostName(reidis.getHost()); //通过实体获取yml配置的内容
configuration.setPort(redis.getPort());
configuration.setDatabase(redis.getDataBase());
//如果还有其他参数同样注册上
return new LettuceConnectionFactory(configuration);
}
}
对象模板注册:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置键的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
//针对对象类型,使用默认的对象序列化
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jacksonSeial.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
// 设置哈希键和值的序列化器
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 启用事务支持
template.setEnableTransactionSupport(true);
// 设置默认序列化器
template.setDefaultSerializer(jackson2JsonRedisSerializer);
// 设置错误处理器
template.setErrorHandler(new DefaultErrorHandler());
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template;
}
}
}
// 自定义错误处理器
public class DefaultErrorHandler implements ErrorHandler {
@Override
public void handleError(Throwable t) {
// 自定义错误处理逻辑
System.err.println("Redis error occurred: " + t.getMessage());
}
}
针对 RedisTemplate的配置,主要是是序列化,包括键/值序列化、默认序列化、针对Hash类型的数据的序列化等
序列化与反序列化
Redis 支持多种序列化和反序列化机制,序列化主要是将复杂的数据结构转换为可以在 Redis 中存储的字节流,以及将这些字节流转回原始的数据结构。根据业务、项目中的数据结构特点、Redis的使用,要选择适合的序列化机制,具体的序列化机制有以下几种:
JDK Serialization (Java 默认序列化)
优点: JDK 序列化是一种通用的序列化方法,它可以序列化任何实现了 Serializable 接口的对象。
缺点: 序列化后的数据较大,效率较低,且不兼容非 Java 环境。
示例:
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer();
String Serialization (字符串序列化)
优点: 简单直观,适用于简单的字符串数据。
缺点: 不适合复杂对象。如果需要缓存Java中的对象,则不建议使用该序列化方法
示例:
import org.springframework.data.redis.serializer.StringRedisSerializer;
StringRedisSerializer stringSerializer = new StringRedisSerializer();
JSON Serialization (JSON 序列化)
优点: 产生的数据较小,易于阅读和调试,跨语言支持良好。
缺点: 序列化和反序列化的性能可能不如原生二进制格式。
示例:
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
ObjectMapper objectMapper = new ObjectMapper();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer(objectMapper);
封装Reids操作接口
定义通用接口
创建基本的Redis服务类接口,便于使用:
/**
* Redis 缓存数据操作
*/
public interface RedisCacheServer {
/**
* 缓存 String 内容
* @param key 键
* @param value 值
*/
void setString(final String key, final String value);
/**
* 删除 String 缓存
* @param key 键
*/
void removeString(final String key);
/**
* 读取 String 缓存
* @param key 键
* @return 值
*/
String getString(final String key);
/**
* 读取 String 缓存,如果键不存在则返回缺省值
* @param key 键
* @param defaultStr 缺省值
* @return 值或缺省值
*/
String getString(final String key, final String defaultStr);
/**
* 缓存哈希表
* @param key 键
* @param map 哈希表
*/
<T> void setHash(final String key, final Map<String, T> map);
/**
* 删除哈希表缓存
* @param key 键
*/
void removeHash(final String key);
/**
* 读取哈希表
* @param key 键
* @return 哈希表
*/
<T> Map<String, T> getHash(final String key);
/**
* 读取哈希表,如果键不存在则返回缺省值
* @param key 键
* @param defaultMap 缺省哈希表
* @return 哈希表或缺省值
*/
<T> Map<String, T> getHash(final String key, final Map<String, T> defaultMap);
/**
* 向列表中添加元素
* @param key 键
* @param values 值
*/
<T> void addList(final String key, final List<T> values);
/**
* 删除列表缓存
* @param key 键
*/
void removeList(final String key);
/**
* 获取全部数据
* @param key key
* @return List
* @param <T>
*/
<T> List<T> getList(final String key);
/**
* 从列表中读取指定范围的元素
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 元素列表
*/
<T> List<T> getList(final String key, final long start, final long end);
/**
* 向集合中添加成员
* @param key 键
* @param members 成员
*/
<T> void addSet(final String key, final Set<T> members);
/**
* 删除集合缓存
* @param key 键
*/
void removeSet(final String key);
/**
* 读取集合中的所有成员
* @param key 键
* @return 成员集合
*/
<T> Set<T> getSet(final String key);
/**
* 向有序集合中添加成员
* @param key 键
* @param scoreMembers 分数-成员映射
*/
<T> void addZSet(final String key, final Map<T, Double> scoreMembers);
/**
* 删除有序集合缓存
* @param key 键
*/
void removeZSet(final String key);
/**
* 从有序集合中读取指定范围的成员
* @param key 键
* @param start 开始索引
* @param end 结束索引
* @return 成员集合
*/
<T> Set<T> getZSet(final String key, final long start, final long end);
/**
* 获取有序集合中成员的分数
* @param key 键
* @param member 成员
* @return 分数
*/
<T> Double getZSetScore(final String key, final T member);
}
接口实现
对应实现类
@Component
public class RedisCacheServerImpl implements RedisCacheServer {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 字符串操作
@Override
public void setString(final String key, final String value) {
stringRedisTemplate.opsForValue().set(key, value);
}
@Override
public void removeString(final String key) {
stringRedisTemplate.delete(key);
}
@Override
public String getString(final String key) {
return stringRedisTemplate.opsForValue().get(key);
}
@Override
public String getString(final String key, final String defaultStr) {
String result = stringRedisTemplate.opsForValue().get(key);
return result != null ? result : defaultStr;
}
// 哈希表操作
@Override
public <T> void setHash(final String key, final Map<String, T> map) {
redisTemplate.opsForHash().putAll(key, map);
}
@Override
public void removeHash(final String key) {
redisTemplate.delete(key);
}
@Override
public <T> Map<String, T> getHash(final String key) {
Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);
if (entries == null || entries.isEmpty()) {
return Collections.emptyMap();
}
return entries.entrySet().stream()
.collect(Collectors.toMap(
e -> (String) e.getKey(),
e -> (T) e.getValue()
));
}
@Override
public <T> Map<String, T> getHash(final String key, final Map<String, T> defaultMap) {
Map<String, T> result = getHash(key);
return result != null && !result.isEmpty() ? result : defaultMap;
}
// 列表操作
@Override
public <T> void addList(final String key, final List<T> values) {
redisTemplate.opsForList().rightPushAll(key, values);
}
@Override
public void removeList(final String key) {
redisTemplate.delete(key);
}
@Override
public <T> List<T> getList(final String key) {
Long size = redisTemplate.opsForList().size(key);
if (size == null || size == 0) {
return null;
}
return (List<T>) redisTemplate.opsForList().range(key, 0, size - 1);
}
@Override
public <T> List<T> getList(final String key, final long start, final long end) {
return (List<T>) redisTemplate.opsForList().range(key, start, end);
}
// 集合操作
@Override
public <T> void addSet(final String key, final Set<T> members) {
redisTemplate.opsForSet().add(key, members.toArray(new Object[0]));
}
@Override
public void removeSet(final String key) {
redisTemplate.delete(key);
}
@Override
public <T> Set<T> getSet(final String key) {
return (Set<T>) redisTemplate.opsForSet().members(key);
}
// 有序集合操作
@Override
public <T> void addZSet(final String key, final Map<T, Double> scoreMembers) {
for (Map.Entry<T, Double> entry : scoreMembers.entrySet()) {
redisTemplate.opsForZSet().add(key, entry.getKey(), entry.getValue());
}
}
@Override
public void removeZSet(final String key) {
redisTemplate.delete(key);
}
@Override
public <T> Set<T> getZSet(final String key, final long start, final long end) {
return (Set<T>) redisTemplate.opsForZSet().rangeByScore(key, start, end);
}
@Override
public <T> Double getZSetScore(final String key, final T member) {
return redisTemplate.opsForZSet().score(key, member);
}
}
模板对象线程安全问题
总的来说,Redis 服务器是单线程的,因此它是线程安全的。但是在客户端应用程序中,特别是多线程环境下,需要确保正确地管理和使用 RedisTemplate 和相关资源的线程问题。
RedisTemplate 和 StringRedisTemplate 是 Spring Data Redis 提供的线程安全的模板类。它们内部使用连接池来管理 Redis 连接,连接池本身也是线程安全的。
只需要确保在配置 RedisTemplate 和 StringRedisTemplate 时没有引入任何非线程安全的组件。 也就是需要保证模板对象时所使用的序列化器、键和值的转换器以及其他相关组件必须是线程安全的。
自定义序列化器线程安全
常见的序列化器包括 JdkSerializationRedisSerializer, StringRedisSerializer, GenericJackson2JsonRedisSerializer 等,这些序列化器通常是线程安全的。需要注意的是自定义的序列化器,需要确保它是线程安全的。
public class CustomSerializer implements RedisSerializer<MyObject> {
@Override
public byte[] serialize(MyObject t) throws SerializationException {
// 确保这里的实现是线程安全的
// 例如,不要在这里使用静态变量或共享状态
// 可以使用线程局部变量(ThreadLocal)来存储临时状态
return ...;
}
@Override
public MyObject deserialize(byte[] bytes) throws SerializationException {
// 确保这里的实现是线程安全的
return ...;
}
}
自定义键值转换器线程安全
RedisTemplate 和 StringRedisTemplate 也可以使用键和值的转换器。这些转换器也应该设计成线程安全的。
public class CustomKeyConverter implements Converter<String, String> {
@Override
public String convert(String source) {
// 确保这里的实现是线程安全的
// 例如,不要在这里使用静态变量或共享状态
return "prefix:" + source;
}
}
关联@Cacheable注解使用Redis
接下来基于SpringBoot的@Cache相关注解,适配到Redis作为主缓存,基于注解进行数据操作。
@Cacheable基础使用
@Cache 注解家族是 Spring Framework 提供的一种声明式缓存机制,可以非常方便地在方法级别启用缓存。使用该家族注解必须在启动类或者配置类中通过@EnableCaching开启缓存使用:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
关于@Cache家族主要有以下几个常用注解及其使用方法:
@Cacheable
作用:@Cacheable 注解用于标记一个方法,表示该方法的结果是可以被缓存的。当方法被调用时,如果缓存中存在结果,则直接返回缓存中的结果,否则执行方法并将结果存入缓存。
常用参数:
- value 或 cacheNames:指定缓存的名称。
- key:指定缓存的键。可以使用 SpEL 表达式。
- unless:条件表达式,只有当该表达式为 false 时才缓存结果。
- condition:条件表达式,只有当该表达式为 true 时才缓存结果。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
//缓存结果不为null的数据,key为参数的id
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// ……读取数据库,组装数据
return new User(id, "John Doe");
}
}
@CachePut
作用:@CachePut 注解用于标记一个方法,表示该方法的结果应该总是被放入缓存中,无论缓存中是否已经存在该结果。该方法总是会被执行。
常用参数:
- value 或 cacheNames:指定缓存的名称。
- key:指定缓存的键。可以使用 SpEL 表达式。
- unless:条件表达式,只有当该表达式为 false 时才更新缓存。
- condition:条件表达式,只有当该表达式为 true 时才更新缓存。
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@CachePut(value = "users", key = "#user.id", unless = "#result == null")
public User updateUser(User user) {
// 模拟更新用户信息
return user;
}
}
@CacheEvict
作用:@CacheEvict 注解用于标记一个方法,表示该方法在执行完毕后应该清除缓存中的某些条目。
常用参数:
- value 或 cacheNames:指定缓存的名称。
- key:指定缓存的键。可以使用 SpEL 表达式。
- allEntries:布尔值,如果为 true,则清空整个缓存。
- beforeInvocation:布尔值,默认为 false。如果为 true,则在方法调用之前清除缓存。
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
// 模拟删除用户
}
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
// 清空所有用户的缓存
}
}
@Caching
作用:@Caching 注解用于组合多个 @Cacheable、@CachePut 和 @CacheEvict 注解,可以在一个方法上同时应用多个缓存操作。
常用参数:
- cacheable:数组,包含多个 @Cacheable 注解。
- put:数组,包含多个 @CachePut 注解。
- evict:数组,包含多个 @CacheEvict 注解。
import org.springframework.cache.annotation.Caching;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Caching(
put = {
@CachePut(value = "users", key = "#user.id", unless = "#result == null")
},
evict = {
@CacheEvict(value = "oldUsers", allEntries = true)
}
)
public User updateUserAndClearOldUsers(User user) {
// 更新用户信息并清空 oldUsers 缓存
return user;
}
}
@Cache适配Redis作为缓存存储器
相关Spring的缓存注解适配器的原理可以参考设计模式:从Spring的缓存实现来理解适配器模式-优快云博客
使用@Cache家族注解以Redis进行数据交互,主要需要向Redis的连接池注册一个RedisManager组件。
以下我们自定义一个支持指定缓存时长的缓存管理器
public class CustomRedisCacheManager extends RedisCacheManager {
private final ReentrantLock lock = new ReentrantLock();
public CustomRedisCacheManager(RedisCacheWriter connectionFactory, RedisCacheConfiguration defaultCacheConfig) {
super(connectionFactory, defaultCacheConfig);
}
@Override
protected RedisCache createRedisCache(String name, RedisCacheConfiguration cacheConfig) {
//创建一个可以在@Cache中通过#符指定缓存时长的对象
String[] arrays = StringUtils.delimitedListToStringArray(name,"#");
name = arrays[0];
//如果存在#则代表用户使用了缓存时长,此数据超时就要清除
if(arrays.length > 1){
long timeOut = Long.parseLong(arrays[1]);
cacheConfig = cacheConfig.entryTtl(Duration.ofSeconds(timeOut));
}
return super.createRedisCache(name, cacheConfig);
}
public void clearCache() {
try {
lock.lock(); // 确保线程安全
for (String cacheName : getCacheNames()) {
getCache(cacheName).clear();
}
} finally {
lock.unlock();
}
}
}
在之前的Redis配置类中多注入一个CacheManger的Bean:
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置键的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
//针对对象类型,使用默认的对象序列化
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jacksonSeial.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
// 设置哈希键和值的序列化器
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(jackson2JsonRedisSerializer);
// 启用事务支持
template.setEnableTransactionSupport(true);
// 设置默认序列化器
template.setDefaultSerializer(jackson2JsonRedisSerializer);
// 设置错误处理器
template.setErrorHandler(new DefaultErrorHandler());
template.afterPropertiesSet();
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory connectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(connectionFactory);
template.afterPropertiesSet();
return template;
}
@Bean
public CacheManager getCacheManager(RedisConnectionFactory factory){
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 redis 的 value 值
Jackson2JsonRedisSerializer<Object> jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);
//针对对象类型,使用默认的对象序列化
ObjectMapper om = new ObjectMapper();
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
jacksonSeial.setObjectMapper(om);
// 使用 StringRedisSerializer 来序列化和反序列化 redis 的 key 值
StringRedisSerializer stringSerial = new StringRedisSerializer();
RedisCacheConfiguration defaultConfigure = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))// 默认通过Cache注解的缓存数据过期时间为1天
.computePrefixWith(cacheName -> "web:test:"+cacheName) //指定通过Cache注解缓存的数据存放的层级
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerial))//指定序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jacksonSeial));
CustomRedisCacheManager redisCacheManager = new CustomRedisCacheManager(RedisCacheWriter.nonLockingRedisCacheWriter(factory),defaultConfigure);
return redisCacheManager;
}
}
}
至此,则可以通过@Cache家族注解向Redis中进行数据交互了:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
//缓存结果不为null的数据,key为参数的id, 数据会缓存 10 S,10S后自动消除
@Cacheable(value = "users#600", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// ……读取数据库,组装数据
return new User(id, "John Doe");
}
}