大家好,我是哪吒。
在上一篇文章中提到,有一个页面加载速度很慢,是通过缓冲流优化的。
查询的时候,会访问后台数据库,查询前20条数据,按道理来说,这应该很快才对。
追踪代码,看看啥问题,最后发现问题有三:
大字段批量查询、批量文件落地、读取大文件并进行网络传输,不慢才怪,这一顿骚操作,5秒能加载完毕,已经烧高香了。
有一个小伙伴在评论中提到,还可以通过缓存继续优化,确实是可以的,缓存也是复用优化的一种。
为了提高页面的加载速度,使用了单条查询 + 触发索引,提高数据库查询速度。
归根结底,还是查询了数据库,如果不查呢,访问速度肯定会更快。
这个时候,就用到了缓存,将运费模板存到缓存中。
缓存就是把访问量较高的热点数据从传统的关系型数据库中加载到内存中,当用户再次访问热点数据时,是从内存中加载,减少了对数据库的访问量,解决了高并发场景下容易造成数据库宕机的问题。
我理解的缓存的本质就是一个用空间换时间的一个思想。
提供“缓存”的目的是为了让数据访问的速度适应CPU的处理速度,其基于的原理是内存中“局部性原理”。
CPU 缓存的是内存数据,用于解决 CPU 处理速度和内存不匹配的问题,比如处理器和内存之间的高速缓存,操作系统在内存管理上,针对虚拟内存 为页表项使用了一特殊的高速缓存TLB转换检测缓冲区,因为每个虚拟内存访问会引起两次物理访问,一次取相关的页表项,一次取数据,TLB引入来加速虚拟地址到物理地址的转换。
本地缓存:在客户端本地的物理内存中划出一部分空间,来缓存客户端回写到服务器的数据。当本地回写缓存达到缓存阈值时,将数据写入到服务器中。
本地缓存是指程序级别的缓存组件,它的特点是本地缓存和应用程序会运行在同一个进程中,所以本地缓存的操作会非常快,因为在同一个进程内也意味着不会有网络上的延迟和开销。
本地缓存适用于单节点非集群的应用场景,它的优点是快,缺点是多程序无法共享缓存。
无法共享缓存可能会造成系统资源的浪费,每个系统都单独维护了一份属于自己的缓存,而同一份缓存有可能被多个系统单独进行存储,从而浪费了系统资源。
分布式缓存是指将应用系统和缓存组件进行分离的缓存机制,这样多个应用系统就可以共享一套缓存数据了,它的特点是共享缓存服务和可集群部署,为缓存系统提供了高可用的运行环境,以及缓存共享的程序运行机制。
下面介绍一个小编最常用的本地缓存 Guava Cache。
Google Guava是一个Java编程库,其中包含了许多高质量的工具类和方法。其中,Guava的缓存工具之一是LoadingCache。LoadingCache是一个带有自动加载功能的缓存,可以自动加载缓存中不存在的数据。其实质是一个键值对(Key-Value Pair)的缓存,可以使用键来获取相应的值。
Guava Cache 的架构设计灵感来源于 ConcurrentHashMap,它使用了多个 segments 方式的细粒度锁,在保证线程安全的同时,支持了高并发的使用场景。Guava Cache 类似于 Map 集合的方式对键值对进行操作,只不过多了过期淘汰等处理逻辑。
Guava Cache对比ConcurrentHashMap优势在哪?
缓存穿透是指在Loadingcache缓存中,由于某些原因,缓存的数据无法被正常访问或处理,导致缓存失去了它的作用。
发生缓存穿透的原因有很多,比如数据量过大、数据更新频繁、数据过期、数据权限限制、缓存性能瓶颈等原因,这里不过多纠结。
设置为expireAfterAcess和expireAfterWrite时,在进行get的过程中,缓存失效的话,会进行load操作,load是一个同步加载的操作,如下图:
如果发生了缓存穿透,当有大量并发请求访问缓存时,会有一个线程去同步查询DB,随即通过reeatrantLock进入loading等待状态,其它请求相同key的线程,一部分在waitforvalue,另一部分在reentantloack的阻塞队列中,等待同步查询完毕,所有请求都会获得最新值。
如果采用refresh的话,会通过scheduleRefresh方法进行load,也是一个线程同步获取DB。
其它线程不会阻塞,性能比expireAfterWrite同步加载高,但是,可能返回新值、也可能返回旧值。
当加载缓存的线程是异步加载的话,对于请求1,如果在异步结束前返回,就会返回旧值,反之是新值。
对于其他线程来说,不会被阻塞,直接返回,返回值可能是新值或者是旧值。
Loadingcache没使用额外的线程去做定时清理和加载的功能,而是依赖于get()请求。
在查询的时候,进行时间对比,如果使用refreshAfterWrite,在长时间没有查询时,查询有可能会得到一个旧值,我们可以通过设置refreshAfterWrite(写刷新,在get时可以同步或异步缓存的时间戳)为5s,将expireAfterWrite(写过期,在put时更新缓存的时间戳)设为10s,当访问频繁的时候,会在每5秒都进行refresh,而当超过10s没有访问,下一次访问必须load新值。
如果发生了缓存穿透,可以针对要查询的数据,在Redis中插入一条数据,添加一个约定好的默认值,比如defaultNull。
比如你想通过某个id查询某某订单,Redis中没有,MySQL中也没有,此时,就可以在Redis中插入一条,存为defaultNull,下次再查询就有了,因为是提前约定好的,前端也明白是啥意思,一切OK,岁月静好。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
private static LoadingCache<String, String> loadCache;
public static void initLoadingCache() {
loadCache = CacheBuilder.newBuilder()
// 并发级别设置为 10,是指可以同时写缓存的线程数
.concurrencyLevel(10)
// 写刷新,在get时可以同步或异步缓存的时间戳
.refreshAfterWrite(5, TimeUnit.SECONDS)
// 写过期,在put时更新缓存的时间戳
.expireAfterWrite(10, TimeUnit.SECONDS)
// 设置缓存容器的初始容量为 10
.initialCapacity(10)
// 设置缓存最大容量为 100,超过之后就会按照 LRU 算法移除缓存项
.maximumSize(100)
// 设置要统计缓存的命中率
.recordStats()
// 指定 CacheLoader,缓存不存在时,可自动加载缓存
.build(new CacheLoader<String, String>() {
@Override
public String load(String key) throws Exception {
// 自动加载缓存的业务
return "error";
}
}
);
}
查询模板信息后,通过loadCache.put(uuid, pdf);
加载到内存中,在编辑模板时,更新缓存过期时间,下次获取模板PDF时,直接从LoadingCache缓存中取,降低数据库访问压力,perfect!!!
然并卵,这种情况是不适合缓存的,因为模板pdf本来就是一个BLOB大数据,你把它放内存里缓存了,你告诉我,能放几个?内存扛得住吗?
优化炫技一时爽,BUG不断一直爽,一直爽
🏆文章收录于:100天精通Java从入门到就业
全网最细Java零基础手把手入门教程,系列课程包括:Java基础、Java8新特性、Java集合、高并发、性能优化等,适合零基础和进阶提升的同学。
🏆哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师。
华为OD机试 2023B卷题库疯狂收录中,刷题点这里
刷的越多,抽中的概率越大,每一题都有详细的答题思路、详细的代码注释、样例测试,发现新题目,随时更新,全天CSDN在线答疑。