当前位置: 首页 > news >正文

【性能篇I】为应用加速:整合 Redis 实现高速缓存

摘要

本文是《Spring Boot 实战派》系列的第六篇,正式开启性能优化之旅。文章将首先剖析“为什么需要缓存”以及缓存所能解决的性能瓶瓶颈。接着,我们将重点介绍如何在 Spring Boot 应用中无缝整合 Redis——这个当今最流行的内存数据存储。

我们将通过两种方式实战 Redis 缓存:手动操作(使用 RedisTemplate)和声明式缓存(使用 Spring Cache 及 @Cacheable 等注解)。读者将学会如何为高频读取的接口(如查询用户信息)添加缓存,从而将响应时间从几百毫秒降低到几毫秒。同时,文章也会对缓存的常见问题,如缓存穿透、击穿、雪崩进行科普,为构建高可用系统打下基础。

系列回顾:
在前面的文章中,我们已经构建了一个结构良好、安全可靠、配置灵活的应用。它就像一辆装备精良的汽车。但是,如果每次启动都需要花费很长时间预热(查询数据库),那么它的驾驶体验必然不会好。对于高频访问的 API,每次都去查询慢速的数据库,会极大地拖慢系统响应速度,并给数据库带来巨大压力。

欢迎来到性能优化的第一站!

想象一个热门电商网站的商品详情页,每秒钟可能有成千上万的用户在访问。如果每次访问都去数据库查询商品信息、价格、库存,数据库很快就会不堪重负,甚至崩溃。

这时,缓存 (Cache) 就如同一位神奇的“速记员”登场了。它的工作原理很简单:

  1. 第一次访问: 用户请求数据,应用先查缓存。缓存里没有,就去查数据库(慢速)。
  2. 存入缓存: 从数据库查到数据后,在返回给用户的同时,把这份数据的副本存入一个高速的存储介质中(比如内存)。
  3. 后续访问: 其他用户再请求同样的数据,应用直接从缓存中获取(极速),无需再访问数据库。

我们今天要使用的这位“速记员”,就是大名鼎鼎的 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 run -d --name my-redis -p 6379:6379 redis
    
    这条命令会从 Docker Hub 拉取最新的 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>

这个启动器底层依赖于 LettuceJedis 这两个 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 会根据 valuekey 生成一个缓存键去查缓存。如果命中,直接返回缓存结果,方法体内的代码根本不会执行。如果未命中,执行方法体,并将返回值放入缓存。
  • @CacheEvict(value = "users", key = "'all'"):

    • 工作原理: 无论方法执行结果如何,执行完毕后,都会根据 valuekey 去清除指定的缓存。

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();}
}

看,UserControllerUserService 的代码变得多么干净!所有的缓存逻辑都通过注解声明式地完成了。


知识拓展:缓存三大问题(科普)

  • 缓存穿透: 请求一个不存在的数据。因为缓存和数据库里都没有,每次请求都会打到数据库,失去了缓存的意义。
    • 解决: 缓存空对象。如果数据库查不到,也在缓存里存一个特殊值(如 null),并设置一个较短的过期时间。
  • 缓存击穿: 一个热点 Key 在某个时刻突然过期,导致大量并发请求瞬间全部打到数据库上,可能导致数据库崩溃。
    • 解决: 使用互斥锁或分布式锁。只让第一个请求去查询数据库并重建缓存,其他请求等待。
  • 缓存雪崩: 大量的 Key 在同一时间集体失效(比如应用重启,或设置了相同的过期时间),导致所有请求都打到数据库。
    • 解决: 在设置过期时间时,增加一个随机值,让 Key 的过期时间分散开。

总结与展望

恭喜你,你已经掌握了为应用加速的第一个“核武器”——缓存!今天我们学会了:

  • 如何快速整合 Spring Boot 与 Redis
  • 使用 RedisTemplate 手动控制缓存的读写和清除。
  • 使用 Spring Cache 注解(@Cacheable, @CacheEvict)实现声明式缓存,极大地简化了代码。
  • 了解了缓存中常见的穿透、击穿、雪崩问题。

缓存极大地提升了我们应用的“读”性能。但是,对于某些耗时的“写”操作,比如发送邮件、生成报表、处理批量数据等,如果让用户在接口上一直等待,体验会非常糟糕。

在下一篇 《【性能篇II】释放主线程:异步任务 (@Async) 与定时任务 (@Scheduled)》 中,我们将学习如何处理这些耗时任务,将它们扔到后台线程中异步执行,从而瞬间响应用户请求,并掌握如何创建周期性执行的定时任务。我们下期再见!

http://www.xdnf.cn/news/959563.html

相关文章:

  • RAID存储技术概述
  • 湖北理元理律师事务所:债务清偿方案中的法律技术革新
  • FreeRtos下创建任务失败原因记录
  • 动态元素绑定事件总失效?通过AI 对话框的开发,详解绑定逻辑!
  • @Transactional 什么情况下会失效
  • Linux应用开发之网络套接字编程(实例篇)
  • VMware Workstation踩坑指南
  • Ubuntu 可执行程序自启动方法
  • apt命令介绍
  • 【音乐分析】根据拍号结合bpm计算bar_duration
  • 每日算法题(12-1)ACM输出九九乘法表-20250609
  • DeviceNet转Modbus-RTU协议网关详细解读
  • 医疗AI模型可解释性编程研究:基于SHAP、LIME与Anchor
  • CCleaner Professional 下载安装教程 - 电脑清理优化工具详细使用指南
  • Kafka入门-监控与可视化
  • 今天做的力扣SQL
  • 二维FDTD算法仿真
  • C++ 类的定义与构造 / 析构函数解析
  • python3基础语法梳理(一)
  • 验证回文串
  • 【学习分享】shell脚本基础(全)
  • 深度解析云存储:概念、架构与应用实践
  • 外链域名年龄 vs 数量老域名的1个链接抵新域名的100个吗?
  • 【配置篇】告别硬编码:多环境配置、@ConfigurationProperties与配置中心初探
  • FDD损失函数 损失函数和梯度的关系
  • Day49 Python打卡训练营
  • 【前端】js Map集合的使用方法
  • C++11委托构造函数和继承构造函数:从入门到精通
  • 查询宝塔的数据库信息
  • 共享存储系统