多级缓存设计详解|给数据库减负,刻不容缓!

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 前言上兵伐谋,其次伐交,其次伐兵,其下攻城。攻城之法,为不得已”,可见攻城之计有很多种,而爬墙攻城是最不明智的做法,军队疲惫受损、钱粮损耗、百姓遭殃。故而我们有很多迂回之策,谋略、外交、军事手段等等,每一种都比攻城的代价小,更轻量级,缓存设计亦是如此。

前言

上兵伐谋,其次伐交,其次伐兵,其下攻城。攻城之法,为不得已”,可见攻城之计有很多种,而爬墙攻城是最不明智的做法,军队疲惫受损、钱粮损耗、百姓遭殃。故而我们有很多迂回之策,谋略、外交、军事手段等等,每一种都比攻城的代价小,更轻量级,缓存设计亦是如此。

正文

为什么要设计缓存呢?

其实高并发应对的解决方案不是互联网独创的,计算机先祖们很早就对类似的场景做了方案。比如《计算机组成原理》这样提到的cpu缓存概念,它是一种高速缓存,容量比内存小但是速度却快很多,这种缓存的出现主要是为了解决cpu运算速度远大于内存读写速度,甚至达到千万倍。

传统的cpu通过fsb直连内存的方式显然就会因为内存访问的等待,导致cpu吞吐量下降,内存成为性能瓶颈。同时又由于内存访问的热点数据集中性,所以需要在cpu与内存之间做一层临时的存储器作为高速缓存。

随着系统复杂性的提升,这种高速缓存和内存之间的速度进一步拉开,由于技术难度和成本等原因,所以有了更大的二级、三级缓存。根据读取顺序,绝大多数的请求首先落在一级缓存上,其次二级...

img_e1830d6ac53c7b34c9e7c5e570e6f25d.jpe

故而应用于SOA甚至微服务的场景,内存相当于存储业务数据的持久化数据库,其吞吐量肯定是远远小于缓存的,而对于java程序来讲,本地的jvm缓存优于集中式的redis缓存。

关系型数据库操作方便、易于维护且访问数据灵活,但是随着数据量的增加,其检索、更新的效率会越来越低。所以在高并发低延迟要求复杂的场景,要给数据库减负,减少其压力。

给数据库减负

1、缓存分布式,做多级缓存

img_aa575fe14115c0697449b037163f4ce9.jpe

读请求时写缓存

写缓存时一级一级写,先写本地缓存,再写集中式缓存。具体些缓存的方法可以有很多种,但是需要注意几项原则:

不要复制粘贴,避免重复代码

切忌和业务耦合太紧,不利于后期维护

开发初期刚刚上线阶段,为了排查问题,常常会给缓存设置开关,但是开关设置多了则会同时升高系统的复杂度,需要结合一套统一配置管理系统。

综上所述,高耦合带来的痛,弥补的代价是很大的,所以可以借鉴Spring cache来实现,实现也比较简单,使用时一个注解就搞定了。

img_415556de4bee9fccff9c64c657d7db3f.jpe

写缓存失败了怎么办?应该先写缓存还是数据库呢?

既然是缓存的设计,那么策略一定是保证最终一致性,那么我们只需要采用异步消息来补偿就好了。

大部分缓存应用的场景是读写比差异很大的,读远大于写,在这种场景下,只需要以数据库为主,先写数据库,再写缓存就好了。

最后补充一点,数据库出现异常时,不要一股脑的catch RuntimeException,而是把具体关心的异常往外抛,然后进行有针对性的异常处理。

关于其他性能方面

缓存设计都是占用越少越好,内存资源昂贵以及太大不好维护都驱使我们这样设计。所以要尽可能减少缓存不必要的数据,有的同学图省事把整个对象序列化存储。另外,序列化与反序列化也是消耗性能的。

2、vs各种缓存同步方案

缓存同步方案有很多种,在考虑一致性、数据库访问压力、实时性等方面做权衡。总的来说有以下几种方式:

懒加载式

如上段提到的方式,读时顺便加载。为了更新缓存数据,需要过期缓存。

img_dda6bc638152c4821a17b4c61f379a94.jpe

优点:简单直接

缺点:

会造成一次缓存不命中

这样当用户并发很大时,恰好缓存中无数据,数据库承担瞬时流量过大会造成风险。

懒加载式太简单了,没有自动加载,异步刷新等机制,为了弥补其缺陷,请参见接下来的两种方法。

补充式

可以在缓存时,把过期时间等信息写到一个异步队列里,后台起个线程池定期扫描这个队列,在快过期时主动reload缓存,使得数据会一直保持在缓存中,如果缓存没有也没有必要去数据库查询了。常见的处理方式有使用binlog加工成消息供增量处理。

img_a8f86521059fbb498e7ba8ca8c05e983.jpe

优点:刷新缓存变为异步的任务,对数据库的压力瞬间由于任务队列的介入而降低了,削平并发的波峰。

缺点:消息一旦积压会造成同步延迟,引入复杂度。

定时加载式

这就需要有个异步线程池定期把数据库的数据刷到集中式缓存,如redis里。

优点:保证所有数据最小时间差同步到缓存中,延迟很低。

缺点:如补充式,需要一个任务调度框架,复杂度提升,且要保证任务的顺序。如果递进一步还想加载到本地缓存,就得本地应用自己起线程抓取,方案维护成本高。可以考虑使用mq或者其他异步任务调度框架。

ps:为了防止队列过大调度出现问题,处理完的数据要尽快结转,且要对积压数据以及写入情况做监控。

3、防止缓存穿透

缓存穿透是指查询的key压根不存在,从而缓存查询不到而查询了数据库。若是这样的key恰好并发请求很大,那么就会对数据库造成不必要的压力。怎么解决呢?

把所有存在的key都存到另外一个存储的Set集合里,查询时可以先查询key是否存在。

干脆简单一些,给查询不到的key也加一个标识空值的Value,这样就不会去查询数据库了,比如场景为查询省市区街道对应的移动营业厅,若是某街道确实没有移动营业厅,key规则不变,value可以设置为"0"等无意义的字符。当然此种方案要保证缓存集群的高可用。

这些Key可能不是永远不存在,所以需要根据业务场景来设置过期时间。

4、热点缓存与缓存淘汰策略

有一些场景,需要只保持一部分的热点缓存,不需要全量缓存,比如热卖的商品信息,购买某类商品的热门商圈信息等等。

综合来讲,缓存过期的策略有以下三种:

FIFO(First In,First Out)

先进先出,淘汰最早进来的缓存数据,一个标准的队列。

img_b76ef451b9010680241d7e0dd51a2162.jpe

以队列为基本数据结构,从队首进入新数据,从队尾淘汰。

LRU(Least RecentlyUsed)

最近最少使用,淘汰最近不使用的缓存数据。如果数据最近被访问过,则不淘汰。

img_0180a04ba04c4df942d9071b822bffea.jpe

和FIFO不同的是,需要对链表做基本模型,读写的时间复杂度是O(1),写入新数据进入头部,链表满了数据从尾部淘汰;

最近时间被访问的数据移动到头部,实现算法有很多,如hashmap+双向链表等等;

问题在于若是偶发性某些key被最近频繁访问,而非常态,则数据受到污染。

LFU(Least Frequently used)

最近使用次数最少的数据被淘汰,注意和LRU的区别在于LRU的淘汰规则是基于访问时间。

img_115f2d0baa76dfcc5b64e90bda7e0fa0.jpe

LFU中的每个数据块都有一个引用计数,数据块按照引用计数排序,若是恰好具有相同引用计数的数据块则按照时间排序;

因为新加入的数据访问次数为1,所以插入到队列尾部;

队列中的数据被新访问后,引用计数增加,队列重新排序;

当需要淘汰数据时,将已经排序的列表最后的数据块删除;

有很明显问题是若短时间内被频繁访问多次,比如访问异常或者循环没有控制住,而后很长时间未使用,则此数据会因为频率高而被错误的保留下来没有被淘汰。尤其对于新来的数据,由于其起始的次数是1,所以即便被正常使用也会因为比不过老的数据而被淘汰。所以维基百科说纯粹的LFU算法不经常单独使用而是组合在其他策略中使用。

4、缓存使用的一些常见问题

Q:那么应该选择用本地缓存(local cache)还是集中式缓存(Cache cluster)呢?

A:首先看数据量,看缓存更新的成本,如果整体缓存数据量不是很大,而且变化的不频繁,那么建议本地缓存。

Q:怎么批量更新一批缓存数据?

A:依次从数据库读取,然后批量写入缓存,批量更新,设置版本过期key或者主动删除。

Q:如果不知道有哪些key怎么定期删除?

A:拿redis来说keys * 太损耗性能,不推荐。可以指定一个集合,把所有的key都存到这个集合里,然后对整个集合进行删除,这样便能完全清理了。

Q:一个key包含的集合很大,redis无法做到内存空间上的均匀Shard?

A:1、可以简单的设置key过期,这样就要允许有缓存不命中的情况;2、给key设置版本,比如为两天后的当前时间,然后读取缓存时用时间判断一下是否需要重新加载缓存,作为版本过期的策略。

Java高架构师、分布式架构、高可扩展、高性能、高并发、性能优化、Spring boot、Redis、ActiveMQ、Nginx、Mycat、Netty、Jvm大型分布式项目实战学习架构师视频免费学习加群:835638062 点击链接加入群聊【Java高级架构】:https://jq.qq.com/?_wv=1027&k=5S3kL3v

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
17天前
|
消息中间件 缓存 数据库
如何保证缓存与数据库的数据一致性?
如何保证缓存与数据库的数据一致性?
32 5
|
15天前
|
存储 缓存 NoSQL
Redis多级缓存指南:从前端到后端全方位优化!
本文探讨了现代互联网应用中,多级缓存的重要性,特别是Redis在缓存中间件的角色。多级缓存能提升数据访问速度、系统稳定性和可扩展性,减少数据库压力,并允许灵活的缓存策略。浏览器本地内存缓存和磁盘缓存分别优化了短期数据和静态资源的存储,而服务端本地内存缓存和网络内存缓存(如Redis)则提供了高速访问和分布式系统的解决方案。服务器本地磁盘缓存因I/O性能瓶颈和复杂管理而不推荐用于缓存,强调了内存和网络缓存的优越性。
40 1
|
1月前
|
缓存 NoSQL 算法
17- 数据库有1000万数据 ,Redis只能缓存20w数据, 如何保证Redis中的数据都是热点数据 ?
保证Redis中的20w数据为热点数据,可以通过设置Redis的LFU(Least Frequently Used)淘汰策略。这样,当数据库有1000万数据而Redis仅能缓存20w时,LFU会自动移除使用频率最低的项,确保缓存中的数据是最常使用的。
63 8
|
2天前
|
缓存 NoSQL 应用服务中间件
Redis多级缓存
Redis多级缓存
8 0
|
12天前
|
消息中间件 缓存 关系型数据库
数据库和缓存如何保证一致性?
数据库和缓存如何保证一致性?
|
17天前
|
缓存 NoSQL Java
Springboot 多级缓存设计与实现
Springboot 多级缓存设计与实现
|
17天前
|
缓存 NoSQL 关系型数据库
在Python Web开发过程中:数据库与缓存,MySQL和NoSQL数据库的主要差异是什么?
MySQL与NoSQL的主要区别在于数据结构、查询语言和可扩展性。MySQL是关系型数据库,依赖预定义的数据表结构,使用SQL进行复杂查询,适合垂直扩展。而NoSQL提供灵活的存储方式(如JSON、哈希表),无统一查询语言,支持横向扩展,适用于处理大规模、非结构化数据和高并发场景。选择哪种取决于应用需求、数据模型及扩展策略。
26 0
|
17天前
|
SQL 缓存 数据库
在Python Web开发过程中:数据库与缓存,如何使用ORM(例如Django ORM)执行查询并优化查询性能?
在Python Web开发中,使用ORM如Django ORM能简化数据库操作。为了优化查询性能,可以:选择合适索引,避免N+1查询(利用`select_related`和`prefetch_related`),批量读取数据(`iterator()`),使用缓存,分页查询,适时使用原生SQL,优化数据库配置,定期优化数据库并监控性能。这些策略能提升响应速度和用户体验。
18 0
|
2月前
|
缓存 应用服务中间件 数据库
【分布式技术专题】「缓存解决方案」一文带领你好好认识一下企业级别的缓存技术解决方案的运作原理和开发实战(多级缓存设计分析)
【分布式技术专题】「缓存解决方案」一文带领你好好认识一下企业级别的缓存技术解决方案的运作原理和开发实战(多级缓存设计分析)
43 1
|
2月前
|
缓存 NoSQL Redis
在Python Web开发过程中:数据库与缓存,除了Redis是内存数据库以外,还有哪些原因使其运行速度快?
Redis在Python Web开发中快速的原因:内存存储、多样化数据结构(如字符串、哈希、列表等)简化数据模型,单线程处理提高效率,结合非阻塞I/O;RDB和AOF提供持久化保障;TCP+二进制协议减少网络开销;管道技术提升通信效率。这些设计使Redis能高效处理高并发请求。
20 3