真实场景:首先给大家引入一个场景,假设咱们写的一套 Java 系统要跑一个几百行的大SQL,从MySQL里查询数据。这种几百行大SQL往往都是那种超级复杂的查询,可能涉及到了多表的关联,也有的是那种数据指标的查询,当然这种数据指标的查询其实是会常见一些,就是针对各种数据表关联起来查询和统计一些指标。
一般来说的话,遇到这种超级大SQL,往往会导致查询MySQL性能很差,一般跑个1s甚至好几秒那是很常见的。
所以,往往对于这种场景来说,如果想要优化一下这个查询的性能,我们一般会用缓存。
那么问题来了,这个缓存的结果是放哪里?可能很多兄弟说可以放 Redis啊!但是,一定要每次用缓存就立马上Redis吗?
毕竟 Redis 还得额外部署集群,一旦引入Redis,你还得考虑Redis是否会有故障,他的一些接入问题,以及跟 Redis 进行网络通信毕竟也是要耗时的。
所以说,其实咱们优先啊,可以先上本地缓存。也就是说,在业务系统运行的 JVM 的堆内存里,来缓存我们的查询结果,下次请求来了,就从本地缓存里取出来直接返回就可以了。
看
重
点
4大本地缓存优缺点?
1. ConcurrentHashMap自定义缓存:通过Map的底层方式,直接将需要缓存的对象放在内存中。优点是简单粗暴,不需要引入第三方包,比较适合一些比较简单的场景。缺点是没有缓存淘汰策略,定制化开发成本高。
2. Guava Cache:Guava是一个Google开源的项目,提供了一些Java工具类和库。Guava Cache是Guava提供的一个本地缓存框架,它使用LRU算法来管理缓存。优点是性能好,支持异步加载和批量操作。缺点是需要引入Guava库。
3. Ehcache:是一个轻量级的Java本地缓存库,它使用了基于时间戳的过期策略和可扩展性设计。优点是性能好,支持异步加载和批量操作。缺点是需要引入Ehcache库。
4. Caffeine:Caffeine是一个高性能的Java本地缓存库,一款基于 Guava Cache 的高性能缓存库,拥有 Guava Cache 的多数特性,并且具有更高的吞吐量和更低的延迟。缺点是需要引入Caffeine库。强烈推荐。
企业普适性方案:常用Ehcache、Caffeine,强烈推荐用Caffeine,看到最后关注,可免费获得文档+源码哟。
一
基于Map自定义缓存
通过ConcurrentHashMap的底层方式,直接将需要缓存的对象放在内存中。优点是简单粗暴,不需要引入第三方包,比较适合一些比较简单的场景。缺点是没有缓存淘汰策略,定制化开发成本高。
1.封装ConcurrentHashMap缓存通用工具类:
/**
*
* ConcurrentMap缓存通用工具类
*
* @author bmbfm
* @date 2023-7-31 19:29:18
*/
@Component
public class MapCache {
private static final Map<String, ConcurrentMap<String, Object>> cache = new ConcurrentHashMap<>();
/**
* 将指定键值对添加到指定缓存中
*
* @param cacheName 缓存名称
* @param key 缓存键
* @param value 缓存值
*/
public void put(String cacheName, String key, Object value) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName == null) {
cacheForName = new ConcurrentHashMap<>();
cache.put(cacheName, cacheForName);
}
cacheForName.put(key, value);
}
/**
* 从指定缓存中获取指定键的值
*
* @param cacheName 缓存名称
* @param key 缓存键
* @return 缓存值,如果缓存中不存在该键,则返回null
*/
public Object get(String cacheName, String key) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName == null) {
return null;
}
return cacheForName.get(key);
}
/**
* 从指定缓存中删除指定键值对
*
* @param cacheName 缓存名称
* @param key 缓存键
*/
public void remove(String cacheName, String key) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName != null) {
cacheForName.remove(key);
}
}
/**
* 检查指定缓存中是否包含指定键
*
* @param cacheName 缓存名称
* @param key 缓存键
* @return 如果缓存中包含该键,则返回true,否则返回false
*/
public boolean contains(String cacheName, String key) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName == null) {
return false;
}
return cacheForName.containsKey(key);
}
/**
* 清空指定缓存,移除所有键值对
*
* @param cacheName 缓存名称
*/
public void clear(String cacheName) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName != null) {
cacheForName.clear();
}
}
/**
* 获取指定缓存中的键值对数量
*
* @param cacheName 缓存名称
* @return 缓存中的键值对数量
*/
public int size(String cacheName) {
ConcurrentMap<String, Object> cacheForName = cache.get(cacheName);
if (cacheForName == null) {
return 0;
}
return cacheForName.size();
}
}
2.整合Spring Boot测试:
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CacheSpringBootApplication.class)
public class MapCacheTest {
@Resource
private MapCache mapCache;
@Test
public void test(){
// 添加缓存
mapCache.put("mapCacheName","DICT_TYPE","键值对");
// 获取缓存
mapCache.get("mapCacheName","DICT_TYPE");
// 检查指定缓存中是否包含指定键
mapCache.contains("mapCacheName","DICT_TYPE");
// 移除缓存
mapCache.remove("mapCacheName","DICT_TYPE");
// 清空缓存
mapCache.clear("mapCacheName");
}
}
二
基于Guava Cache
Guava是一个Google开源的项目,提供了一些Java工具类和库。Guava Cache是Guava提供的一个本地缓存框架,它使用LRU算法来管理缓存。优点是性能好,支持异步加载和批量操作。缺点是需要引入Guava库。
2.1 引入依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
2.2 封装GuavaCache缓存通用工具类
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
/**
*
* Guava缓存通用工具类
*
* @author bmbfm
* @date 2023-7-31 19:29:18
*/
@Component
public class GuavaCache {
private static final String CACHE_key = "cache_key";
private static final Cache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(100)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
/**
* 将一个键值对添加到缓存中。
*
* @param key 缓存的键
* @param value 要添加的值
*/
public static void put(String key, String value) {
cache.put(key, value);
}
/**
* 从缓存中获取指定键的值。
*
* @param key 要获取值的键
* @return 给定键的值,如果缓存中不存在该键,则返回null
*/
public static String get(String key) {
return cache.getIfPresent(key);
}
/**
* 从缓存中删除指定键的值。
*
* @param key 要删除的键
*/
public static void remove(String key) {
cache.invalidate(key);
}
/**
* 清空缓存中的所有键值对。
*/
public static void clear() {
cache.invalidateAll();
}
/**
* 返回缓存中的键值对数量。
*
* @return 键值对数量
*/
public static long size() {
return cache.size();
}
}
2.3 整合Spring Boot测试
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CacheSpringBootApplication.class)
public class GuavaCacheTest {
@Test
public void test(){
// 添加缓存
GuavaCache.put("key1", "value1");
String value1 = GuavaCache.get("key1");
System.out.println("value1: " + value1);
// 移除缓存
GuavaCache.remove("key1");
String value2 = GuavaCache.get("key1");
System.out.println("value2: " + value2); // 输出 null
// 清除缓存
GuavaCache.clear();
String value3 = GuavaCache.get("key1");
System.out.println("value3: " + value3); // 输出 null
}
}
三
Ehcache开源缓存框架
是一个轻量级的Java本地缓存库,它使用了基于时间戳的过期策略和可扩展性设计。优点是性能好,支持异步加载和批量操作。缺点是需要引入Ehcache库。
3.1 Ehcache本地缓存介绍
Ehcache是纯java 的开源缓存框架,具有快速、精干等特点,是 Hibernate 中默认的 CacheProvider。它主要面向通用缓存、JavaEE 和轻量级容器,具有内存和磁盘存储、缓存加载器、缓存扩展、缓存异常处理程序。 Ehcache 被广泛用于在 Hibernate、Spring、Cocoon 等其他开源系统。
3.1.1 Ehcache的主要特性
1. 快速;
2. 简单;
3. 多种缓存策略;
4. 缓存数据有两级:内存和磁盘,因此无需担心容量问题;
5. 缓存数据会在虚拟机重启的过程中写入磁盘;
6. 可以通过 RMI、可插入 API 等方式进行分布式缓存;
7. 具有缓存和缓存管理器的侦听接口;
8. 支持多缓存管理器实例,以及一个实例的多个缓存区域;
9. 提供 Hibernate 的缓存实现。
3.1.2 Ehcache使用介绍
Ehcache 是用来管理缓存的一个工具,其缓存的数据可以是存放在内存里面的,也可以是存放在硬盘上的。其核心是 CacheManager,一切 Ehcache 的应用都是从 CacheManager 开始的。它是用来管理 Cache(缓存)的,一个应用可以有多个 CacheManager,而一个 CacheManager 下又可以有多个 Cache。Cache 内部保存的是一个个的 Element,而一个 Element 中保存的是一个 key 和 value 的配对,相当于 Map 里面的一个 Entry。
3.1.3 Ehcache缓存过期策略
当缓存需要被清理时(比如空间占用已经接近临界值了),需要使用某种淘汰算法来决定清理掉哪些数据。常用的淘汰算法有下面几种:
● FIFO:First In First Out,先进先出。判断被存储的时间,离目前最远的数据优先被淘汰。
● LRU:Least Recently Used,最近最少使用。判断最近被使用的时间,目前最远的数据优先被淘汰。
● LFU:Least Frequently Used,最不经常使用。在一段时间内,数据被使用次数最少的,优先被淘汰。
3.2 SpringBoot整合Ehcache
3.2.1 Maven环境依赖
<!-- 开启 cache 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--ehcache缓存框架-->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.2</version>
</dependency>
3.2.2 application.yml配置信息
spring:
# 缓存配置读取
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
PS:也可以将ehcache.xml直接放在resources目录下
3.2.3 启动项添加注解
添加@EnableCaching开启 ehcache 缓存模式。
3.2.4 EhCache配置resources下面创建 ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd">
<!-- 磁盘缓存位置 -->
<diskStore path="java.io.tmpdir"/>
<cache name="dict"
maxElementsInMemory="10000"
eternal="false"
overflowToDisk="true"
maxElementsOnDisk="10000000"
timeToIdleSeconds="10"
timeToLiveSeconds="20"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LFU">
</cache>
<!-- 默认缓存 -->
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="3600"
timeToLiveSeconds="3600"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU">
<persistence strategy="localTempSwap"/>
</defaultCache>
<!-- 字典元素缓存
LRU 最近最少使用:当缓存紧张时,我们要存储新的数据进来就必定要删除一些老数据
FIFO 先进先出
LFU 最少使用
-->
<cache name="dictionary"
maxElementsInMemory="10000"
eternal="true"
overflowToDisk="true"
maxElementsOnDisk="10000000"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LFU">
</cache>
</ehcache>
3.2.5 ehcache.xml相关参数解释
1. diskStore:指定数据(.data and .index)存储位置,可指定磁盘中的文件夹位置期
2. defaultCache: 默认的管理策略
Ehcache 使用 Map 集合实现的 element 其实就是 key 和 value。
一、以下属性是必须的:
● name: Cache的名称,必须是唯一的(ehcache会把这个cache放到HashMap里)。
● maxElementsInMemory:在内存中缓存的element的最大数目。
● maxElementsOnDisk:在磁盘上缓存的element的最大数目,默认值为0,表示不限制。
● eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
● overflowToDisk: 如果内存中数据超过内存限制,是否要缓存到磁盘上。
二、以下属性是可选的:
● timeToIdleSeconds: 对象空闲时间,指对象在多长时间没有被访问就会失效。只对eternal为false的有效。默认值0,表示一直可以访问。
● timeToLiveSeconds: 对象存活时间,指对象从创建到失效所需要的时间。只对eternal为false的有效。默认值0,表示一直可以访问。
● diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。
● diskExpiryThreadIntervalSeconds: 对象检测线程运行时间间隔。标识对象状态的线程多长时间运行一次。
● diskSpoolBufferSizeMB: DiskStore使用的磁盘大小,默认值30MB。每个cache使用各自的DiskStore。
● memoryStoreEvictionPolicy: 如果内存中数据超过内存限制,向磁盘缓存时的策略。默认值LRU,可选FIFO、LFU。
3.2.6 SpringBoot核心EhCacheManagerConfig.java配置
@Configuration
@EnableCaching
public class EhCacheManagerConfig {
@Bean
public CacheManager ehCacheManager(){
CacheManager cacheManager = CacheManager.create();
return cacheManager;
}
}
3.2.7 整合Spring Boot测试
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CacheSpringBootApplication.class)
public class EhCacheTest {
@Resource
private CacheManager ehCacheManager;
@Test
public void test(){
// 获取缓存名称
Cache cache = ehCacheManager.getCache("dictionary");
// 添加缓存
cache.put(new Element("APPROVE_STATUS", JSONUtil.toJsonStr("1:待审核,2:已审核,3:已退回")));
// 获取缓存
System.out.println(cache.get("APPROVE_STATUS").getObjectValue());
// 移除缓存
cache.remove("APPROVE_STATUS");
}
@Test
public void test02(){
// 要演示注解方式,单独启动ehcache下的启动类,通过controller接口访问测试
}
}
3.2.8 整合Spring Boot使用缓存注解测试
@Service
@Slf4j
@CacheConfig(cacheNames = "dict")
public class BaseServiceImpl implements BaseService {
// 获取缓存添加这个注解
@Cacheable(key = "#id")
@Override
public SysDict findDictById(Integer id){
log.info("---第一次查询,不走缓存---");
SysDict sysDict = new SysDict();
sysDict.setId(1);
sysDict.setTitle("审批状态");
sysDict.setIdentification("APPROVE_STATUS");
sysDict.setType(1);
sysDict.setValue("1:待审核,2:已审核,3:已退回");
return sysDict;
}
// 更新缓存添加这个注解
@CachePut(key = "#sysDict.id")
@Override
public SysDict updateSysDict(SysDict sysDict){
log.info("---更新缓存---");
SysDict dict = new SysDict();
Integer id = sysDict.getId();
dict.setId(id);
dict.setTitle("日志类型");
dict.setIdentification("LOG_TYPE");
dict.setType(1);
dict.setValue("1:业务,2:登录,3:系统");
return sysDict;
}
@CacheEvict(key = "#id")
@Override
public void deleteSysDictById(Integer id) {
}
}
四
Caffeine本地缓存介绍
Caffeine是基于 Java 8 的高性能,接近最佳的缓存库。
Caffeine使用 Google Guava 启发的 API 提供内存缓存。 改进取决于您设计 Guava 缓存和 ConcurrentLinkedHashMap 的体验。
4.1.1 Caffeine的主要特性
l自动将条目自动加载到缓存中,可以选择异步加载
l基于频率和新近度超过最大值时基于大小的逐出
l自上次访问或上次写入以来测得的基于时间的条目到期
l发生第一个陈旧的条目请求时,异步刷新
l键自动包装在弱引用中
l值自动包装在弱引用或软引用中
l逐出(或以其他方式删除)条目的通知
l写入传播到外部资源
l缓存访问统计信息的累积
4.2 SpringBoot整合Caffeine商用级别代码
4.2.1 Maven环境依赖
<!-- Caffeine是基于java8实现的新一代缓存工具 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>2.8.2</version>
</dependency>
<!-- Caffeine需要SpringBoot内置上下文版本支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
4.2.2 application.yml配置信息
#caffeine 本地缓存配置
caffeine:
cacheNames: dictInfo
initCapacity: 100
maxSize: 1000
expireAfterAccess: 600
4.2.3 CaffeineConfig.java本地缓存配置
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
* Caffeine本地缓存配置
*
* @author bmbfm
* @date 2023-7-31 21:26:09
*/
@Data
@Component
@ConfigurationProperties(prefix = "caffeine", ignoreInvalidFields = true)
public class CaffeineConfig {
/**
* cache name配置
*/
private String cacheNames;
/**
* 初始的缓存空间大小
*/
private String initCapacity;
/**
* 缓存最大的条数
*/
private String maxSize;
/**
* 最后一次写入或访问后经过固定的时间
*/
private String expireAfterAccess;
/**
* 创建缓存或最新一次更新缓存后经过固定的时间间隔,刷新缓存
*/
private String refreshAfterWrite;
}
4.2.4 CaffeineManagerConfig.java本地缓存Caffeine manager配置
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.google.common.collect.Lists;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
/**
* Caffeine manager配置
*
* @author bmbfm
* @date 2023-7-31 21:26:29
*/
@Component
public class CaffeineManagerConfig {
@Resource
private CaffeineConfig caffeineConfig;
@Bean(name = "caffeine")
public CacheManager initCacheManager(){
CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager();
Caffeine caffeine = Caffeine.newBuilder().initialCapacity(
Convert.toInt(caffeineConfig.getInitCapacity(), 100))
.maximumSize(Convert.toInt(caffeineConfig.getMaxSize(), 1000))
.expireAfterAccess(Convert.toInt(caffeineConfig.getExpireAfterAccess(), 1000),
TimeUnit.SECONDS);
caffeineCacheManager.setCaffeine(caffeine);
caffeineCacheManager.setCacheNames(StrUtil.isEmpty(caffeineConfig.getCacheNames()) ?
Lists.newArrayList("commCache") : Arrays.asList(caffeineConfig.getCacheNames().split(";")));
caffeineCacheManager.setAllowNullValues(false);
return caffeineCacheManager;
}
@Bean
public CacheLoader<Object, Object> cacheLoader(){
return new CacheLoader<Object, Object>() {
@Override
public Object load(Object o) throws Exception {
return null;
}
public Object reload(Object key, Object oldValue){
// 重写这个方法将oldValue值返回,然后刷新缓存
return oldValue;
}
};
}
}
4.2.5 Caffeine本地缓存操作工具类
import com.bmbfm.cache.caffeine.config.CaffeineConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.annotation.Order;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
*
* caffeine本地缓存操作工具类
*
* @author bmbfm
* @date 2023-7-31 21:33:26
*/
@Slf4j
@Configuration
@Order(2)
@Import(CaffeineConfig.class)
public class CaffeineCacheUtil {
@Resource
private CacheManager caffeineCacheManager;
/**
* 获取缓存
*/
public Object getCache(String cacheName, Object key){
Cache cache = caffeineCacheManager.getCache(cacheName);
if(null == cache){
return null;
}
Cache.ValueWrapper valueWrapper = cache.get(key);
if(null == valueWrapper){
return null;
}
return valueWrapper.get();
}
/**
* 添加缓存
*/
public void putCache(String cacheName, Object key, Object value) {
Cache cache = caffeineCacheManager.getCache(cacheName);
if(null == cache){
String errMsg = String.format("缓存cacheName未配置,请检查!cacheName:%s", cacheName);
log.error(errMsg);
return;
}
CaffeineCache caffeineCache = (CaffeineCache)cache;
caffeineCache.put(key, value);
}
/**
* 清空缓存
*/
public void clearCache(String cacheName){
Cache cache = caffeineCacheManager.getCache(cacheName);
if(cache != null){
cache.clear();
}
}
/**
* 失效缓存
*/
public void evictCache(String cacheName, Object key){
Cache cache = caffeineCacheManager.getCache(cacheName);
if(null == cache){
String errMsg = String.format("缓存cacheName未配置,请检查!cacheName:%s", cacheName);
log.error(errMsg);
return;
}
cache.evict(key);
}
/**
* 获取所有的缓存Name
*/
public List<String> cacheNames(){
return new ArrayList<>(caffeineCacheManager.getCacheNames());
}
}
4.3 SpringBoot整合Caffeine测试
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.bmbfm.cache.CacheSpringBootApplication;
import com.bmbfm.cache.caffeine.util.CaffeineCacheUtil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = CacheSpringBootApplication.class)
public class CaffeineCacheTest {
@Resource
private CaffeineCacheUtil caffeineCacheUtil;
@Test
public void test(){
// 添加缓存
caffeineCacheUtil.putCache("DICT_INFO","APPROVE_STATUS", JSONUtil.toJsonStr("1:待审核,2:已审核,3:已退回"));
// 缓存失效
caffeineCacheUtil.evictCache("DICT_INFO","APPROVE_STATUS");
// 获取缓存
Object localActJson = caffeineCacheUtil.getCache("DICT_INFO","APPROVE_STATUS");
Map<String, String> value = new HashMap<>();
if(ObjectUtil.isNotEmpty(localActJson)){
String dictValue = JSONUtil.toJsonStr(localActJson);
String[] outerSplit = dictValue.split(",");
value = new LinkedHashMap<>();
for (String osp : outerSplit) {
String[] split = osp.split(":");
if(split.length > 1){
value.put(split[0], split[1]);
}
}
}
// 清空缓存
caffeineCacheUtil.clearCache("DICT_INFO");
}
}
五
扩展本地缓存预热
那么下一个问题又来了,很多查询他可能当天第一次查的时候,本地缓存里是没有的,还是得去 MySQL 里花费几秒钟来查询,查完了以后才能放入到本地缓存里去,那这样岂不是每天都有一些人第一次查询很慢很慢吗?
有没有更好的办法呢?当然有了,那就是缓存预热,我们的业务系统可以把每天的查询请求和参数都记录下来。
对于一些数据报表的复杂查询,其实每天的查询条件都是差不多的,只不过是当天的日期会有变化而已,另外就是对于一些数据报表的数据,往往是通过大数据平台进行离线计算的。
啥叫做离线计算呢?就是说可能有一个大数据系统每天凌晨的时候会把昨天的数据算一遍,算好的数据结果写入到 MySQL 里去,然后每天更新数据就这一次,接着当天就不更新数据了。
然后呢,用户每天都会对我们的系统发起很多次复杂报表查询语句,但是这个 SQL 多表关联的一些逻辑,以及附加的一些查询条件几乎都是有规律的是差不多的,就是每天选择的当天日期是不太一样的。
所以此时我们就可以把这些查询条件记录下来,然后每天凌晨的时候,趁着大家都睡觉了,就根据经常查询的条件和当天日期,提前去查询数据,查询结果提前写入本地缓存。