文章目录
1. 缓存基础概念
1.1 什么是缓存
缓存是一种高速存储技术,用于临时存储频繁访问的数据,以提高系统性能和响应速度。在软件架构中,缓存通常位于应用程序和数据库之间,作为数据的快速访问层。
1.2 缓存的作用
- 提高响应速度:从内存中读取数据比从磁盘快几个数量级
- 减少数据库压力:减少对数据库的直接访问
- 提升用户体验:快速响应用户请求
- 节约成本:减少服务器资源消耗
1.3 常见的缓存类型
- 本地缓存:如HashMap、Guava Cache
- 分布式缓存:如Redis、Memcached
- 数据库缓存:如MySQL查询缓存
- CDN缓存:内容分发网络缓存
1.4 缓存架构示例
// 典型的缓存使用模式
public class UserService {
private RedisTemplate<String, Object> redisTemplate;
private UserRepository userRepository;
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 先查缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user; // 缓存命中
}
// 2. 缓存未命中,查数据库
user = userRepository.findById(userId);
if (user != null) {
// 3. 将数据写入缓存
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
return user;
}
}
2. 缓存雪崩 (Cache Avalanche)
2.1 什么是缓存雪崩
缓存雪崩是指在同一时间,大量的缓存key同时失效,导致大量请求直接打到数据库上,造成数据库瞬间压力过大甚至宕机的现象。
2.2 缓存雪崩的原因
- 缓存服务器宕机:Redis服务器突然宕机
- 大量key同时过期:设置了相同的过期时间
- 缓存预热不充分:系统重启后缓存为空
2.3 缓存雪崩的危害
- 数据库瞬间压力暴增
- 系统响应时间急剧增加
- 可能导致数据库连接池耗尽
- 严重时可能导致整个系统崩溃
2.4 缓存雪崩的解决方案
方案1:设置随机过期时间
@Service
public class ProductService {
private RedisTemplate<String, Object> redisTemplate;
private ProductRepository productRepository;
public Product getProductById(Long productId) {
String key = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
product = productRepository.findById(productId);
if (product != null) {
// 设置随机过期时间:30分钟 + 0-10分钟的随机时间
int randomMinutes = new Random().nextInt(10);
Duration expireTime = Duration.ofMinutes(30 + randomMinutes);
redisTemplate.opsForValue().set(key, product, expireTime);
}
}
return product;
}
}
方案2:缓存集群和主从复制
# Redis集群配置示例
spring:
redis:
cluster:
nodes:
- 192.168.1.100:7001
- 192.168.1.100:7002
- 192.168.1.100:7003
- 192.168.1.101:7001
- 192.168.1.101:7002
- 192.168.1.101:7003
max-redirects: 3
timeout: 3000ms
lettuce:
pool:
max-active: 16
max-idle: 8
min-idle: 0
方案3:熔断降级机制
@Component
public class ProductServiceWithCircuitBreaker {
private RedisTemplate<String, Object> redisTemplate;
private ProductRepository productRepository;
private CircuitBreaker circuitBreaker;
public ProductServiceWithCircuitBreaker() {
// 配置熔断器
this.circuitBreaker = CircuitBreaker.ofDefaults("productService");
circuitBreaker.getEventPublisher().onStateTransition(event ->
System.out.println("CircuitBreaker state transition: " + event));
}
public Product getProductById(Long productId) {
return circuitBreaker.executeSupplier(() -> {
String key = "product:" + productId;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product == null) {
product = productRepository.findById(productId);
if (product != null) {
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
}
}
return product;
});
}
}
方案4:本地缓存兜底
@Service
public class ProductServiceWithLocalCache {
private RedisTemplate<String, Object> redisTemplate;
private ProductRepository productRepository;
private Cache<String, Product> localCache;
public ProductServiceWithLocalCache() {
// 创建本地缓存
this.localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(5, TimeUnit.MINUTES)
.build();
}
public Product getProductById(Long productId) {
String key = "product:" + productId;
try {
// 1. 先查Redis缓存
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
// 同时更新本地缓存
localCache.put(key, product);
return product;
}
} catch (Exception e) {
// Redis异常时,查询本地缓存
Product localProduct = localCache.getIfPresent(key);
if (localProduct != null) {
return localProduct;
}
}
// 2. 查询数据库
Product product = productRepository.findById(productId);
if (product != null) {
try {
redisTemplate.opsForValue().set(key, product, Duration.ofMinutes(30));
} catch (Exception e) {
// Redis写入失败,只更新本地缓存
localCache.put(key, product);
}
}
return product;
}
}
2.5 缓存雪崩预防最佳实践
@Configuration
public class CacheConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// 设置序列化方式
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 默认30分钟过期
.disableCachingNullValues();
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config)
.build();
}
}
3. 缓存穿透 (Cache Penetration)
3.1 什么是缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时才查询数据库,而且不存在的数据不会写入缓存,导致这个不存在的数据每次请求都要查询数据库,给数据库造成压力。
3.2 缓存穿透的场景示例
// 问题代码示例
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 查缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 2. 查数据库
user = userRepository.findById(userId);
if (user != null) {
// 3. 只有数据存在才缓存
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
// 如果user为null,不缓存,下次还会查数据库
return user;
}
3.3 缓存穿透的危害
- 大量无效请求穿透到数据库
- 数据库查询压力增大
- 系统整体性能下降
- 可能被恶意攻击利用
3.4 缓存穿透的解决方案
方案1:缓存空值
@Service
public class UserServiceWithNullCache {
private RedisTemplate<String, Object> redisTemplate;
private UserRepository userRepository;
public User getUserById(Long userId) {
String key = "user:" + userId;
// 1. 查缓存
Object cached = redisTemplate.opsForValue().get(key);
if (cached != null) {
// 如果是特殊标记,说明数据不存在
if ("NULL".equals(cached)) {
return null;
}
return (User) cached;
}
// 2. 查数据库
User user = userRepository.findById(userId);
if (user != null) {
// 3. 缓存有效数据
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
} else {
// 4. 缓存空值,但设置较短的过期时间
redisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
}
return user;
}
}
方案2:布隆过滤器
@Service
public class UserServiceWithBloomFilter {
private RedisTemplate<String, Object> redisTemplate;
private UserRepository userRepository;
private BloomFilter<Long> bloomFilter;
@PostConstruct
public void initBloomFilter() {
// 创建布隆过滤器,预计100万个元素,误判率0.01%
bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.0001);
// 将所有用户ID加入布隆过滤器
List<Long> userIds = userRepository.findAllUserIds();
userIds.forEach(bloomFilter::put);
}
public User getUserById(Long userId) {
// 1. 先用布隆过滤器判断
if (!bloomFilter.mightContain(userId)) {
// 布隆过滤器说不存在,一定不存在
return null;
}
String key = "user:" + userId;
// 2. 查缓存
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 3. 查数据库
user = userRepository.findById(userId);
if (user != null) {
redisTemplate.opsForValue().set(key, user, Duration.ofMinutes(30));
}
return user;
}
}
方案3:参数校验
@RestController
public class UserController {
private UserService userService;
@GetMapping("/user/{userId}")
public ResponseEntity<User> getUser(@PathVariable Long userId) {
// 1. 参数校验
if (userId == null || userId <= 0) {
return ResponseEntity.badRequest().build();
}
// 2. 业务范围校验
if (userId > 999999999L) {
// 假设用户ID不会超过这个值
return ResponseEntity.notFound().build();
}
User user