一、Spring Boot 中使用 Redis
我来详细介绍如何在 Spring Boot 项目中集成和使用 Redis。
1. 添加依赖
在 pom.xml 中添加 Spring Data Redis 依赖:
XML
<dependencies>
<!-- Spring Boot Redis 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- Lettuce 连接池(可选,推荐) -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<!-- 如果需要使用 JSON 序列化 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
2. 配置文件
在 application.yml 或 application.properties 中配置 Redis:
application.yml:
bash
spring:
redis:
host: localhost
port: 6379
password: # 如果有密码就填写
database: 0 # Redis 数据库索引(默认为0)
timeout: 3000ms # 连接超时时间
lettuce:
pool:
max-active: 8 # 连接池最大连接数
max-idle: 8 # 连接池最大空闲连接数
min-idle: 0 # 连接池最小空闲连接数
max-wait: -1ms # 连接池最大阻塞等待时间
3. Redis 配置类(推荐)
创建配置类以自定义序列化方式:
java
package com.example.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.StringRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 使用 Jackson2JsonRedisSerializer 来序列化和反序列化 value
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer =
new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
// 使用 StringRedisSerializer 来序列化和反序列化 key
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key 采用 String 的序列化方式
template.setKeySerializer(stringRedisSerializer);
// hash 的 key 也采用 String 的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
// value 序列化方式采用 jackson
template.setValueSerializer(jackson2JsonRedisSerializer);
// hash 的 value 序列化方式采用 jackson
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
4. 使用方式
方式一:直接注入 RedisTemplate
java
package com.example.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 设置值
public void setUser(String key, User user) {
redisTemplate.opsForValue().set(key, user);
}
// 设置值并设置过期时间
public void setUserWithExpire(String key, User user, long timeout) {
redisTemplate.opsForValue().set(key, user, timeout, TimeUnit.SECONDS);
}
// 获取值
public User getUser(String key) {
return (User) redisTemplate.opsForValue().get(key);
}
// 删除
public void deleteUser(String key) {
redisTemplate.delete(key);
}
// 判断 key 是否存在
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
// 设置过期时间
public void expire(String key, long timeout) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
方式二:封装 Redis 工具类(推荐)
java
package com.example.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// ========== String 操作 ==========
/**
* 设置缓存
*/
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 设置缓存并设置过期时间
*/
public void set(String key, Object value, long timeout, TimeUnit unit) {
redisTemplate.opsForValue().set(key, value, timeout, unit);
}
/**
* 获取缓存
*/
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 删除缓存
*/
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
/**
* 批量删除
*/
public Long delete(Collection<String> keys) {
return redisTemplate.delete(keys);
}
/**
* 设置过期时间
*/
public Boolean expire(String key, long timeout, TimeUnit unit) {
return redisTemplate.expire(key, timeout, unit);
}
/**
* 判断 key 是否存在
*/
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
/**
* 递增
*/
public Long increment(String key, long delta) {
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*/
public Long decrement(String key, long delta) {
return redisTemplate.opsForValue().decrement(key, delta);
}
// ========== Hash 操作 ==========
/**
* Hash 设置
*/
public void hSet(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
/**
* Hash 获取
*/
public Object hGet(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* Hash 删除
*/
public Long hDelete(String key, Object... hashKeys) {
return redisTemplate.opsForHash().delete(key, hashKeys);
}
// ========== List 操作 ==========
/**
* List 右侧推入
*/
public Long lPush(String key, Object value) {
return redisTemplate.opsForList().rightPush(key, value);
}
/**
* List 左侧弹出
*/
public Object lPop(String key) {
return redisTemplate.opsForList().leftPop(key);
}
// ========== Set 操作 ==========
/**
* Set 添加
*/
public Long sAdd(String key, Object... values) {
return redisTemplate.opsForSet().add(key, values);
}
/**
* Set 移除
*/
public Long sRemove(String key, Object... values) {
return redisTemplate.opsForSet().remove(key, values);
}
// ========== Sorted Set 操作 ==========
/**
* ZSet 添加
*/
public Boolean zAdd(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
}
5. 实际使用示例
java
@RestController
@RequestMapping("/api/user")
public class UserController {
@Autowired
private RedisUtil redisUtil;
// 缓存用户信息
@GetMapping("/{id}")
public User getUser(@PathVariable Long id) {
String key = "user:" + id;
// 先从缓存中获取
User user = (User) redisUtil.get(key);
if (user == null) {
// 缓存未命中,从数据库查询
user = userService.getUserById(id);
if (user != null) {
// 存入缓存,过期时间 30 分钟
redisUtil.set(key, user, 30, TimeUnit.MINUTES);
}
}
return user;
}
// 更新用户时删除缓存
@PutMapping("/{id}")
public void updateUser(@PathVariable Long id, @RequestBody User user) {
userService.updateUser(user);
// 删除缓存
redisUtil.delete("user:" + id);
}
}
6. 常见操作总结
| 数据类型 | RedisTemplate 方法 | 说明 |
|---|---|---|
| String | opsForValue() |
字符串操作 |
| Hash | opsForHash() |
哈希表操作 |
| List | opsForList() |
列表操作 |
| Set | opsForSet() |
集合操作 |
| Sorted Set | opsForZSet() |
有序集合操作 |
7. 注意事项
- 序列化问题:默认使用 JDK 序列化,建议使用 JSON 序列化(如上面配置类所示)
- 缓存穿透:查询不存在的数据,建议缓存空值或使用布隆过滤器
- 缓存雪崩:大量缓存同时失效,建议设置随机过期时间
- 缓存击穿:热点数据过期,建议使用互斥锁或永不过期
- 连接池配置:根据实际并发量调整连接池参数
二、为什么要编写redis配置类呢
1. 可以直接使用,但有问题
引入依赖后可以直接 @Autowired 注入使用,Spring Boot 会自动配置一个默认的 RedisTemplate。
java
@Autowired
private RedisTemplate redisTemplate; // 可以直接用
但是,默认的 RedisTemplate 有一个严重问题:序列化方式!
2. 默认序列化的问题
Spring Boot 默认的 RedisTemplate 使用 JDK 序列化(JdkSerializationRedisSerializer),会导致:
问题演示:
java
// 存入数据
redisTemplate.opsForValue().set("user:1", user);
在 Redis 中看到的数据是这样的:
Key: "\xac\xed\x00\x05t\x00\x06user:1"
Value: "\xac\xed\x00\x05sr\x00\x1ccom.example.entity.User..."
问题有哪些?
- 不可读:存储的是二进制数据,无法直接在 Redis 客户端查看
- 占用空间大:JDK 序列化后的数据比 JSON 大很多
- 跨语言不兼容:其他语言(如 Python、Go)无法读取 Java 序列化的数据
- 安全风险:JDK 序列化存在已知的安全漏洞
对比:使用 JSON 序列化
Key: "user:1"
Value: {"id":1,"name":"张三","age":25}
这样就:
- ✅ 可读性强
- ✅ 体积更小
- ✅ 跨语言兼容
- ✅ 更安全
3. 为什么是 RedisTemplate<String, Object>?
泛型说明
java
RedisTemplate<K, V>
// K: Key 的类型
// V: Value 的类型
为什么用 <String, Object>?
Key 使用 String:
java
// Redis 的 key 通常都是字符串
"user:1"
"product:100"
"cache:article:20"
Value 使用 Object:
java
// 可以存储各种类型的对象
redisTemplate.opsForValue().set("user:1", userObject); // User 对象
redisTemplate.opsForValue().set("count", 100); // Integer
redisTemplate.opsForValue().set("list", Arrays.asList(1,2,3)); // List
使用 Object 类型最灵活,可以存储任何对象。
4. 配置类做了什么?
让我简化说明配置类的作用:
java
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 关键:修改序列化方式
// 1. Key 使用 String 序列化
template.setKeySerializer(new StringRedisSerializer());
// 2. Value 使用 JSON 序列化
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// 3. Hash 的 Key 和 Value 也设置序列化方式
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
return template;
}
核心作用就是:把默认的 JDK 序列化改成 JSON 序列化!
5. 不配置 vs 配置的对比
场景:存储用户对象
java
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);
不配置(使用默认)
Redis 中存储的内容:
bash
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x06user:1"
127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x06user:1"
"\xac\xed\x00\x05sr\x00\x1c..." # 一堆乱码
配置后(使用 JSON)
Redis 中存储的内容:
bash
127.0.0.1:6379> keys *
1) "user:1"
127.0.0.1:6379> get user:1
"{\"id\":1,\"name\":\"张三\",\"age\":25}"
是不是清晰多了?
总结
- 可以不配置,但会用 JDK 序列化,导致数据不可读
- 配置的目的:改用 JSON 序列化,让数据可读、节省空间、跨语言兼容
<String, Object>:Key 用字符串,Value 用 Object 更灵活- 推荐做法:写配置类,统一使用 JSON 序列化
三、配置类中的ObjectMapper的相关操作
redis配置类中有一段代码:
java
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL );
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
让我逐行解释这段代码的作用。
这段代码在解决什么问题?
核心问题:反序列化时的类型丢失
当你从 Redis 取出数据时,Jackson 不知道原始对象是什么类型,只能反序列化成 LinkedHashMap 或其他通用类型。
问题演示
java
// 存入 User 对象
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);
// 取出时
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass());
// 输出:class java.util.LinkedHashMap(不是 User!)
// 无法直接使用
User user = (User) obj; // 报错:ClassCastException
逐行解释
1. 创建 ObjectMapper
java
ObjectMapper objectMapper = new ObjectMapper();
这是 Jackson 的核心类,负责 Java 对象和 JSON 之间的转换。
2. 设置可见性
java
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
作用:告诉 Jackson 可以访问对象的所有属性
PropertyAccessor.ALL 包括:
FIELD(字段)GETTER(get 方法)SETTER(set 方法)CREATOR(构造方法)IS_GETTER(is 方法)
JsonAutoDetect.Visibility.ANY 表示:
public可以访问protected可以访问private也可以访问 ⬅️ 关键
示例:
java
public class User {
private Long id; // private 字段
private String name; // private 字段
// 没有 getter/setter 也能序列化!
}
不设置这个配置的话:
java
// 默认只能访问 public 字段或有 getter/setter 的字段
// private 字段没有 getter 就无法序列化
3. 激活默认类型信息(重点!)
java
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL
);
这是最关键的配置!作用:在 JSON 中存储类型信息
不配置时的问题:
存入 Redis:
java
{
"id": 1,
"name": "张三",
"age": 25
}
从 Redis 取出:
java
Object obj = redisTemplate.opsForValue().get("user:1");
// obj 是 LinkedHashMap,不是 User!
// 因为 Jackson 不知道原始类型是什么
配置后的效果:
存入 Redis(包含类型信息):
java
[
"com.example.entity.User",
{
"id": 1,
"name": "张三",
"age": 25
}
]
从 Redis 取出:
java
Object obj = redisTemplate.opsForValue().get("user:1");
// obj 就是 User 类型!可以直接转换
User user = (User) obj; // ✅ 成功
参数说明:
LaissezFaireSubTypeValidator.instance
- 一个宽松的类型验证器
- 允许反序列化几乎所有类型
LaissezFaire是法语,意思是"放任自由"
ObjectMapper.DefaultTyping.NON_FINAL
- 为非 final 类添加类型信息
- 选项包括:
JAVA_LANG_OBJECT:只对 Object 类型OBJECT_AND_NON_CONCRETE:Object 和抽象类/接口NON_CONCRETE_AND_ARRAYS:抽象类、接口和数组NON_FINAL:所有非 final 类 ⬅️ 最常用EVERYTHING:所有类型
4. 设置到序列化器
java
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
把配置好的 ObjectMapper 设置给 Jackson 序列化器,让它使用我们的配置。
完整效果对比
配置前(类型丢失)
java
// 存入
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);
// Redis 中存储:
{
"id": 1,
"name": "张三",
"age": 25
}
// 取出
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass());
// 输出:class java.util.LinkedHashMap ❌
User user = (User) obj; // 报错!ClassCastException
配置后(保留类型)
java
// 存入
User user = new User(1L, "张三", 25);
redisTemplate.opsForValue().set("user:1", user);
// Redis 中存储:
[
"com.example.entity.User",
{
"id": 1,
"name": "张三",
"age": 25
}
]
// 取出
Object obj = redisTemplate.opsForValue().get("user:1");
System.out.println(obj.getClass());
// 输出:class com.example.entity.User ✅
User user = (User) obj; // 成功!
代码简化版(帮助理解)
java
ObjectMapper objectMapper = new ObjectMapper();
// 1. 让 Jackson 能访问 private 字段
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 2. 在 JSON 中保存类型信息(最重要!)
objectMapper.activateDefaultTyping(
LaissezFaireSubTypeValidator.instance, // 验证器:允许所有类型
ObjectMapper.DefaultTyping.NON_FINAL // 为非 final 类添加类型信息
);
// 3. 应用这些配置
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
注意事项
1. 安全风险
// 使用 LaissezFaireSubTypeValidator 可能有安全风险
// 因为它允许反序列化任何类型
// 生产环境可以考虑更严格的验证器
2. 性能影响
类型信息会增加存储空间
// 原始:{"id":1,"name":"张三"}
// 带类型:["com.example.User",{"id":1,"name":"张三"}]
3. 替代方案
如果不想使用类型信息,可以手动指定类型:
java
// 存入时就明确类型
ValueOperations<String, User> ops = redisTemplate.opsForValue();
ops.set("user:1", user);
// 取出时也明确类型
User user = ops.get("user:1");
总结
这段代码的核心作用:
- setVisibility:让 Jackson 能访问 private 字段
- activateDefaultTyping :在 JSON 中存储类型信息(最重要!)
- 目的:解决反序列化时类型丢失的问题
不配置 :取出来是 LinkedHashMap
配置后 :取出来是原始的 User 对象