在项目中如何巧妙使用缓存
缓存
对于经常访问的数据,每次都从数据库(硬盘)中获取是比较慢,可以利用性能更高的存储来提高系统响应速度,俗称缓存 。合理使用缓存可以显著降低数据库的压力、提高系统性能。
那么,什么样的数据适合缓存呢?一般情况下就4个字“读多写少 ”,要频繁查询的、不怎么修改的。
具体来说:
- 高频访问的数据:如系统首页、热门推荐内容等。
- 计算成本较高的数据:如复杂查询结果、大量数据的统计结果。
- 允许短时间延迟的数据:如不需要实时更新的排行榜、图片列表等。
在我们的项目中,主页是用户高频访问的内容,调用的获取图片列表的接口也是高频访问的。而且即使数据更新存在一定延迟,也不会对用户体验造成明显影响,因此非常适合缓存。
Redis分布式缓存
分布式缓存是指将缓存数据分布存储在多台服务器上,以便在高并发场景下提供更高的吞吐量和更好的容错性。
Redis是实现分布式缓存的主流方案,也是后端开发必学的技能。主要是由于它具有下面几个优势:
- 高性能:基于内存操作,访问速度极快。单节点 Redis的读写QPS可达10w次每秒!
- 丰富的数据结构:支持字符串、列表、集合、哈希、位图等,适用于各种数据结构存储。
- 分布式支持:可以通过RedisCluster构建高可用、高性能的分布式缓存,还提供哨兵集群机制提升可用性、提供分片集群机制提高可扩展性。
缓存设计
需要缓存首页的图片列表数据,也就是对 listPictureVOByPage接口进行缓存。首先按照缓存3要素"key、value、过期时间”进行设计。
- 缓存 key 设计
由于接口支持传入不同的查询条件,对应的数据不同,因此需要将查询条件作为缓存key的一部分。
可以将查询条件对象转换为JSON字符串,但这个JSON会比较长,可以利用哈希算法(md5)来压缩key。
此外,由于使用分布式缓存,可能由多个项目和业务共享,因此需要在key的开头拼接前缀进行隔离。设计出的key如下:
yunpicture:listPictureVOByPage:${查询条件key} - 缓存 value 设计
缓存从数据库中查到的Page分页对象,存储为什么格式呢?这里有2种选择:
- 为了可读性,可以转换为JSON结构的字符串
- 为了压缩空间,可以存为二进制等其他结构
但是对应的 Redis 数据结构都是 string。
- 缓存过期时间设置
必须设置缓存过期时间! 根据实际业务场景和缓存空间的大小、数据的一致性的要求设置,合适即可,此处由于查询条件较多、而且考虑到图片会持续更新,设置为5~60分钟即可。
Caffeine 本地缓存
当应用需要频繁访问某些数据时,可以将这些数据缓存到应用的内存中(比如JVM中);下次访问时,直接从内存读取,而不需要经过网络或其他存储系统。
相比于分布式缓存,本地缓存的速度更快,但是无法在多个服务器间共享数据、而且不方便扩容。
所以本地缓存的应用场景一般是:
- 数据访问量有限的小型数据集
- 不需要服务器间共享数据的单机应用
- 高频、低延迟的访问场景(如用户临时会话信息、短期热点数据)。
对于Java项目,Caffeine是主流的本地缓存技术,拥有极高的性能和丰富的功能。比如可以精确控制缓存数量和大小、支持缓存过期、支持多种缓存淘汰策略、支持异步操作、线程安全等。
由于本地缓存不需要引入额外的中间件,成本更低。因此如果只是要提升数据访问性能,优先考虑本地缓存而不是分布式缓存。
缓存设计
本地缓存的设计和分布式缓存基本一致,不再赞述。但有2个区别:
- 本地缓存需要自己创建初始化缓存结构(可以简单理解为要自己new一个HashMap)。
- 由于本地缓存本身就是服务器隔离的,而且占用服务器的内存,key可以更精简一些,不用再添加项目前缀。
多级缓存
多级缓存是指结合本地缓存和分布式缓存的优点,在同一业务场景下构建两级缓存系统,这样可以兼顾本地缓存的高性能、以及分布式缓存的数据一致性和可靠性。
多级缓存的工作流程:
- 第一级(Caffeine本地缓存):优先从本地缓存中读取数据。如果命中,则直接返回。
- 第二级(Redis分布式缓存):如果本地缓存未命中,则查询Redis分布式缓存。如果Redis命中,则返回数据并更新本地缓存。
- 数据库查询:如果Redis也未命中,则查询数据库,并将结果写入Redis和本地缓存。