Redis (Remote Dictionary Server) 是一款高性能的键值对存储数据库,它以内存存储为主,具有速度快、支持丰富的数据类型等特点,被广泛应用于缓存、会话管理、排行榜等场景。 Spring Boot 提供了对 Redis 的良好支持,使得我们可以轻松地在 Spring Boot 项目中集成 Redis,从而提升应用的性能和可扩展性。
本文将详细介绍如何在 Spring Boot 项目中整合 Redis,并提供完整的代码示例和最佳实践。
一、为什么要使用 Redis?
- 高性能缓存: Redis 以内存存储为主,读写速度非常快,可以作为缓存层,减少对数据库的访问,从而提升应用性能。
- 丰富的数据类型:Redis 支持 String、List、Set、Hash、Sorted Set 等多种数据类型,可以满足不同的应用场景需求。
- 会话管理: Redis 可以用于存储用户会话信息,实现会话共享和负载均衡。
- 排行榜: Redis 的 Sorted Set可以方便地实现排行榜功能。
- 消息队列: Redis 的 List 数据类型可以用于实现简单的消息队列功能。
- 分布式锁: Redis可以用于实现分布式锁,解决分布式环境下的资源竞争问题。
二、Spring Boot 整合 Redis实践
2.1 添加 Redis 依赖
java
<!-- redis 缓存操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- pool 对象池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
2.2 配置 Redis 连接信息
java
spring:
# redis 配置
redis:
# 地址
host: **.**.**.**
# 端口,默认为6379
port: 6380
# 数据库索引
database: 12
# 密码
password: ******
# 连接超时时间
timeout: 60s
lettuce:
pool:
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中的最大空闲连接
max-idle: 8
# 连接池的最大数据库连接数
max-active: 8
# #连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
2.3 重写redis中的StringRedisSerializer序列化器
java
import com.alibaba.fastjson.JSON;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.util.Assert;
import java.nio.charset.Charset;
public class HashKeyStringRedisSerializer implements RedisSerializer<Object> {
private final Charset charset;
private final String target = "\"";
private final String replacement = "";
public HashKeyStringRedisSerializer() {
this(Charset.forName("UTF8"));
}
public HashKeyStringRedisSerializer(Charset charset) {
Assert.notNull(charset, "Charset must not be null!");
this.charset = charset;
}
@Override
public Object deserialize(byte[] bytes) {
return (bytes == null ? null : new String(bytes, charset));
}
@Override
public byte[] serialize(Object object) {
String string = JSON.toJSONString(object);
if (string == null) {
return null;
}
string = string.replace(target, replacement);
return string.getBytes(charset);
}
}
2.4 创建Redis配置类
java
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
@Configuration
@EnableCaching
public class RedisConfig {
/**
* 过期时间1天
*/
private final Duration timeToLive = Duration.ofDays(1);
/**
* HaskKey的序列化方式
*/
private final HashKeyStringRedisSerializer hashKeySerializer = new HashKeyStringRedisSerializer();
/**
* String的序列化方式
*/
private final StringRedisSerializer stringSerializer = new StringRedisSerializer();
/**
* 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
*/
private final Jackson2JsonRedisSerializer valueSerializer = new Jackson2JsonRedisSerializer(Object.class);
/**
* 代码块,会优先执行
* 用来设置Jackson2JsonRedisSerializer
*/
{
ObjectMapper objectMapper = new ObjectMapper();
//设置所有访问权限以及所有的实际类型都可序列化和反序列化
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 指定序列化输入的类型,类必须是非final修饰的
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
//下面两行解决Java8新日期API序列化问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
javaTimeModule.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
javaTimeModule.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern("HH:mm:ss")));
objectMapper.registerModule(javaTimeModule);
valueSerializer.setObjectMapper(objectMapper);
}
/**
* 在SpringBoot2.0之后,spring容器自动的生成了StringRedisTemplate和RedisTemplate<Object,Object>,可以直接注入
* 但是在实际使用中,大多不会直接使用RedisTemplate<Object,Object>,而是会对key,value进行序列化,所以我们还需要新增一个配置类
* 换句话说,由于原生的redis自动装配,在存储key和value时,没有设置序列化方式,故自己创建redisTemplate实例
*
* @param factory
* @return
*/
@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// key采用String的序列化方式
template.setKeySerializer(stringSerializer);
// hash的key也采用String的序列化方式
template.setHashKeySerializer(hashKeySerializer);
// value序列化方式采用jackson
template.setValueSerializer(valueSerializer);
// hash的value序列化方式采用jackson
template.setHashValueSerializer(valueSerializer);
template.afterPropertiesSet();
return template;
}
@Bean(name = "redisCacheManager")
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 配置序列化(解决乱码的问题),通过config对象对缓存进行自定义配置
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
// 设置缓存的默认过期时间
.entryTtl(timeToLive)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer))
// 不缓存空值
.disableCachingNullValues();
//根据redis缓存配置和reid连接工厂生成redis缓存管理器
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
}
2.5 创建Redis工具类
java
import cn.hutool.core.collection.CollectionUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Component
public class RedisCache {
@Autowired
public 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.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 自增计数器
* @param key
* @param delta
* @return
*/
public Long incrementValue(final String key, final Long delta)
{
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 设置有效时间
*
* @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)
{
return redisTemplate.expire(key, timeout, unit);
}
/**
* 获得缓存的基本对象。
*
* @param key 缓存键值
* @return 缓存键值对应的数据
*/
public <T> T getCacheObject(final String key)
{
ValueOperations<String, T> operation = redisTemplate.opsForValue();
return operation.get(key);
}
/**
* 删除单个对象
*
* @param key
*/
public boolean deleteObject(final String key)
{
return redisTemplate.delete(key);
}
/**
* 删除集合对象
*
* @param collection 多个对象
* @return
*/
public long deleteObject(final Collection collection)
{
return redisTemplate.delete(collection);
}
/**
* 缓存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 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 = redisTemplate.boundSetOps(key);
Iterator<T> it = dataSet.iterator();
while (it.hasNext())
{
setOperation.add(it.next());
}
return setOperation;
}
/**
* 获得缓存的set
*
* @param key
* @return
*/
public <T> Set<T> getCacheSet(final String key)
{
return redisTemplate.opsForSet().members(key);
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <T> void setCacheMap(final String key, final Map<String, T> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 缓存Map
*
* @param key
* @param dataMap
*/
public <K, V> void setCacheCommonMap(final String key, final Map<K, V> dataMap)
{
if (dataMap != null) {
redisTemplate.opsForHash().putAll(key, dataMap);
}
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <T> Map<String, T> getCacheMap(final String key)
{
return redisTemplate.opsForHash().entries(key);
}
/**
* 获得缓存的Map
*
* @param key
* @return
*/
public <K, V> Map<K, V> getCacheCommonMap(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 deleteCacheMapValue(final String key, final String hKey)
{
redisTemplate.opsForHash().delete(key, hKey);
}
/**
* 删除并缓存List数据
*
* @param key 缓存的键值
* @param dataMap 待缓存的Map数据
* @return 缓存的对象
*/
public <T> void deleteAndSetCacheMap(final String key, final Map<String, T> dataMap)
{
if(redisTemplate.delete(key)){
setCacheMap(key, dataMap);
}
}
/**
* 删除并缓存List数据,带过期时间
*
* @param key 缓存的键值
* @param dataMap 待缓存的Map数据
* @return 缓存的对象
*/
public <T> void deleteAndSetCacheMap(final String key, final Map<String, T> dataMap, final long timeout, final TimeUnit unit)
{
if(redisTemplate.delete(key)){
setCacheMap(key, dataMap);
expire(key, timeout, unit);
}
}
/**
* 获取多个Hash中的数据
*
* @param key Redis键
* @param hKeys Hash键集合
* @return Hash对象集合
*/
public <T> List<T> getMultiCacheMapValue(final String key, final Collection<Object> hKeys)
{
return redisTemplate.opsForHash().multiGet(key, hKeys);
}
/**
* 获得缓存的基本对象列表
*
* @param pattern 字符串前缀
* @return 对象列表
*/
public Collection<String> keys(final String pattern)
{
return redisTemplate.keys(pattern);
}
/**
* 删除并缓存List数据
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long deleteAndSetCacheList(final String key, final List<T> dataList)
{
long count = 0;
if(redisTemplate.delete(key)){
count = redisTemplate.opsForList().rightPushAll(key, dataList);
}
return count;
}
/**
* 删除并缓存List数据,带过期时间
*
* @param key 缓存的键值
* @param dataList 待缓存的List数据
* @return 缓存的对象
*/
public <T> long deleteAndSetCacheList(final String key, final List<T> dataList, final long timeout, final TimeUnit unit)
{
long count = 0;
if(redisTemplate.delete(key)){
count = redisTemplate.opsForList().rightPushAll(key, dataList);
expire(key, timeout, unit);
}
return count;
}
/**
* 向集合中插入元素,并设置分数
* @param key
* @param value
* @param score
* @param <T>
*/
public <T> void addCacheZSet(final String key, final T value, double score)
{
redisTemplate.opsForZSet().add(key, value, score);
}
/**
* 批量添加ZSet
* @param key
* @param map
* @param <T>
*/
public <T> void batchAddCacheZSet(final String key, final Map<T, Double> map)
{
Set<DefaultTypedTuple<T>> set = new HashSet<>();
Iterator<Map.Entry<T, Double>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<T, Double> entry = iterator.next();
set.add(new DefaultTypedTuple<T>(entry.getKey(), entry.getValue()));
}
redisTemplate.opsForZSet().add(key, set);
}
/**
* 给指定元素添加分数
* @param key
* @param value
* @param score
* @param <T>
* @return
*/
public <T> Double incrementZSetScore(final String key, final T value, double score){
return redisTemplate.opsForZSet().incrementScore(key, value, score);
}
/**
* 获取指定元素的分数
* @param key
* @param value
* @param <T>
* @return
*/
public <T> Double getZSetScore(final String key, final T value){
return redisTemplate.opsForZSet().score(key, value);
}
/**
* 删除指定元素
* @param key
* @param values
* @param <T>
*/
public <T> void deleteZSetKey(final String key, final T[] values){
redisTemplate.opsForZSet().remove(key, values);
}
/**
* 根据key模糊删除
* @param key
* @return
*/
public Long blurDelete(final String key) {
Set<String> keys = redisTemplate.keys(key + ":*");
if(CollectionUtil.isEmpty(keys)){
return null;
}
return deleteObject(keys);
}
}
2.6 编写测试方式操作Redis
java
@RestController
@RequestMapping("/yes")
public class ExtendController extends BaseController {
@Autowired
private UserInfoService userInfoService;
@Autowired
private RedisCache redisCache;
@GetMapping("/{id}")
public Result<UserInfo> getUserById(@PathVariable int id) {
UserInfo user = userInfoService.getById(id);
redisCache.setCacheObject(String.valueOf(id), user, 1440, TimeUnit.MINUTES);
return Result.success(user);
}
}
三、最佳实践
- 选择合适的数据类型: 根据业务需求选择合适的 Redis 数据类型,例如 String、List、Set、Hash、SortedSet。
- 合理设置过期时间: 为缓存数据设置合理的过期时间,避免缓存过期导致大量请求访问数据库。
- 使用连接池: Spring Boot默认使用连接池来管理 Redis 连接,可以提高性能。
- 考虑序列化: 对于非 String类型的数据,需要考虑序列化和反序列化。Spring Boot 默认使用 JdkSerializationRedisSerializer进行序列化。
- 监控 Redis: 使用 Redis 自带的监控工具或第三方工具监控 Redis 的性能和状态。