手机网站开发公司电话,建设网站可以搜索电影,蜂鸟影院高清免费观看,我的小程序在哪里找缓存击穿#xff08;Cache Breakdown#xff09; 是在高并发场景下#xff0c;当某个热点数据在缓存中失效或不存在时#xff0c;瞬间大量请求同时击中数据库#xff0c;导致数据库压力骤增甚至崩溃的现象。为了解决这一问题#xff0c;“永不过期” “逻辑过期” 的策略…缓存击穿Cache Breakdown 是在高并发场景下当某个热点数据在缓存中失效或不存在时瞬间大量请求同时击中数据库导致数据库压力骤增甚至崩溃的现象。为了解决这一问题“永不过期” “逻辑过期” 的策略是一种有效的解决方案。这种方法通过将缓存数据设为永不过期同时在数据内部维护一个逻辑过期时间从而控制何时更新缓存避免大量请求直接访问数据库。
本文将详细介绍这一解决方案并提供完整的 Java 实现示例使用 Redis 作为缓存存储。
一、“永不过期” “逻辑过期” 策略概述
1. 永不过期
将缓存数据设置为永不过期即不依赖 Redis 的 TTL这样缓存项本身不会因时间原因自动失效。所有的过期逻辑由应用程序内部控制。
2. 逻辑过期
每个缓存数据项内部包含一个逻辑过期时间如时间戳。当应用程序读取数据时会检查当前时间与逻辑过期时间的关系
未过期直接返回缓存数据。已过期 触发后台线程或异步任务刷新缓存数据。立即返回旧的缓存数据保持应用响应性。
通过这种方式可以避免大量请求同时刷新缓存减轻数据库压力同时确保数据在逻辑上是最新的。
二、实现步骤
定义缓存数据结构将数据与逻辑过期时间一起存储在 Redis 中。读取数据时检查逻辑过期时间 如果未过期直接返回数据。如果已过期异步刷新缓存并返回旧数据。 刷新缓存数据 仅允许一个线程进行数据刷新避免多线程同时刷新。更新 Redis 中的数据及其逻辑过期时间。
三、Java 实现示例
以下是一个基于 Java 和 Redis 的完整实现示例。我们将使用 Redisson 作为 Redis 客户端它支持分布式锁和异步操作适合实现“永不过期” “逻辑过期” 策略。
1. 引入依赖
首先在项目的 pom.xml 中添加 Redisson 依赖
dependencies!-- Redisson --dependencygroupIdorg.redisson/groupIdartifactIdredisson/artifactIdversion3.23.6/version/dependency!-- JSON 处理如使用 Jackson --dependencygroupIdcom.fasterxml.jackson.core/groupIdartifactIdjackson-databind/artifactIdversion2.15.0/version/dependency
/dependencies2. 定义缓存数据结构
我们需要一个数据结构来存储实际数据和逻辑过期时间。以下是一个示例类
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;public class CacheDataT {JsonProperty(data)private T data;JsonProperty(expiryTime)private long expiryTime; // 逻辑过期时间单位毫秒public CacheData() {}public CacheData(T data, long expiryTime) {this.data data;this.expiryTime expiryTime;}public T getData() {return data;}public void setData(T data) {this.data data;}public long getExpiryTime() {return expiryTime;}public void setExpiryTime(long expiryTime) {this.expiryTime expiryTime;}JsonIgnorepublic boolean isExpired() {return System.currentTimeMillis() expiryTime;}
}3. Redis 配置与初始化
配置 Redisson 客户端以连接 Redis
import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;public class RedisConfig {private static RedissonClient redissonClient;static {Config config new Config();// 配置单机模式config.useSingleServer().setAddress(redis://127.0.0.1:6379).setConnectionTimeout(10000).setRetryAttempts(3).setRetryInterval(1500);redissonClient Redisson.create(config);}public static RedissonClient getRedissonClient() {return redissonClient;}
}4. 缓存管理器实现
实现缓存读取、逻辑过期检查和异步刷新
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;import java.io.IOException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;public class CacheManager {private RedissonClient redissonClient;private ObjectMapper objectMapper;private ExecutorService executorService;// 缓存逻辑过期时间单位毫秒private final long LOGICAL_EXPIRY 5 * 60 * 1000; // 5分钟public CacheManager() {this.redissonClient RedisConfig.getRedissonClient();this.objectMapper new ObjectMapper();// 创建固定线程池用于异步刷新this.executorService Executors.newFixedThreadPool(10);}/*** 获取缓存数据** param key Redis 键* param dbQueryFunc 查询数据库的函数* param T 数据类型* return 缓存数据或旧数据*/public T T getCacheData(String key, DBQueryFuncT dbQueryFunc) {try {String json redissonClient.getBucket(key).get().toString();if (json ! null) {// 反序列化CacheDataT cacheData objectMapper.readValue(json, CacheData.class);if (!cacheData.isExpired()) {// 未过期返回数据return cacheData.getData();} else {// 已过期异步刷新refreshCacheAsync(key, dbQueryFunc);// 返回旧数据return cacheData.getData();}} else {// 缓存不存在尝试刷新refreshCacheAsync(key, dbQueryFunc);// 返回 null 或者可以选择同步查询数据库return null;}} catch (IOException e) {e.printStackTrace();return null;}}/*** 异步刷新缓存** param key Redis 键* param dbQueryFunc 查询数据库的函数* param T 数据类型*/private T void refreshCacheAsync(String key, DBQueryFuncT dbQueryFunc) {executorService.submit(() - {RLock lock redissonClient.getLock(lock: key);boolean isLockAcquired false;try {// 尝试获取锁防止多线程同时刷新isLockAcquired lock.tryLock(500, 3000, TimeUnit.MILLISECONDS);if (isLockAcquired) {// 再次检查缓存是否过期防止被其他线程刷新String json redissonClient.getBucket(key).get().toString();CacheDataT cacheData objectMapper.readValue(json, CacheData.class);if (cacheData.isExpired()) {// 查询数据库T data dbQueryFunc.query();// 更新缓存CacheDataT newCacheData new CacheData(data, System.currentTimeMillis() LOGICAL_EXPIRY);String newJson objectMapper.writeValueAsString(newCacheData);redissonClient.getBucket(key).set(newJson);}}} catch (InterruptedException | IOException e) {e.printStackTrace();} finally {if (isLockAcquired lock.isHeldByCurrentThread()) {lock.unlock();}}});}/*** 刷新缓存数据同步调用用于缓存不存在时** param key Redis 键* param dbQueryFunc 查询数据库的函数* param T 数据类型*/public T T refreshCache(String key, DBQueryFuncT dbQueryFunc) {RLock lock redissonClient.getLock(lock: key);boolean isLockAcquired false;try {// 获取锁等待最多 500 毫秒isLockAcquired lock.tryLock(500, 3000, TimeUnit.MILLISECONDS);if (isLockAcquired) {// 查询数据库T data dbQueryFunc.query();// 更新缓存CacheDataT newCacheData new CacheData(data, System.currentTimeMillis() LOGICAL_EXPIRY);String newJson objectMapper.writeValueAsString(newCacheData);redissonClient.getBucket(key).set(newJson);return data;} else {// 获取锁失败可能由其他线程刷新等待一段时间后尝试获取Thread.sleep(100);String json redissonClient.getBucket(key).get().toString();if (json ! null) {CacheDataT cacheData objectMapper.readValue(json, CacheData.class);return cacheData.getData();} else {// 最终未获取到数据返回 null 或选择其他处理方式return null;}}} catch (InterruptedException | IOException e) {e.printStackTrace();return null;} finally {if (isLockAcquired lock.isHeldByCurrentThread()) {lock.unlock();}}}/*** 关闭缓存管理器释放资源*/public void shutdown() {executorService.shutdown();redissonClient.shutdown();}/*** 数据库查询函数接口** param T 数据类型*/public interface DBQueryFuncT {T query();}
}5. 使用示例
假设我们有一个 User 数据模型并希望缓存用户信息
public class User {private String id;private String name;private int age;// 构造方法、getter、setter等public User() {}public User(String id, String name, int age) {this.id id;this.name name;this.age age;}// Getters and Setters// ...
}模拟数据库查询方法
public class UserService {/*** 模拟数据库查询** param userId 用户 ID* return 用户信息*/public User getUserFromDB(String userId) {// 模拟数据库延迟try {Thread.sleep(100); // 100ms 延迟} catch (InterruptedException e) {e.printStackTrace();}// 返回模拟数据return new User(userId, User_ userId, 25);}
}主程序示例
public class Main {public static void main(String[] args) {CacheManager cacheManager new CacheManager();UserService userService new UserService();String userId 12345;String cacheKey user: userId;// 定义数据库查询函数CacheManager.DBQueryFuncUser dbQueryFunc () - userService.getUserFromDB(userId);// 第一次访问缓存可能不存在或已过期User user cacheManager.getCacheData(cacheKey, dbQueryFunc);if (user null) {// 缓存不存在进行同步刷新user cacheManager.refreshCache(cacheKey, dbQueryFunc);}System.out.println(User: user.getName() , Age: user.getAge());// 之后的访问如果缓存未过期直接返回缓存数据User cachedUser cacheManager.getCacheData(cacheKey, dbQueryFunc);System.out.println(Cached User: cachedUser.getName() , Age: cachedUser.getAge());// 关闭缓存管理器cacheManager.shutdown();}
}6. 运行流程说明 首次访问 调用 getCacheData 方法。缓存可能不存在或已逻辑过期。触发异步刷新缓存通过 refreshCacheAsync 方法。如果缓存不存在调用 refreshCache 方法进行同步刷新。从数据库获取数据并更新缓存。返回获取到的数据。 后续访问 调用 getCacheData 方法。检查逻辑过期时间。如果未过期直接返回缓存数据。如果已过期触发异步刷新缓存同时返回旧数据保持高响应性。
7. 优点与注意事项
优点
防止缓存击穿通过锁机制和异步刷新避免高并发下大量请求同时触发数据库访问。高响应性即使缓存已逻辑过期也能立即返回旧数据不会造成请求阻塞。灵活性逻辑过期时间可根据业务需求动态调整。
注意事项
数据一致性旧数据可能与数据库中的最新数据存在一定的时间差需要根据业务需求权衡。锁的可靠性确保分布式锁机制的可靠性避免死锁或锁丢失。线程池管理合理配置线程池大小避免过多异步任务导致资源竞争。异常处理完善异常处理机制确保在数据刷新失败时系统稳定。
四、扩展与优化
1. 使用 Redis Lua 脚本优化原子性
为了进一步确保操作的原子性可以考虑使用 Redis 的 Lua 脚本将读取和写入操作合并为一个原子操作。
2. 引入消息队列进行异步刷新
对于大规模分布式系统可以引入消息队列如 Kafka、RabbitMQ来异步处理缓存刷新任务提升系统的可扩展性和可靠性。
3. 监控与报警
建立完善的监控机制实时监控缓存命中率、数据库访问量、缓存刷新失败次数等指标及时发现并处理异常情况。
五、总结
通过 “永不过期” “逻辑过期” 的策略可以有效防止缓存击穿问题确保系统在高并发下的稳定性和高可用性。本文详细介绍了该策略的原理及其 Java 实现包括数据结构设计、缓存读取与逻辑过期检查、异步刷新机制等关键环节。根据实际业务需求开发者可以进一步优化和扩展这一策略以构建高性能、高可靠性的分布式系统。