🎓博主介绍:Java、Python、js全栈开发 “多面手”,精通多种编程语言和技术,痴迷于人工智能领域。秉持着对技术的热爱与执着,持续探索创新,愿在此分享交流和学习,与大家共进步。
📖全栈开发环境搭建运行攻略:多语言一站式指南(环境搭建+运行+调试+发布+保姆级详解)
👉感兴趣的可以先收藏起来,希望帮助更多的人
SpringBoot + Redis 实战:缓存穿透、雪崩、击穿解决方案
一、引言
在现代的互联网应用中,缓存是提高系统性能和响应速度的重要手段之一。Redis 作为一款高性能的内存数据库,被广泛应用于缓存场景。而 Spring Boot 作为一个快速开发框架,与 Redis 结合可以轻松实现缓存功能。然而,在使用 Redis 缓存时,会遇到缓存穿透、雪崩、击穿等问题,这些问题如果不加以解决,会严重影响系统的稳定性和性能。本文将详细介绍这些问题的产生原因,并给出相应的解决方案,同时结合 Spring Boot 进行实战演示。
二、Redis 缓存基础回顾
2.1 Redis 简介
Redis 是一个开源的、基于内存的数据结构存储系统,它可以用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串(String)、哈希(Hash)、列表(List)、集合(Set)和有序集合(ZSet)等,这使得它在不同的应用场景中都能发挥出色的性能。
2.2 Spring Boot 集成 Redis
在 Spring Boot 项目中集成 Redis 非常简单,只需要添加相应的依赖,并进行简单的配置即可。
2.2.1 添加依赖
在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2.2 配置 Redis
在 application.properties
或 application.yml
中配置 Redis 连接信息:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.password=
2.2.3 使用 RedisTemplate
在 Spring Boot 中,可以使用 RedisTemplate
来操作 Redis 缓存。以下是一个简单的示例:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
}
三、缓存穿透问题及解决方案
3.1 缓存穿透的定义和原因
缓存穿透是指客户端请求的数据在缓存和数据库中都不存在,这样每次请求都会直接访问数据库,导致数据库压力过大。常见的原因是恶意攻击,攻击者可能会发送大量不存在的请求来消耗系统资源。
3.2 解决方案
3.2.1 布隆过滤器
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。在缓存穿透的场景中,可以使用布隆过滤器来过滤掉那些不存在的请求。
以下是一个使用 Google Guava 实现布隆过滤器的示例:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import java.nio.charset.Charset;
@Service
public class BloomFilterService {
private BloomFilter<String> bloomFilter;
@PostConstruct
public void init() {
// 预计插入的数据量
int expectedInsertions = 1000000;
// 误判率
double fpp = 0.001;
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp);
// 模拟初始化布隆过滤器
for (int i = 0; i < expectedInsertions; i++) {
bloomFilter.put("key" + i);
}
}
public boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
}
在业务代码中使用布隆过滤器:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CacheService {
@Autowired
private RedisService redisService;
@Autowired
private BloomFilterService bloomFilterService;
public Object getData(String key) {
if (!bloomFilterService.mightContain(key)) {
return null;
}
Object value = redisService.get(key);
if (value == null) {
// 从数据库中查询数据
value = queryFromDatabase(key);
if (value != null) {
redisService.set(key, value);
}
}
return value;
}
private Object queryFromDatabase(String key) {
// 模拟从数据库中查询数据
return null;
}
}
3.2.2 空值缓存
当从数据库中查询到的数据为空时,也将空值缓存到 Redis 中,并设置一个较短的过期时间。这样下次相同的请求就可以直接从缓存中获取空值,避免了再次访问数据库。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class CacheServiceWithNullCache {
@Autowired
private RedisService redisService;
public Object getData(String key) {
Object value = redisService.get(key);
if (value == null) {
// 从数据库中查询数据
value = queryFromDatabase(key);
if (value == null) {
// 缓存空值,设置较短的过期时间
redisService.set(key, null);
redisService.expire(key, 60); // 60 秒
} else {
redisService.set(key, value);
}
}
return value;
}
private Object queryFromDatabase(String key) {
// 模拟从数据库中查询数据
return null;
}
}
四、缓存雪崩问题及解决方案
4.1 缓存雪崩的定义和原因
缓存雪崩是指在某一时刻,大量的缓存同时过期,导致大量的请求直接访问数据库,从而使数据库压力过大,甚至可能导致数据库崩溃。常见的原因是缓存服务器宕机或大量缓存设置了相同的过期时间。
4.2 解决方案
4.2.1 随机过期时间
为每个缓存设置随机的过期时间,避免大量缓存同时过期。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Random;
@Service
public class CacheServiceWithRandomExpire {
@Autowired
private RedisService redisService;
public void setData(String key, Object value) {
redisService.set(key, value);
// 随机生成过期时间,范围在 1 到 10 分钟之间
Random random = new Random();
int expireTime = 60 + random.nextInt(540);
redisService.expire(key, expireTime);
}
public Object getData(String key) {
return redisService.get(key);
}
}
4.2.2 缓存预热
在系统启动时,将一些常用的数据提前加载到缓存中,并设置合理的过期时间。这样可以避免在系统运行初期出现大量的缓存穿透和雪崩问题。
4.2.3 分布式锁
使用分布式锁来控制对数据库的访问,当大量缓存过期时,只有获取到锁的请求才能访问数据库,其他请求需要等待。可以使用 Redis 的分布式锁来实现。
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 CacheServiceWithDistributedLock {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Object getData(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
// 获取分布式锁
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock:" + key, "lock", 10, TimeUnit.SECONDS);
if (lock != null && lock) {
try {
// 从数据库中查询数据
value = queryFromDatabase(key);
if (value != null) {
redisTemplate.opsForValue().set(key, value);
}
} finally {
// 释放锁
redisTemplate.delete("lock:" + key);
}
} else {
// 等待一段时间后重试
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
return getData(key);
}
}
return value;
}
private Object queryFromDatabase(String key) {
// 模拟从数据库中查询数据
return null;
}
}
五、缓存击穿问题及解决方案
5.1 缓存击穿的定义和原因
缓存击穿是指某个热点数据的缓存过期后,大量的请求同时访问该数据,导致这些请求直接访问数据库,从而使数据库压力过大。
5.2 解决方案
5.2.1 永不过期
对于热点数据,可以设置为永不过期,然后在后台使用定时任务来更新缓存。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
@Service
public class CacheServiceWithNeverExpire {
@Autowired
private RedisService redisService;
public Object getData(String key) {
return redisService.get(key);
}
@Scheduled(fixedRate = 3600000) // 每小时更新一次缓存
public void updateCache() {
// 更新热点数据的缓存
String key = "hotKey";
Object value = queryFromDatabase(key);
if (value != null) {
redisService.set(key, value);
}
}
private Object queryFromDatabase(String key) {
// 模拟从数据库中查询数据
return null;
}
}
5.2.2 分布式锁
与缓存雪崩的解决方案类似,使用分布式锁来控制对数据库的访问,当热点数据的缓存过期时,只有获取到锁的请求才能访问数据库,其他请求需要等待。
六、总结
通过本文的介绍,我们了解了 Redis 缓存穿透、雪崩、击穿问题的产生原因,并给出了相应的解决方案。在实际应用中,需要根据具体的业务场景选择合适的解决方案,以确保系统的稳定性和性能。同时,我们还结合 Spring Boot 进行了实战演示,展示了如何在 Spring Boot 项目中使用 Redis 缓存,并解决缓存相关的问题。