【性能篇I】为应用加速:整合 Redis 实现高速缓存
摘要
本文是《Spring Boot 实战派》系列的第六篇,正式开启性能优化之旅。文章将首先剖析“为什么需要缓存”以及缓存所能解决的性能瓶瓶颈。接着,我们将重点介绍如何在 Spring Boot 应用中无缝整合 Redis——这个当今最流行的内存数据存储。
我们将通过两种方式实战 Redis 缓存:手动操作(使用 RedisTemplate
)和声明式缓存(使用 Spring Cache 及 @Cacheable
等注解)。读者将学会如何为高频读取的接口(如查询用户信息)添加缓存,从而将响应时间从几百毫秒降低到几毫秒。同时,文章也会对缓存的常见问题,如缓存穿透、击穿、雪崩进行科普,为构建高可用系统打下基础。
系列回顾:
在前面的文章中,我们已经构建了一个结构良好、安全可靠、配置灵活的应用。它就像一辆装备精良的汽车。但是,如果每次启动都需要花费很长时间预热(查询数据库),那么它的驾驶体验必然不会好。对于高频访问的 API,每次都去查询慢速的数据库,会极大地拖慢系统响应速度,并给数据库带来巨大压力。
欢迎来到性能优化的第一站!
想象一个热门电商网站的商品详情页,每秒钟可能有成千上万的用户在访问。如果每次访问都去数据库查询商品信息、价格、库存,数据库很快就会不堪重负,甚至崩溃。
这时,缓存 (Cache) 就如同一位神奇的“速记员”登场了。它的工作原理很简单:
- 第一次访问: 用户请求数据,应用先查缓存。缓存里没有,就去查数据库(慢速)。
- 存入缓存: 从数据库查到数据后,在返回给用户的同时,把这份数据的副本存入一个高速的存储介质中(比如内存)。
- 后续访问: 其他用户再请求同样的数据,应用直接从缓存中获取(极速),无需再访问数据库。
我们今天要使用的这位“速记员”,就是大名鼎鼎的 Redis。Redis 是一个开源的、基于内存的 Key-Value 数据库,因其闪电般的速度(读写性能可达 10万+ QPS),被广泛用于缓存、分布式锁、消息队列等场景。
第一步:准备工作 —— 安装并运行 Redis
在开始编码前,你需要一个可用的 Redis 服务。
- Windows 用户: 推荐使用 WSL2 (Windows Subsystem for Linux) 安装,或者直接使用 Docker。
- Mac 用户: 可以使用 Homebrew:
brew install redis
。 - Linux 用户: 使用包管理器安装,如
sudo apt-get install redis-server
。 - 最简单的方式 (通用): 使用 Docker。
这条命令会从 Docker Hub 拉取最新的 Redis 镜像,并在后台启动一个名为docker run -d --name my-redis -p 6379:6379 redis
my-redis
的容器,将容器的 6379 端口映射到宿主机的 6379 端口。
安装并启动后,你可以使用 redis-cli
命令来连接和操作 Redis。
第二步:整合 Spring Boot 与 Redis
Spring Boot 整合 Redis 极其简单,只需要两步。
1. 添加依赖 (pom.xml)
打开 pom.xml
,添加 Spring Data Redis 的启动器:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
这个启动器底层依赖于 Lettuce 或 Jedis 这两个 Redis Java 客户端。Spring Boot 2.x 以后默认使用 Lettuce,因为它基于 Netty,支持异步和响应式操作,性能更优。
2. 配置连接 (application-dev.properties)
在你的开发环境配置文件中,添加 Redis 的连接信息:
# --- Redis Settings ---
spring.redis.host=localhost
spring.redis.port=6379
# spring.redis.password= # 如果你的 Redis 设置了密码
spring.redis.database=0 # 使用 0 号数据库
好了,整合工作已经完成!重启应用,如果控制台没有报错,说明 Spring Boot 已经成功连接到了 Redis。
第三站:手动挡的乐趣 —— 使用 RedisTemplate
操作缓存
RedisTemplate
是 Spring Data Redis 提供的核心工具,它封装了对 Redis 的各种操作,让我们能像调用方法一样与 Redis 交互。
问题场景: 查询所有用户列表 (GET /users/all
) 是一个高频操作,我们希望对它进行缓存。
1. 配置 RedisTemplate
的序列化方式
默认情况下,RedisTemplate
会使用 JDK 的序列化方式将对象存入 Redis,这会导致在 Redis 客户端中看到一堆乱码,可读性极差。我们通常会把它配置成 JSON 格式。
在 config
包下创建 RedisConfig.java
:
package com.example.myfirstapp.config;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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;@Configuration
public class RedisConfig {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {RedisTemplate<String, Object> template = new RedisTemplate<>();template.setConnectionFactory(connectionFactory);// 使用 String 序列化 Keytemplate.setKeySerializer(new StringRedisSerializer());template.setHashKeySerializer(new StringRedisSerializer());// 使用 JSON 序列化 ValueGenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();template.setValueSerializer(jsonSerializer);template.setHashValueSerializer(jsonSerializer);template.afterPropertiesSet();return template;}
}
2. 改造 UserController
,手动实现缓存逻辑
package com.example.myfirstapp.controller;import com.example.myfirstapp.common.Result;
import com.example.myfirstapp.entity.User;
import com.example.myfirstapp.repository.UserRepository;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.concurrent.TimeUnit;@RestController
@RequestMapping("/users")
public class UserController {@Autowired private UserRepository userRepository;@Autowired private RedisTemplate<String, Object> redisTemplate;@Autowired private ObjectMapper objectMapper; // Spring Boot 默认配置了 ObjectMapperprivate static final String CACHE_KEY_ALL_USERS = "users:all";@GetMapping("/all")public Result<List<User>> getAllUsers() {// 1. 先从缓存中查找Object cachedData = redisTemplate.opsForValue().get(CACHE_KEY_ALL_USERS);if (cachedData != null) {System.out.println("--- 从缓存中获取用户列表 ---");// 因为 RedisTemplate 存的是 Object,需要手动转换List<User> users = objectMapper.convertValue(cachedData, new TypeReference<>() {});return Result.success(users);}// 2. 缓存未命中,查询数据库System.out.println("--- 从数据库中获取用户列表 ---");List<User> users = userRepository.findAll();// 3. 将查询结果放入缓存,并设置过期时间(例如 5 分钟)redisTemplate.opsForValue().set(CACHE_KEY_ALL_USERS, users, 5, TimeUnit.MINUTES);return Result.success(users);}// ... 其他方法 ...// 注意:当新增或删除用户时,需要清除缓存,否则会出现数据不一致@PostMapping("/add")public Result<User> addUser(@RequestBody User user) {User savedUser = userRepository.save(user);// 清除缓存redisTemplate.delete(CACHE_KEY_ALL_USERS);return Result.success(savedUser);}@DeleteMapping("/delete/{id}")public Result<Void> deleteUserById(@PathVariable Long id) {userRepository.deleteById(id);// 清除缓存redisTemplate.delete(CACHE_KEY_ALL_USERS);return Result.success();}
}
测试:
- 第一次调用
GET /users/all
,控制台会打印 “— 从数据库中获取用户列表 —”。 - 在 5 分钟内再次调用,控制台会打印 “— 从缓存中获取用户列表 —”,你会发现响应速度快了非常多!
手动挡的优缺点:
- 优点: 控制力极强,可以实现非常复杂的缓存策略。
- 缺点: 缓存逻辑与业务逻辑耦合在一起,代码变得臃肿,容易出错(比如忘记清除缓存)。
第四站:自动挡的优雅 —— Spring Cache 与声明式缓存
对于大多数场景,我们并不需要那么精细的控制。Spring Cache 提供了一套基于注解的缓存解决方案,可以让我们的业务代码恢复清爽。
1. 开启缓存功能
在你的主启动类 MyFirstAppApplication.java
或任何一个配置类上,添加 @EnableCaching
注解。
@SpringBootApplication
@EnableCaching // 开启声明式缓存
public class MyFirstAppApplication {public static void main(String[] args) {SpringApplication.run(MyFirstAppApplication.class, args);}
}
2. 使用缓存注解改造业务方法
现在,我们创建一个 UserService
来专门处理业务逻辑(这是一个更好的实践,将业务逻辑从 Controller 中分离出来)。
UserService.java
package com.example.myfirstapp.service;import com.example.myfirstapp.entity.User;
import com.example.myfirstapp.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;@Service
public class UserService {@Autowiredprivate UserRepository userRepository;@Cacheable(value = "users", key = "'all'") // 核心注解public List<User> getAllUsers() {System.out.println("--- (注解)从数据库中获取用户列表 ---");return userRepository.findAll();}@CacheEvict(value = "users", key = "'all'") // 核心注解public User addUser(User user) {return userRepository.save(user);}@CacheEvict(value = "users", key = "'all'") // 核心注解public void deleteUserById(Long id) {userRepository.deleteById(id);}
}
注解解读:
-
@Cacheable(value = "users", key = "'all'")
:value = "users"
: 指定缓存的名称(在 Redis 中会体现为 key 的一部分,如users::all
)。key = "'all'"
: 指定缓存的键。'all'
是一个 SpEL 表达式,表示一个固定的字符串 “all”。如果是基于参数的,可以写成key = "#id"
。- 工作原理: 方法调用前,Spring 会根据
value
和key
生成一个缓存键去查缓存。如果命中,直接返回缓存结果,方法体内的代码根本不会执行。如果未命中,执行方法体,并将返回值放入缓存。
-
@CacheEvict(value = "users", key = "'all'")
:- 工作原理: 无论方法执行结果如何,执行完毕后,都会根据
value
和key
去清除指定的缓存。
- 工作原理: 无论方法执行结果如何,执行完毕后,都会根据
3. 改造 UserController
,调用 UserService
@RestController
@RequestMapping("/users")
public class UserController {@Autowired private UserService userService;@GetMapping("/all")public Result<List<User>> getAllUsers() {return Result.success(userService.getAllUsers());}@PostMapping("/add")public Result<User> addUser(@RequestBody User user) {return Result.success(userService.addUser(user));}@DeleteMapping("/delete/{id}")public Result<Void> deleteUserById(@PathVariable Long id) {userService.deleteUserById(id);return Result.success();}
}
看,UserController
和 UserService
的代码变得多么干净!所有的缓存逻辑都通过注解声明式地完成了。
知识拓展:缓存三大问题(科普)
- 缓存穿透: 请求一个不存在的数据。因为缓存和数据库里都没有,每次请求都会打到数据库,失去了缓存的意义。
- 解决: 缓存空对象。如果数据库查不到,也在缓存里存一个特殊值(如
null
),并设置一个较短的过期时间。
- 解决: 缓存空对象。如果数据库查不到,也在缓存里存一个特殊值(如
- 缓存击穿: 一个热点 Key 在某个时刻突然过期,导致大量并发请求瞬间全部打到数据库上,可能导致数据库崩溃。
- 解决: 使用互斥锁或分布式锁。只让第一个请求去查询数据库并重建缓存,其他请求等待。
- 缓存雪崩: 大量的 Key 在同一时间集体失效(比如应用重启,或设置了相同的过期时间),导致所有请求都打到数据库。
- 解决: 在设置过期时间时,增加一个随机值,让 Key 的过期时间分散开。
总结与展望
恭喜你,你已经掌握了为应用加速的第一个“核武器”——缓存!今天我们学会了:
- 如何快速整合 Spring Boot 与 Redis。
- 使用
RedisTemplate
手动控制缓存的读写和清除。 - 使用 Spring Cache 注解(
@Cacheable
,@CacheEvict
)实现声明式缓存,极大地简化了代码。 - 了解了缓存中常见的穿透、击穿、雪崩问题。
缓存极大地提升了我们应用的“读”性能。但是,对于某些耗时的“写”操作,比如发送邮件、生成报表、处理批量数据等,如果让用户在接口上一直等待,体验会非常糟糕。
在下一篇 《【性能篇II】释放主线程:异步任务 (@Async) 与定时任务 (@Scheduled)》 中,我们将学习如何处理这些耗时任务,将它们扔到后台线程中异步执行,从而瞬间响应用户请求,并掌握如何创建周期性执行的定时任务。我们下期再见!