当前位置: 首页 > 科技观察

多级缓存设计详解-减轻数据库负担刻不容缓!

时间:2023-03-14 17:46:07 科技观察

自古兵家谋略多,《谋攻篇》,“所以,一是攻军,二是攻敌,二是攻军,三是出兵。攻城,攻城之法,不得已而为之。可见攻城的方式有很多种。最不明智的做法是爬墙攻城。军队将被耗损,金钱和食物将丢失,人民将遭受损失。所以,我们有很多迂回的策略、谋略、外交、军事手段等等,每一种都比攻城更省钱、更轻巧,缓存设计也是如此。为什么要设计缓存?其实高并发的解决方案并不是互联网独有的。计算机的祖先早就为类似的场景制定了计划。比如《计算机组成原理》提到的cpucache概念就是高速缓存,容量比内存小,但速度快很多。数千次。传统的CPU直接通过fsb连接内存的方式,显然会因为等待内存访问而导致CPU的吞吐量下降,内存会成为性能瓶颈。同时,由于访问内存时热点数据比较集中,因此需要在CPU和内存之间建立一层临时存储作为缓存。随着系统复杂度的增加,缓存与内存之间的速度进一步扩大。由于技术难度和成本,存在较大的二级和三级缓存。按照读取顺序,绝大多数请求先落在一级缓存上,再落在二级缓存上……因此,应用到SOA甚至微服务场景时,内存就相当于一个持久化数据库存储业务数据的,它的吞吐量肯定远小于缓存,而对于java程序来说,本地jvm缓存要好于集中式redis缓存。关系数据库易于操作,易于维护,访问数据灵活。但是,随着数据量的增加,检索和更新的效率会越来越低。因此,在高并发、低延迟要求的复杂场景下,需要减轻数据库的负担,减轻其压力。减轻数据库负担1.分布式缓存,多级缓存(1)写读请求时,写入缓存。写入缓存时,先写入本地缓存,再写入集中缓存。具体的缓存方式有很多,但是有几个原则需要注意:不要复制粘贴,避免重复代码,避免和业务耦合太紧,不利于后期维护和发展。在线上开发初期,为了排查问题,缓存中往往会设置开关,但是过多的开关设置同时会增加系统的复杂度。需要结合统一的配置管理系统。京东物流有一套叫UCC,下章再细说。。。综上所述,高耦合带来的痛苦,弥补起来代价很大,所以可以参考Spring缓存来实现,而实现比较简单。使用的时候,一个注解就可以搞定。(2)写缓存失败怎么办?我应该先写入缓存还是数据库?既然是缓存设计,策略上肯定是保证最终一致性的,所以我们只需要使用异步消息来弥补即可。在大多数缓存应用场景中,读写比相差很大,读远大于写。这种场景只需要以数据库为中心,先写入数据库,再写入缓存即可。***补充一点,当数据库出现异常时,不要只捕获RuntimeException,而是抛出你关心的具体异常,然后进行针对性的异常处理。(3)在其他性能方面,缓存设计是尽量少占用。昂贵的内存资源和太大且难以维护驱使我们采用这种方式进行设计。因此,需要尽量减少缓存不必要的数据,有些同学为了图省事,将整个对象序列化存储起来。另外,序列化和反序列化也是很耗性能的。2.VS各种缓存同步方案缓存同步方案有很多,从一致性、数据库访问压力、实时性等方面考虑。一般来说有以下几种方法:(1)懒加载法就是上一段说的,在阅读的时候顺便加载。为了更新缓存数据,缓存需要过期。优点:简单直接缺点:会造成缓存失效。这样当用户并发很高的时候,缓存中没有数据,数据库就会承担瞬时流量过大的风险。懒加载太简单了,没有自动加载、异步刷新等机制,为了弥补它的不足,请参考下面两种方法。(2)辅助方法可以在缓存时将过期时间等信息写入一个异步队列,并在后台设置一个线程池定时扫描这个队列,在快要过期时主动重新加载缓存,从而使数据将始终保存在缓存中。如果没有缓存,则不需要查询数据库。常见的处理方式是使用binlog将消息处理成消息进行增量处理。优点:刷新缓存成为异步任务,由于任务队列的介入瞬间降低了数据库的压力,拉平了并发高峰。缺点:消息一旦积压,会造成同步延迟,引入复杂度。(3)定时加载需要异步线程池定时将数据库数据刷新到集中式缓存中,比如redis。优点:保证所有数据以最小的时间差同步到缓存中,延迟很低。缺点:如补充,需要任务调度框架,增加了复杂度,必须保证任务的顺序。如果想进一步加载到本地缓存中,则必须由本地应用自己启动线程抢占,该方案维护成本高。考虑使用mq或其他异步任务调度框架。ps:为了防止因为queuesize过大导致调度问题,处理完的数据要尽快结转,监控数据的积压和写入状态。3、防止缓存穿透缓存穿透就是查询key根本不存在,所以查询不到缓存,查询数据库。如果这样的key恰好有大量的并发请求,就会给数据库造成不必要的压力。如何解决?将所有存在的key存储在另一个存储的Set集合中,查询时可以先检查key是否存在。简单的说就是给不可查询的key加上一个标识空值的值,这样数据库就查询不到了。比如场景是查询省、市、市街道对应的移动营业厅。如果某条街道没有移动营业厅,关键规则不变,可将该值设置为无意义的字符,如“0”。当然,这个方案必须保证缓存集群的高可用。这些key可能不会永远存在,所以需要根据业务场景设置过期时间。4.热点缓存和缓存淘汰策略有些场景只需要保留部分热点缓存,不需要缓存全量缓存,比如热门商品信息、热门商圈信息等购买的产品类型等。一般来说,缓存过期有3种策略:(1)FIFO(FirstIn,FirstOut)先进先出,淘汰最早进来的缓存数据,一个标准的队列。以队列为基本数据结构,新数据从队首入队,队尾淘汰。(2)LRU(LeastRecentlyUsed)最近最少使用,淘汰最近没有使用的缓存数据。如果数据最近被访问过,则不会被逐出。与FIFO不同的是需要链表的基本模型。读写的时间复杂度是O(1)。新数据写入头部,当链表满时,从尾部剔除数据;将最近访问的数据移到头部第一部分,实现算法有很多,比如hashmap+双向链表等;问题是,如果最近偶尔频繁访问某些键,但不正常,数据就会被污染。(3)LFU(LeastFrequentlyused)淘汰最近最少使用的数据。注意与LRU的区别在于LRU的淘汰规则是基于访问时间的。LFU中的每个数据块都有一个引用计数,数据块根据引用计数进行排序。如果数据块碰巧有相同的引用计数,则按时间排序;因为新加入的数据访问次数为1,所以插入到队尾;队列中的数据被新访问后,引用计数增加,队列重新排序;当需要删除数据时,删除排序列表***的数据块;有一个明显的问题就是如果在短时间内频繁访问多次,比如访问异常或者循环不受控制,然后又长时间没有使用,数据就会被错误的保留和不会因高频而被淘汰。尤其是新数据,由于它的初始计数为1,即使正常使用,也会因为不如旧数据而被淘汰。所以维基百科上说纯LFU算法不是经常单独使用而是和其他策略结合使用。4.缓存使用的一些常见问题Q:那么应该选择使用本地缓存(localcache)还是集中缓存(Cachecluster)呢?A:首先看数据量和更新缓存的开销。如果整体缓存数据量不是很大,而且变化不频繁,建议使用本地缓存。Q:如何批量更新一批缓存数据?A:顺序从数据库读取,然后批量写入缓存,批量更新,设置版本过期键或者主动删除。Q:不知道有哪些key,如何定时删除?A:以redis为例,keys*太耗性能,不推荐。可以指定一个集合,把所有的key都存放在这个集合中,然后删除整个集合,这样就可以彻底清理干净了。Q:一个key包含一个大集合,redis无法实现内存空间的统一分片?A:可以简单的设置key过期时间,这样缓存不保;为key设置版本,比如两天后的当前时间,然后在读取缓存的时候根据时间判断是否需要重新加载缓存,作为版本过期的策略。王子辰:物流研发部架构师,GIS技术部负责人。2012年加入京东,有多年一线团队推广准备经验。负责部分物流研发部门的架构,专注于低延迟系统设计和海量数据处理。曾负责青龙配送订单团队,主导重构架构设计和主要研发工作,在短时间内将服务性能提升数十倍。还设计开发了地址配送网点分类模型,实现了配送订单精准配送至路段,降本增效,大大提高了自动配送订单的准确性。目前负责物流GIS部门,先后主持国标转京标、物流可视化等项目。【本文来自专栏作者张凯涛微信?(凯涛的博客),?id:kaitao-1234567】点此阅读作者更多好文