从公众号转载,关注微信公众号掌握更多技术动态
---------------------------------------------------------------
一、缓存底子
1.缓存简介
缓存提升性能的幅度,不只取决于存储介质的速度,还取决于缓存掷中率。为了进步掷中 率,缓存会基于时间、空间两个维度更新数据。在时间上可以采用 LRU、FIFO 等算法镌汰 数据,而在空间上则可以预读、归并连续的数据。如果只是简单地选择最流行的缓存管理 算法,就很容易忽略业务特性,从而导致缓存性能的下降。
(1)掷中率
当某个请求可以或许通过访问缓存而得到相应时,称为缓存掷中。缓存掷中率越高,缓存的使用率也就越高。
(2)最大空间
缓存中可以容纳最大元素的数目。当缓存存放的数据超过最大空间时,就需要根据镌汰算法来镌汰部门数据存放新到达的数据。
⼤容量缓存是能带来性能加速的 收益,但是成本也会更⾼,⽽⼩容量缓存不⼀定就起不到加速访问的效果。⼀般来说,建议把缓存容量 设置为总数据量的15%到30%,兼顾访问性能和内存空间开销。
(3)镌汰算法
缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳固服务的同时 有效提升掷中率?这就由缓存镌汰算法来处理,计划适合自身数据特征的镌汰算法可以或许有效提升缓存掷中率。常见的镌汰算法有:
- FIFO(first in first out)「先进先出」。最先进入缓存的数据在缓存空间不敷的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间担当新的数据。计谋算法主要比力缓存元素的创建时间。「适用于保证高频数据有效性场景,优先保障最新数据可用」。
- LFU(less frequently used)「最少使用」,无论是否过期,根据元素的被使用次数判定,清除使用次数较少的元素开释空间。计谋算法主要比力元素的hitCount(掷中次数)。「适用于保证高频数据有效性场景」。
- LRU(least recently used)「近来最少使用」,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素开释空间。计谋算法主要比力元素近来一次被get使用时间。当遇到爬虫时,缓存的数据变成非热门数据。「比力适用于热门数据场景,优先保证热门数据的有效性。」
LRU计谋更加关注数据的时效性,⽽LFU计谋更加关注数据的访问频次。
(4)缓存的使用场景
- 经常需要读取的数据
- 频繁访问的数据 热门数据缓存
- IO 瓶颈数据
- 计算昂贵的数据
- 无需及时更新的数据
- 缓存的目标是减少对后端服务的访问,降低后端服务的压力
(5)缓存更新计谋
缓存中的数据会和数据源中的真实数据有一段时间窗口的不同等,需要使用某些计谋进行更新,下面会介绍几种主要的缓存更新计谋。
①LRU/LFU/FIFO算法剔除:剔除算法通常用于缓存使用量超过了预设的最大值时候,如何对现有的数据进行剔除。例如Redis使用maxmemory-policy这个设置作为内存最大值后对于数据的剔除计谋。
②超时剔除:通过给缓存数据设置过期时间,让其在过期时间后自动删除,例如Redis提供的expire命令。如果业务可以容忍一段时间内,缓存层数据和存储层数据不同等,那么可以为其设置过期时间。在数据过期后,再从真实数据源获取数据,重新放到缓存并设置过期时间。例如一个视频的描述信息,可以容忍几分钟内数据不同等,但是涉及交易方面的业务,后果可想而知。
③主动更新:应用方对于数据的同等性要求高,需要在真实数据更新后,立刻更新缓存数据。例如可以使用消息系统或者其他方式通知缓存更新。
有两个建议:
- 低同等性业务建议设置最大内存和镌汰计谋的方式使用。
- 高同等性业务可以结合使用超时剔除和主动更新,这样纵然主动更新出了问题,也能保证数据过期时间后删除脏数据。
(6)缓存粒度控制
缓存粒度问题是一个容易被忽视的问题,如果使用不当,可能会造成很多无用空间的浪费,网络带宽的浪费,代码通用性较差等情况,需要综合数据通用性、空间占用比、代码维护性三点进行弃取。
对于缓存的使用,需要对其存储的内容和数目严格限制,并对大小进行估算,通过文档进行维护。
2.缓存如何进步性能
缓存指的将数据存储在相对较高访问速度的存储介质中,以供系统处理。内存是半导体元件。对于内存而言,只要给出了内存地址,就可以直接访问该地址取出 数据。内存的访问速度很快但价格昂贵。而磁盘是机器器件。磁盘访问数据时,需要等磁盘盘片旋转到磁头下,才能读取相应的数 据。尽管磁盘的旋转速度很快,但是和内存的随机访问相比,性能差距非常大。一样寻常来说,如果是随机读写,会有 10 万到 100 万倍左右的差距。但如果是顺序访问 大批量数据的话,磁盘的性能和内存就是一个数目级的。磁盘的最小读写单位是扇区,目前常见的磁盘扇区是 4K 个字节。使用系统一次会读写多个扇区,所以使用系统的最小读 写单位是块(Block),也叫作簇(Cluster)。当要从磁盘中读取一个数据时,使用系 统会一次性将整个块都读出来。因此,对于大批量的顺序读写来说,磁盘的效率会比随机读 写高许多。
一方面缓存访问速度快,可以减少数据访问的时间,另一方面如果缓存的数据是经过计算的处理得到的,那么被缓存的数据无需重复计算即可直接使用,因此缓存还起到减少计算时间的作用。例如,一个论坛需要在首页展示当前有多少用户同时在线,如果使用 MySQL 来存储当前用户状态,则每次获取这个总数都要“count(*)”大量数据,这样的使用无论怎么优化 MySQL,性能都不会太高。如果要及时展示用户同时在线数,则 MySQL 性能无法支持。
缓存的本质是一个内存的Hash表,网站应用中,数据缓存以一对key、value的情势存储在内存Hash表中。Hash表数据读写的时间复杂度为O(1)。缓存主要用来存放读多写少、很少厘革的数据,比如商品的类目信息,热门词的搜索列表信息,热门商品信息等。应用程序读取数据,先到缓存中读取,如果读取不到或数据已失效,再访问数据库并将数据写入到缓存。
网站数据访问通常遵循二八定律(80%访问落在20%数据上),因此使用Hash表和内存的高速访问特性,将这20%的数据缓存起来,可很好改善系统性能,进步数据读取速度,降低存储访问压力,进步吞吐量。以微博为例:一个明星发一条微博,可能几万万人来浏览。如果使用 MySQL 来存储微博,用户写微博只有一条 insert 语句,但每个用户浏览时都要 select 一次,纵然有索引,几万万条 select 语句对 MySQL 数据库的压力也会非常大。
获取缓存的时候万万不用通过服务调用获取缓存,调用服务花费的时间远宏大于获取缓存,这样意义不大
3.缓存失效的方式
- 被动失效,主要处理如模板变动和一些对时效性不太敏感数据的失效,采用设置一定时间 长度(如只缓存 3 秒钟)这种自动失效的方式。固然,你也要开发一个后台管理界面, 以便可以或许在紧急情况动手工失效某些 Cache。
- 主动失效,一样寻常有 Cache 失效中心监控数据库表厘革发送失效请求、系统发布也需要清 空 Cache 数据等几种场景。其中失效中心负担了主要的失效功能,这个失效中心的逻 图如下:
失效中心会监控关键数据表的变动(有个中间件来解析 MySQL 的 binglog,然后发现有 Insert、Update、Delete 等使用时,会把变动前的数据以及要变动的数据转成一个消息发 送给订阅方),通过这种方式来发送失效请求给 Cache,从而清除 Cache 数据。
二、缓存带来的复杂性
缓存避免再高峰刷新,避免连接数占满
1.合理使用缓存
①频繁修改的数据不应该使用缓存
当数据的读写的比例很大的时候才保举使用缓存。
②对于访问频率低的数据不应该使用缓存
因为缓存以内存为存储,内存资源宝贵,不可能将所有数据都进行缓存。
③数据同等性要求的访问不应该使用缓存
一样寻常会对缓存设置过期时间,而当缓存没到过期时间更新了数据这时可能出现数据不同等与脏读
缓存每每针对的是“资源”,当某一个使用是“幂等”的和“安全”的,那么这样的使用就可以被抽象为对“资源”的获取使用,那么它才可以考虑被缓存。有些使用不幂等、不安全,比银行转账
④缓存是为了解决“开销”的问题
这个开销,可不只有时间的开销。虽然我们在很多情况下讲的开销,确实都是在时间维度上的,但它还可以是 CPU、网络、I/O 等一切资源。所以缓存的目标不仅仅是为了让系统速度更快
⑤写数据库计谋
对于读写缓存来说,如果要对数据进⾏增编削,就需要在缓存中进⾏,同时还要根据采取的写回计谋,决定是否同步写回到数据库中。
- 同步直写计谋:写缓存时,也同步写数据库,缓存和数据库中的数据⼀致;
- 异步写回计谋:写缓存时不同步写数据库,比及数据从缓存中镌汰时,再写回数据库。使⽤这种计谋时,
如果数据还没有写回数据库,缓存就发⽣了故障,那么,此时,数据库就没有最新的数据了。
所以,对于读写缓存来说,要想保证缓存和数据库中的数据⼀致,就要采⽤同步直写计谋。不过,需要注意的是,如果采⽤这种计谋,就需要同时更新缓存和数据库。所以要在业务应⽤中使⽤事件机制,来保证缓存和数据库的更新具有原⼦性,也就是说,两者要不⼀起更新,要不都不更新,返回错误信息,进⾏重试。否则就⽆法实现同步直写。
固然,在有些场景下,我们对数据⼀致性的要求可能不是那么⾼,⽐如说缓存的是电商商品的⾮关键属性或者短视频的创建或修改时间等,那么可以使⽤异步写回计谋。
2.数据库缓存数据同等性——终极同等性
①缓存先后删除问题
不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不同等的情况。
先删除缓存
- 如果先删除Redis缓存数据,然而还没有来得及写入MySQL,另一个线程就来读取
- 这个时候发现缓存为空,则去Mysql数据库中读取旧数据写入缓存,此时缓存中为脏数据。
- 然后数据库更新后发现Redis和Mysql出现了数据不同等的问题
后删除缓存
- 如果先写了库,然后再删除缓存,不幸的写库的线程挂了,导致了缓存没有删除
- 这个时候就会直接读取旧缓存,终极也导致了数据不同等情况
- 因为写和读是并发的,没法保证顺序,就会出现缓存和数据库的数据不同等的问题
②延时双删计谋
在写库前后都进行redis.del(key)使用,并且设定合理的超时时间。
- 先删除缓存
- 再写数据库
- 休眠500毫秒(时间的控制是玄学)
- 再次删除缓存
- public void write( String key, Object data ){ redis.delKey( key ); db.updateData( data ); Thread.sleep( 500 ); redis.delKey( key ); }b
复制代码
问题:这个500毫秒怎么确定的,具体该休眠多久时间呢?
- 需要评估自己的项目标读数据业务逻辑的耗时。
- 这么做的目标,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
- 固然这种计谋还要考虑redis和数据库主从同步的耗时。
- 最后的的写数据的休眠时间:则在读数据业务逻辑的耗时底子上,加几百ms即可。比如:休眠1秒。
设置缓存过期时间是关键点
- 理论上来说,给缓存设置过期时间,是保证终极同等性的解决方案
- 所有的写使用以数据库为准,只要到达缓存过期时间,缓存删除
- 如果背面另有读请求的话,就会从数据库中读取新值然后回填缓存
方案缺点
结合双删计谋+缓存超时设置,这样最差的情况就是:
- 在缓存过期时间内发生数据存在不同等
- 同时又增长了写请求的耗时。
③异步更新缓存(基于Mysql binlog的同步机制)
可以把要删除的缓存值或者是要更新的数据库值暂存到消息队列中。当应⽤没有可以或许成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进⾏删除或更新。如果可以或许成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复使用,此时也可以保证数据库和缓存的数据⼀致了。否则的话,还需要再次进⾏重试。如果重试超过的⼀定次数,还是没有成功,就需要向业务层发送报错信息了。
- 涉及到更新的数据使用,使用Mysql binlog 进行增量订阅消费
- 将消息发送到消息队列
- 通过消息队列消费将增量数据更新到Redis上
- 使用情况
- 读取Redis缓存:热数据都在Redis上
- 写Mysql:增编削都是在Mysql进行使用
- 更新Redis数据:Mysql的数据使用都记录到binlog,通过消息队列及时更新到Redis上
3.缓存穿透(少量可担当)
对于像电商中的商品系统、搜索系统这类与用户关联不大的系统,根本不会产生
缓存穿透是指缓存没有发挥作用,业务系统虽然去缓存查询数据,但缓存中没有数据,业务系统需要再次去存储系统查询数据。通常情况下有两种情况:
①被访问的存储数据不存在
一样寻常情况下,如果存储系统中没有某个数据,则不会在缓存中存储相应的数据,这样就导致用户查询的时候,在缓存中找不到对应的数据,每次都要去存储系统中再查询一遍,然后返回数据不存在。缓存在这个场景中并没有起到分担存储系统访问压力的作用。
通常情况下,业务上读取不存在的数据的请求量并不会太大,但如果出现一些非常情况,例如被黑客攻击,故意大量访问某些读取不存在数据的业务,有可能会将存储系统拖垮。
- 缓存空值(保举),如果查询存储系统的数据没有找到,则直接设置一个默认值(可以是空值,也可以是具体的值)存到缓存中,这样第二次读取缓存时就会获取到默认值,而不会继承访问存储系统。但是如果后续这个请求有新值了需要把原来缓存的空值删除掉(所以一样寻常过期时间可以稍微设置的比力短)。
- 通过布隆过滤器。查询缓存之前先去布隆过滤器查询下这个数据是否存在。如果数据不存在,然后直接返回空。这样的话也会减少底层系统的查询压力。
- 缓存没有直接返回。这种方式的话要根据自己的实际业务来进行选择。比如固定的数据,一些省份信息或者都会信息,可以全部缓存起来。这样的话数据有厘革的情况,缓存也需要跟着厘革。实现起来可能比力复杂。
②缓存数据生成淹灭大量时间或者资源
如果刚幸亏业务访问的时候缓存失效了,那么也会出现缓存没有发挥作用,访问压力全部集中在存储系统上的情况。
典型的就是电商的商品分页,假设我们在某个电商平台上选择“手机”这个类别查看,由于数据巨大,不能把所有数据都缓存起来,只能按照分页来进行缓存,由于难以预测用户到底会访问哪些分页,因此业务上最简单的就是每次点击分页的时候按分页计算和生成缓存。通常情况下这样实现是根本满足要求的,但是如果被竞争对手用爬虫来遍历的时候,系统性能就可能出现问题。
具体的场景有:
分页缓存的有效期设置为 1 天,因为设置太长时间的话,缓存不能反应真实的数据。
通常情况下,用户不会从第 1 页到最后 1 页全部看完,一样寻常用户访问集中在前 10 页,因此第 10 页以后的缓存过期失效的可能性很大。
竞争对手每周来爬取数据,爬虫会将所有分类的所有数据全部遍历,从第 1 页到最后 1 页全部都会读取,此时很多分页缓存可能都失效了。由于很多分页都没有缓存数据,从数据库中生成缓存数据又非常淹灭性能(order by limit 使用),因此爬虫会将整个数据库全部拖慢。
这种情况并没有太好的解决方案,因为爬虫会遍历所有的数据,而且什么时候来爬取也是不确定的,可能是天天都来,也可能是每周,也可能是一个月来一次,我们也不可能为了应对爬虫而将所有数据永久缓存。通常的应对方案要么就是辨认爬虫然后禁止访问,但这可能会影响 SEO 和推广;要么就是做好监控,发现问题后及时处理,因为爬虫不是攻击,不会进行暴力粉碎,对系统的影响是逐步的,监控发现问题后有时间进行处理。
4.缓存雪崩
缓存雪崩是指当缓存失效(过期)后引起系统性能急剧下降的情况。当缓存过期被清除后,业务系统需要重新生成缓存,因此需要再次访问存储系统,再次进行运算,这个处理步调耗时几十毫秒乃至上百毫秒。而对于一个高并发的业务系统来说,几百毫秒内可能会接到几百上千个请求。由于旧的缓存已经被清除,新的缓存还未生成,并且处理这些请求的线程都不知道别的有一个线程正在生成缓存,因此所有的请求都会去重新生成缓存,都会去访问存储系统,从而对存储系统造成巨大的性能压力。这些压力又会拖慢整个系统,严峻的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
①更新锁
对缓存更新使用进行加锁保护,保证只有一个线程可以或许进行缓存更新,未能获取更新锁的线程要么等待锁开释后重新读取缓存,要么就返回空值或者默认值。
对于采用分布式集群的业务系统,由于存在几十上百台服务器,纵然单台服务器只有一个线程更新缓存,但几十上百台服务器一起算下来也会有几十上百个线程同时来更新缓存,同样存在雪崩的问题。因此分布式集群的业务系统要实现更新锁机制,需要用到分布式锁,如 ZooKeeper。
②后台更新
由后台线程来更新缓存,而不是由业务线程来更新缓存,缓存自己的有效期设置为永久,后台线程定时更新缓存。后台定时机制需要考虑一种特殊的场景,当缓存系统内存不敷时,会“踢掉”一些缓存数据,从缓存被“踢掉”到下一次定时更新缓存的这段时间内,业务线程读取缓存返回空值,而业务线程自己又不会去更新缓存,因此业务上看到的现象就是数据丢了。解决的方式有两种:
- 后台线程除了定时更新缓存,还要频繁地去读取缓存(例如,1 秒或者 100 毫秒读取一次),如果发现缓存被“踢了”就立刻更新缓存,这种方式实现简单,但读取时间间隔不能设置太长,因为如果缓存被踢了,缓存读取间隔时间又太长,这段时间内业务访问都拿不到真正的数据而是一个空的缓存值,用户体验一样寻常。
- 业务线程发现缓存失效后,通过消息队列发送一条消息通知后台线程更新缓存。可能会出现多个业务线程都发送了缓存更新消息,但着实对后台线程没有影响,后台线程收到消息后更新缓存前可以判定缓存是否存在,存在就不执行更新使用。这种方式实现依靠消息队列,复杂度会高一些,但缓存更新更及时,用户体验更好。
后台更新既顺应单机多线程的场景,也适合分布式集群的场景,相比更新锁机制要简单一些。后台更新机制还适合业务刚上线的时候进行缓存预热。缓存预热指系统上线后,将相关的缓存数据直接加载到缓存系统,而不是等待用户访问才来触发缓存加载。
③灰度发布
当系统初始化的时候,比如说系统升级重启或者是缓存刚上线,这个时候缓存是空的,如果大量的请求直接打过来,很容易引发大量缓存穿透导致雪崩。为了避免这种情况,可以采用灰度发布的方式,先接入少量请求,再逐步增长系统的请求数目,直到全部请求都切换完成。
④多级缓存
不同级别缓存时间过期时间不一样,纵然某个级别缓存过期了,另有其他缓存级别 兜底。比如我们Redis缓存过期了,另有本地缓存。这样的话纵然没有掷中redis,有可能会掷中本地缓存。
5.缓存击穿
缓存击穿是指热门key在某个时间点过期的时候,而恰幸亏这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db,属于常见的“热门”问题。这个的话可以用缓存雪崩的几种解决方法来避免:
- 缓存永不过期。Redis中保存的key永久不失效,这样的话就不会出现大量缓存同时失效的问题,但是这种做法会浪费更多的存储空间,一样寻常应该也不会保举这种做法。
- 异步重建缓存。这样的话需要维护每个key的过期时间,定时去轮询这些key的过期时间。例如一个key的value设置的过期时间是30min,那我们可以为这个key设置它自己的一个过期时间为20min。所以当这个key到了20min的时候我们就可以重新去构建这个key的缓存,同时也更新这个key的一个过期时间。
- 互斥锁重建缓存。这种情况的话只能针对于同一个key的情况下,比如你有100个并发请求都要来取A的缓存,这时候可以借助redis分布式锁来构建缓存,让只有一个请求可以去查询DB其他99个(没有获取到锁)都在外面等着,等A查询到数据并且把缓存构建好之后其他99个请求都只需要从缓存取就好了。原理就跟我们java的DCL(double checked locking)头脑有点类似。
6.缓存热门
虽然缓存系统自己的性能比力高,但对于一些特别热门的数据,如果大部门乃至所有的业务请求都掷中同一份缓存数据,则这份数据所在的缓存服务器的压力也很大。例如,某明星微博发布“我们”来宣告爱情了,短时间内上万万的用户都会来围观。
缓存热门的解决方案就是复制多份缓存副本,将请求分散到多个缓存服务器上,减轻缓存热门导致的单台缓存服务器压力。以微博为例,对于粉丝数超过 100 万的明星,每条微博都可以生成 100 份缓存,缓存的数据是一样的,通过在缓存的 key 内里加上编号进行区分,每次读缓存时都随机读取其中某份缓存。
缓存副本计划有一个细节需要注意,就是不同的缓存副本不要设置统一的过期时间,否则就会出现所有缓存副本同时生成同时失效的情况,从而引发缓存雪崩效应。正确的做法是设定一个过期时间范围,不同的缓存副本的过期时间是指定范围内的随机值。
①如何辨认热门key
- 凭经验判定哪些是热Key;
- 客户端统计上报;
- 服务代理层上报
②如何解决热key问题?
- Redis集群扩容:增长分片副本,平衡读流量;
- 将热key分散到不同的服务器中;
- 使用二级缓存,即JVM本地缓存,减少Redis的读请求。
③热门key重建优化
使用“缓存+过期时间”的计谋既可以加速数据读写,又保证数据的定期更新,这种模式根本可以或许满足绝大部门需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:
- 当前key是一个热门key(例如一个热门的娱乐消息),并发量非常大。
- 重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、多个依靠等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,乃至可能会让应用崩溃。
要解决这个问题也不是很复杂,但是不能为了解决这个问题给系统带来更多的麻烦,所以需要制定如下目标:
- 减少重建缓存的次数
- 数据尽可能同等。
- 较少的潜在伤害
- 互斥锁:此方法只答应一个线程重建缓存,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可,整个过程如图所示。
- 永久不过期
- 从缓存层面来看,确实没有设置过期时间,所以不会出现热门key过期后产生的问题,也就是“物理”不过期。
- 从功能层面来看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程去构建缓存。
④拦截非法的查询请求
可以使用验证码、IP限制等手段限制恶意攻击,并用敏感词过滤器等拦截不合理的非法查询。
7.无底洞优化
为了满足业务需要可能会添加大量新的缓存节点,但是发现性能不光没有好转反而下降了。用一句通俗的话解释就是,更多的节点不代表更高的性能,所谓“无底洞”就是说投入越多不一定产出越多。但是分布式又是不可以避免的,因为访问量和数据量越来越大,一个节点根本抗不住,所以如何高效地在分布式缓存中批量使用是一个难点。
无底洞问题分析:
①客户端一次批量使用会涉及多次网络使用,也就意味着批量使用会随着节点的增多,耗时会不断增大。
②网络连接数变多,对节点的性能也有一定影响。
如安在分布式条件下优化批量使用?我们来看一下常见的IO优化思绪:
- 命令自己的优化,例如优化SQL语句等。
- 减少网络通信次数。
- 降低接入成本,例如客户端使用长连/连接池、NIO等。
这里我们假设命令、客户端连接已经为最优,重点讨论减少网络使用次数。下面我们将结合Redis Cluster的一些特性对四种分布式的批量使用方式进行阐明。
①串行命令:由于n个key是比力匀称地分布在Redis Cluster的各个节点上,因此无法使用mget命令一次性获取,所以通常来讲要获取n个key的值,最简单的方法就是逐次执行n个get命令,这种使用时间复杂度较高,它的使用时间=n次网络时间+n次命令时间,网络次数是n。很显然这种方案不是最优的,但是实现起来比力简单。
②串行IO:Redis Cluster使用CRC16算法计算出散列值,再取对16383的余数就可以算出slot值,同时Smart客户端会保存slot和节点的对应关系,有了这两个数据就可以将属于同一个节点的key进行归档,得到每个节点的key子列表,之后对每个节点执行mget或者Pipeline使用,它的使用时间=node次网络时间+n次命令时间,网络次数是node的个数,整个过程如下图所示,很显着这种方案比第一种要好很多,但是如果节点数太多,还是有一定的性能问题。
③并行IO:此方案是将方案2中的最后一步改为多线程执行,网络次数虽然还是节点个数,但由于使用多线程网络时间变为O(1),这种方案会增长编程的复杂度。
④hash_tag实现:Redis Cluster的hash_tag功能,它可以将多个key强制分配到一个节点上,它的使用时间=1次网络时间+n次命令时间。
四种批量使用解决方案对比
三、缓存应用模式
写后立刻读,脏数据库入缓存:在主从数据库同步完成之前,如果有读请求,都可能发生读Cache Miss,读从库把旧数据存入缓存的情况。(此时应该避免写后立刻读)
1.Cache-Aside(解决了并发数据脏读问题)
数据获取计谋:大多数缓存,比如拦截过滤器中的缓存,根本上都是按照这种方式来设置和使用的。
①数据读取情况
- 应用先去查看缓存是否有所需数据;
- 如果有,应用直接将缓存数据返回给请求方;
- 如果没有,应用执行原始逻辑,例如查询数据库得到结果数据;
- 应用将结果数据写入缓存。
②数据更新计谋:
- 应用先更新数据库;
- 应用再令缓存失效(不论数据库是否更新成功)
数据更新的这个计谋,通常来说,最重要的一点是必须先更新数据库,而不是先令缓存失效,即这个顺序不能倒过来。缘故原由在于,如果先令缓存失效,那么在数据库更新成功前,如果有别的一个请求访问了缓存,发现缓存数据库已经失效,于是就会按照数据获取计谋,从数据库中使用这个已经陈旧的数值去更新缓存中的数据,这就导致这个过期的数据会长期存在于缓存中,终极导致数据不同等的严峻问题。
数据库更新以后,需要令缓存失效,而不是更新缓存为数据库的最新值。
如果两个险些同时发出的请求分别要更新数据库中的值为 A 和 B,如果结果是 B 的更新晚于 A,那么数据库中的终极值是 B。但是,如果在数据库更新后去更新缓存,而不是令缓存失效,那么缓存中的数据就有可能是 A,而不是 B。因为数据库虽然是“更新为 A”在“更新为 B”之前发生,但如果不做特殊的跨存储系统的事件控制,缓存的更新顺序就未必会遵从“A 先于 B”这个规则,这就会导致这个缓存中的数据会是一个长期错误的值 A。
如果是更新缓存为数据库最新值,而不是令缓存失效,为什么会产生问题:
如果是令缓存失效,这个问题就消失了。因为 B 是后写入数据库的,那么在 B 写入数据库以后,无论是写入 B 的请求让缓存失效,还是并发的竞争情况下写入 A 的请求让缓存失效,缓存反正都是失效了。那么下一次的访问就会从数据库中取得最新的值,并写入缓存,这个值就一定是 B。
虽然说catch aside可以被称之为缓存使用的最佳实践,但与此同时,它引入了缓存的掷中率降低的问题,(每次都删除缓存自然导致更不容易掷中了),因此它更适用于对缓存掷中率要求并不是特别高的场景。如果要求较高的缓存掷中率,依然需要采用更新数据库后同时更新缓存的方案。在更新数据库后同时更新缓存,会在并发的场景下出现数据不同等,那我们该怎么规避呢?方案也有两种。
- 引入分布式锁。在更新缓存之前尝试获取锁,如果已经被占用就先阻塞住线程,等待其他线程开释锁后再尝试更新。但这会影响并发使用的性能。
设置较短缓存时间。设置较短的缓存过期时间可以或许使得数据不同等问题存在的时间也比力长,对业务的影响相对较小。但是与此同时,着实这也使得缓存掷中率降低,又回到了前面的问题里...
2.Read-Through
这种情况下缓存系统彻底变成了它身后数据库的代理,二者成为了一个团体,应用的请求访问只能看到缓存的返回数据,而数据库系统对它是透明的。
有的框架提供的内置缓存,例如一些 ORM 框架,就是按这种 Read-Through 和 Write-Through 来实现的。
数据获取计谋
- 应用向缓存要求数据;
- 如果缓存中有数据,返回给应用,应用再将数据返回;
- 如果没有,缓存查询数据库,并将结果写入自己;
- 缓存将数据返回给应用。
3.Write-Through
和 Read-Through 类似,但 Write-Through 是用来处理数据更新的场景。
数据更新计谋:
- 应用更新数据库成功;
- 如果缓存中有对应数据,先更新该数据;
- 缓存告知应用更新完成。
缓存系统需要自己内部保证并发场景下,缓存更新的顺序要和数据库更新的顺序同等。比如说,两个请求分别要把数据更新为 A 和 B,那么如果 B 后写入数据库,缓存中最后的结果也必须是 B。这个同等性可以用乐观锁等方式来保证。
4.Write-Back
对于 Write-Back 模式来说,更新使用发生的时候,数据写入缓存之后就立刻返回了,而数据库的更新异步完成。这种模式在一些分布式系统中很常见。
这种方式带来的最大利益是拥有最大的请求吞吐量,并且使用非常迅速,数据库的更新乃至可以批量进行,因而拥有杰出的更新效率以及稳固的速率,这个缓存就像是一个写入的缓冲,可以平滑访问尖峰。别的,对于存在数据库短时间无法访问的问题,它也可以或许很好地处
理。
但是它的弊端也很显着,异步更新一定会存在着不可避免的同等性问题,并且也存在着数据丢失的风险(数据写入缓存但还未入库时,如果宕机了,那么这些数据就丢失了)。
四、缓存方式
1.本地缓存/历程缓存(同一历程)
本地缓存的话是应用和缓存都在同一个历程内里,获取缓存数据的时候纯内存使用,没有额外的网络开销,速度非常快。它适用于缓存一些应用中根本不会厘革的数据,比如(国家、省份、都会等)。
本地缓存与业务系统耦合在一起,应用之间无法直接共享缓存的内容。需要每个应用节点单独的维护自己的缓存。每个节点都需要一份一样的缓存,对服务器内存造成一种浪费。本地缓存呆板重启、或者宕机都会丢失。
①同等性问题解决
第一种方案,可以通过单节点通知其他节点。如上图:写请求发生在server1,在修改完自己内存数据与数据库中的数据之后,可以主动通知其他server节点,也修改内存的数据。
这种方案的缺点是:同一功能的一个集群的多个节点,相互耦合在一起,特别是节点较多时,网状连接关系极其复杂。
第二种方案,可以通过MQ通知其他节点。如上图,写请求发生在server1,在修改完自己内存数据与数据库中的数据之后,给MQ发布数据厘革通知,其他server节点订阅MQ消息,也修改内存数据。这种方案虽然排除了节点之间的耦合,但引入了MQ,使得系统更加复杂。
前两种方案,节点数目越多,数据冗余份数越多,数据同时更新的原子性越难保证,同等性也就越难保证。
第三种方案,为了避免耦合,降低复杂性,干脆放弃了“及时同等性”,每个节点启动一个timer,定时从后端拉取最新的数据,更新内存缓存。在有节点更新后端数据,而其他节点通过timer更新数据之间,会读到脏数据。
②分类
- EhCache:需要持久化。使用持久化功能需要,缓存稳固,以免持久化的数据不准确影响结果。有集群解决方案。
- Guava cache:Guava cache说简单点就是一个支持LRU的ConCurrentHashMap,它没有Ehcache那么多的各种特性,只是提供了增、删、改、查、刷新规则和时效规则设定等最根本的元素。做一个jar包中的一个功能之一,Guava cache非常简洁并能满足觉大部门人的要求。
③什么时候使用——只管不用
情况一,只读数据,可以考虑在历程启动时加载到内存。
此时也可以把数据加载到redis / memcache,历程外缓存服务也能解决这类问题。
情况二,极其高并发的,如果透传后端压力极大的场景,可以考虑使用历程内缓存。
例如,秒杀业务,并发量极高,需要站点层挡住流量,可以使用内存缓存。
情况三,一定水平上答应数据不同等业务。
例如,有一些计数场景,运营场景,页面对数据同等性要求较低,可以考虑使用历程内页面缓存。
2.客户端缓存
- 页面缓存:页面自身对某些元素进行缓存、服务端将静态页面或者动态页面进行缓存给客户端使用
- 浏览器端缓存:将服务器的资源缓存到本地从而减轻服务器的负担,加快加载速度
- App缓存
3.服务端缓存(分布式缓存)
redis自然支持高可用,memcache要想要实现高可用,需要进行二次开发,不过缓存不一定需要实现高可用缓存场景,很多时候,是答应cache miss;缓存挂了,很多时候可以通过DB读取数据
①redis
有持久化需求或者对数据结构和处理有高级要求的应用
适用于对读写效率要求都很高,数据处理业务复杂和对安全性要求较高的系统(如新浪微博的计数和微博发布部门系统,对数据安全性、读写要求都很高)
②memcache
纯KV,数据量非常大,并发量非常大的业务,使用memcache或许更适合。更适合存储一些设置信息
动态系统中减轻数据库负载,提升性能;做缓存,适合多读少写,大数据量的情况,value最大1m(如大家网大量查询用户信息、好友信息、文章信息等)
③Tair
单节点的性能比力方面,redis是性能比tair高大概1/5
在分布式集群支持方面tair支持副本,支持多种集群结构,如:一机房一个集群、双 机房单集群单份、双机房独立集群、双机房单集群双份、双机房主备集群;
④底层实现机制
内存分配
memcache使用预分配内存池的方式管理内存,可以或许省去内存分配时间。
redis则是临时申请空间,可能导致碎片。
从这一点上,mc会更快一些。
虚拟内存使用
memcache把所有的数据存储在物理内存里。
redis有自己的VM机制,理论上可以或许存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上。
从这一点上,数据量大时,mc会更快一些。
网络模子
memcache使用非阻塞IO复用模子,redis也是使用非阻塞IO复用模子。
但由于redis还提供一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度。
从这一点上,由于redis提供的功能较多,mc会更快一些。
线程模子
memcache使用多线程,主线程监听,worker子线程担当请求,执行读写,这个过程中,可能存在锁冲突。
redis使用单线程,虽无锁冲突,但难以使用多核的特性提升团体吞吐量。
从这一点上,mc会快一些。
⑤对比选择
- 性能上:
- Memcached单个key-value大小有限,一个value最大只支持1MB,而Redis最大支持512MB。
- 在100k以上的数据中,Memcached性能要高于Redis。
- 内存空间和数据量大小:
- MemCached可以修改最大内存,采用LRU算法。
- Redis增长了VM的特性,突破了物理内存的限制。
- 使用便利上:
- MemCached数据结构单一,仅用来缓存数据。
- 而Redis支持更加丰富的数据范例,也可以在服务器端直接对数据进行丰富的使用,这样可以减少网络IO次数和数据体积。
- 可靠性上:
- MemCached不支持数据持久化,断电或重启后数据消失,但其稳固性是有保证的。
- Redis支持数据持久化和数据规复,答应单点故障,但是同时也会付出性能的代价。
- 应用场景:
- Memcached:动态系统中减轻数据库负载,提升性能;做缓存,适合多读少写,大数据量的情况(如大家网大量查询用户信息、好友信息、文章信息等)。
- Redis:适用于对读写效率要求都很高,数据处理业务复杂和对安全性要求较高的系统(如新浪微博的计数和微博发布部门系统,对数据安全性、读写要求都很高)。
4.缓存的误用
(1)把缓存作为服务与服务之间转达数据的媒介
- 服务1和服务2约定好key和value,通过缓存转达数据
- 服务1将数据写入缓存,服务2从缓存读取数据,达到两个服务通信的目标
该方案存在的问题是:
- 数据管道,数据通知场景,MQ更加适合
- 多个服务关联同一个缓存实例,会导致服务耦合
(2)使用缓存未考虑雪崩
- 服务先读缓存,缓存掷中则返回
- 缓存不掷中,再读数据库
提前做容量预估,如果缓存挂掉,数据库仍能扛住,才能执行上述方案。否则,就要进一步计划。
常见方案一:高可用缓存
使用高可用缓存集群,一个缓存实例挂掉后,可以或许自动做故障转移。
常见方案二:缓存水平切分
使用缓存水平切分,一个缓存实例挂掉后,不至于所有的流量都压到数据库上。
(3)调用方缓存数据
- 服务提供方缓存,向调用方屏蔽数据获取的复杂性(√)
- 服务调用方,也缓存一份数据,先读自己的缓存,再决定是否调用服务(×)
该方案存在的问题是:
- 调用方需要关注数据获取的复杂性
- 更严峻的,服务修改db里的数据,镌汰了服务cache之后,难以通知调用方镌汰其cache里的数据,从而导致数据不同等
- 或许服务可以通过MQ通知调用方镌汰数据,但是下游的服务不应该依靠上游的调用方
(4)多服务共用缓存实例
该方案存在的问题是:
- 不同服务对应的数据量,吞吐量不一样,共用一个实例容易导致一个服务把另一个服务的热数据挤出去
- 共用一个实例,会导致服务之间的耦合,与微服务架构的“数据库,缓存私有”的计划原则是相悖的
(5)将缓存当数据库
虽然一些缓存比如redis支持持久化,但其本质上仍旧是不稳固的,所以不能只将数据存储到缓存中。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |