1
前言
随着互联网从简单的单向浏览请求,发展为基于用户个性信息的定制化以及社交化的请求,这要求产品需要做到以用户和关系为基础,对海量数据进行分析和计算。对于后端服务来说,意味着用户的每次请求都需要查询用户的个人信息和大量的关系信息,此外大部分场景还需要对上述信息进行聚合、过滤、排序,最终才能返回给用户。
CPU是信息处理、程序运行的最终执行单元,如果它的世界也有“秒”的概念,假设它的时钟跳一下为一秒,那么在CPU(CPU的一个核心)眼中的时间概念是什么样的呢?
可见I/O的速度与CPU和内存相比是要差几个数量级的,如果数据全部从数据库获取,一次请求涉及多次数据库操作会大大增加响应时间,无法提供好的用户体验。
对于大型高并发场景下的Web应用,缓存更为重要,更高的缓存命中率就意味着更好的性能。缓存系统的引入,是提升系统响应时延、提升用户体验的唯一途径,良好的缓存架构设计也是高并发系统的基石。
缓存的思想基于以下几点:
引入缓存会给系统带来以下优势:
同样的,引入缓存也会带来以下劣势:
在缓存系统的设计架构中,还有很多坑,很多的明枪暗箭,如果设计不当会导致很多严重的后果。设计不当,轻则请求变慢、性能降低,重则会数据不一致、系统可用性降低,甚至会导致缓存雪崩,整个系统无法对外提供服务。
2
缓存的主要存储模式
三种模式各有优劣,适用于不同的业务场景,不存在最佳模式。
● Cache Aside(旁路缓存)
写: 更新db时,删除缓存,当下次读取数据库时,驱动缓存的更新。
读: 读的时候先读缓存,缓存未命中,那么就读数据库,并且将数据回种到缓存,同时返回相应结果
特点:懒加载思想,以数据库中的数据为准。在稍微复杂点的缓存场景,缓存都不简单是数据库中直接取出来的,可能还需要从其他表查询一些数据,然后进行一些复杂的运算,才能最终计算出值。这种存储模式适合于对数据一致性要求比较高的业务,或者是缓存数据更新比较复杂、代价比较高的业务。例如:一个缓存涉及多个表的多个字段,在1分钟内被修改了100次,但是这个缓存在1分钟内就被读取了1次。如果使用这种存储模式只删除缓存的话,那么1分钟内,这个缓存不过就重新计算一次而已,开销大幅度降低。
● Read/Write Through(读写穿透)
写: 缓存存在,更新数据库,缓存不存在,同时更新缓存和数据库
读: 缓存未命中,由缓存服务加载数据并且写入缓存
特点:
读写穿透对热数据友好,特别适合有冷热数据区分的场合。
1)简化应用程序代码
在缓存方法中,应用程序代码仍然很复杂,并且直接依赖于数据库,如果多个应用程序处理相同的数据,甚至会出现代码重复。读写穿透模式将一些数据访问代码从应用程序转移到缓存层,这极大地简化了应用程序并更清晰地抽象了数据库操作。
2)具有更好的读取可伸缩性
在多数情况下,缓存数据过期以后,多个并行用户线程最终会打到数据库,再加上数以百万计的缓存项和数千个并行用户请求,数据库上的负载会显著增加。读写穿透可以保证应用程序永远不会为这些缓存项访问数据库,这也可以让数据库负载保持在最小值。
3)具有更好的写性能
读写穿透模式可以让应用程序快速更新缓存并返回,之后它让缓存服务在后台更新数据库。当数据库写操作的执行速度不如缓存更新的速度快时,还可以指定限流机制,将数据库写操作安排在非高峰时间进行,减轻数据库的压力。
4)过期时自动刷新缓存
读写穿透模式允许缓存在过期时自动从数据库重新加载对象。这意味着应用程序不必在高峰时间访问数据库,因为最新数据总是在缓存中。
● Write Behind Caching(异步缓存写入)
写:只更新缓存,缓存服务异步更新数据库。
读:缓存未命中由封装好的缓存服务加载数据并且写入缓存。
特点:写性能最高,定期异步刷新数据库数据,数据丢失的概率大,适合写频率高,并且写操作需要合并的场景。使用异步缓存写入模式,数据的读取和更新通过缓存进行,与读写穿透模式不同,更新的数据并不会立即传到数据库。相反,在缓存服务中一旦进行更新操作,缓存服务就会跟踪脏记录列表,并定期将当前的脏记录集刷新到数据库中。作为额外的性能改善,缓存服务会合并这些脏记录,合并意味着如果相同的记录被更新,或者在缓冲区内被多次标记为脏数据,则只保证最后一次更新。对于那些值更新非常频繁,例如金融市场中的股票价格等场景,这种方式能够很大程度上改善性能。如果股票价格每秒钟变化 100 次,则意味着在 30 秒内会发生 30 x 100 次更新,合并将其减少至只有一次。
3
缓存7大经典问题
的常用解决方案
1缓存集中失效
缓存集中失效大多数情况出现在高并发的时候,如果大量的缓存数据集中在一个时间段失效,查询请求会打到数据库,数据库压力凸显。比如同一批火车票、飞机票,当可以售卖时,系统会一次性加载到缓存,并且过期时间设置为预先配置的固定时间,那过期时间到期后,系统就会因为热点数据的集中没有命中而出现性能变慢的情况。
解决方案:
2缓存穿透
缓存穿透是指一些异常访问,每次都去查询压根儿就不存在的key,导致每次请求都会打到数据库上去。例如查询不存在的用户,查询不存在的商品id。如果是用户偶尔错误输入,问题不大。但如果是一些特殊用户,控制一批肉鸡,持续的访问缓存不存在的key,会严重影响系统的性能,影响正常用户的访问,甚至可能会让数据库直接宕机。我们在设计系统时,通常只考虑正常的访问请求,所以这种情况往往容易被忽略。
解决方案:
3 缓存雪崩
缓存雪崩是缓存机器因为某种原因全部或者部分宕机,导致大量的数据落到数据库,最终把数据库打死。例如某个服务,恰好在请求高峰期间缓存服务宕机,本来打到缓存的请求,这是时候全部打到数据库,数据库扛不住在报警以后也会宕机,重启数据库以后,新的请求会再次把数据库打死。
解决方案:
4缓存数据不一致
同一份数据,既在缓存里又在数据库里,肯定会出现数据库与缓存中的数据不一致现象。如果引入多级缓存架构,缓存会存在多个副本,多个副本之间也会出现缓存不一致现象。缓存机器的带宽被打满,或者机房网络出现波动时,缓存更新失败,新数据没有写入缓存,就会导致缓存和 DB 的数据不一致。缓存 rehash 时,某个缓存机器反复异常,多次上下线,更新请求多次 rehash。这样,一份数据存在多个节点,且每次 rehash 只更新某个节点,导致一些缓存节点产生脏数据。再比如,数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改,数据库和缓存中的数据不一样了。
解决方案:
5竞争并发
当系统的线上流量特别大的时候,缓存中会出现数据并发竞争的现象。在高并发的场景下,如果缓存数据正好过期,各个并发请求之间又没有任何协调动作,这样并发请求就会打到数据库,对数据造成较大的压力,严重的可能会导致缓存“雪崩”。另外高并发竞争也会导致数据不一致问题,例如多个redis客户端同时set同一个key时,key最开始的值是1,本来按顺序修改为2,3,4,最后是4,但是顺序变成了4,3,2,最后变成了2。
解决方案:
分布式锁+时间戳
可以基于redis或者zookeeper实现一个分布式锁,当一个key被高并发访问时,让请求去抢锁。也可以引入消息中间件,把Redis.set操作放在消息队列中。总之,将并行读写改成串行读写的方式,从而来避免资源竞争。对于key的操作的顺序性问题,可以通过设置一个时间戳来解决。大部分场景下,要写入缓存的数据都是从数据库中查询出来的。在数据写入数据库时,可以维护一个时间戳字段,这样数据被查询出来时都会带一个时间戳。写缓存的时候,可以判断一下当前数据的时间戳是否比缓存里的数据的时间戳要新,这样就避免了旧数据对新数据的覆盖。
6热点Key问题
对于大多数互联网系统,数据是分冷热的,访问频率高的key被称为热key,比如热点新闻、热点的评论。而在突发事件发生时,瞬间会有大量用户去访问这个突发热点信息,这个突发热点信息所在的缓存节点由于超大流量而达到理网卡、带宽、CPU 的极限,从而导致缓存访问变慢、卡顿、甚至宕机。接下来数据请求到数据库,最终导致整个服务不可用。比如微博中数十万、数百万的用户同时去吃一个新瓜,秒杀、双11、618 、春节等线上促销活动,明星结婚、离婚、出轨这种特殊突发事件。
解决方案:
要解决这种极热 key 的问题,首先要找出这些 热key 。对于重要节假日、线上促销活动、凭借经验可以提前评估出可能的热 key 来。而对于突发事件,无法提前评估,可以通过 Spark或者Flink,进行流式计算,及时发现新发布的热点 key。而对于之前已发出的事情,逐步发酵成为热 key 的,则可以通过 Hadoop 进行离线跑批计算,找出最近历史数据中的高频热 key。还可以通过客户端进行统计或者上报。找到热 key 后,就有很多解决办法了。首先可以将这些热 key 进行分散处理。redis cluster有固定的16384个hash slot,对每个key计算CRC16值,然后对16384取模,可以获取key对应的hash slot。比如一个热 key 名字叫 hotkey,可以被分散为 hotkey#1、hotkey#2、hotkey#3,……hotkey#n,这 n 个 key 就会分散存在多个缓存节点,然后 client 端请求时,随机访问其中某个后缀的热key,这样就可以把热 key 的请求打散。
其次,也可以 key 的名字不变,对缓存提前进行多副本+多级结合的缓存架构设计。比如利用ehcache,或者一个HashMap都可以。在你发现热key以后,把热key加载到系统的JVM中,之后针对热key的请求,可以直接从jvm中获取数据。再次,如果热 key 较多,还可以通过监控体系对缓存的 SLA 实时监控,通过快速扩容来减少热 key 的冲击。
7大Key问题
有些时候开发人员设计不合理,在缓存中会形成特别大的对象,这些大对象会导致数据迁移卡顿,另外在内存分配方面,如果一个key特别大,当需要扩容时,会一次性申请更大的一块内存,这也会导致卡顿。如果大对象被删除,内存会被一次性回收,卡顿现象会再次发生。在平时的业务开发中,要尽量避免大key的产生。如果发现系统的缓存大起大落,极有可能是大key引起的,这就需要开发人员定位出大key的来源,然后进行相关的业务代码重构。Redis官方已经提供了相关的指令进行大key的扫描,可以直接使用。
解决方案:
希望今天的讲解对大家有所帮助,谢谢!
Thanks for reading!