分布式系统之缓存的微观应用经验谈(四) 【交互场景篇】 前端
前言
近几个月一直在忙些杂事,几乎年后都没怎么闲过。忙忙碌碌中就进入了2018年的秋天了,不得不感叹时间老是如白驹过隙,也不知道收获了什么和失去了什么。最近稍微休息,买了两本与技术无关的书,其一是 Yann Martel 写的《The High Mountains of Portugal》(葡萄牙的高山),发现阅读此书是须要一些耐心的,对人生暗喻很深,也有足够的留白,有兴趣的朋友能够细品下。好了,下面回归正题,尝试写写工做中缓存技术相关的一些实战经验和思考。
正文
在分布式Web程序设计中,解决高并发以及内部解耦的关键技术离不开缓存和队列,而缓存角色相似计算机硬件中CPU的各级缓存。现在的业务规模稍大的互联网项目,即便在最初beta版的开发上,都会进行预留设计。可是在诸多应用场景里,也带来了某些高成本的技术问题,须要细致权衡。本系列主要围绕分布式系统中服务端缓存相关技术,也会结合朋友间的探讨说起本身的思考细节。文中如有不妥之处,恳请指正。
为了方便独立成文,原谅在内容排版上的一点点我的强迫症。
第四篇打算做为系列最后一篇,这里尝试谈谈缓存的一些并发交互场景,包括与数据库(特指 RDBMS)交互,和一些独立的高并发场景相关补充处理方案(若涉及具体应用一样将主要以Redis举例)。
另见:分布式系统之缓存的微观应用经验谈(三)(数据分片和集群篇)数据库
(https://www.cnblogs.com/bsfz/)
(https://yq.aliyun.com/u/autumnbing)
1、简单谈下缓存和数据库的交互流程
为了便于后面的相关讨论,这里约定文中的数据库(Database)均指传统的 RDBMS,使用DB标识,同时需区别于缓存(Cache)里的DB划分空间。
我在早前一篇缓存设计细节的文章里,有阐述关于 Cache 自身 CURD 时的一些具体细节,而这里将结合DB,就 DB 和 Cache 之间的并行 CURD 操做进行一些讨论。固然,这里面在交互层面上是必定会涉及到分布式事务(Distributed Transaction)相关的一致性话题,但为了不表述出现模糊和没必要要的边界放大,这里我尽量剥离开来,专一在基于 Cache 的处理上。
预先抽象这样一个基础场景:后端
DB中存在一张资金关联表(FT),这里 FT 里存储的都是热点条目(属于极高频访问数据),在系统设计时,FT里的数据将与对应的 Cache 服务 C1 进行关联存储(这里仅指一级缓存),以达到提高必定的并发查询性能。缓存
1.1 向 FT 中新增(Create)一条数据
经过 SQL 向 FT中插入一条数据:若是插入失败,则不须要对 C1有任何操做;若是插入成功,则此时须要判断,考虑是否在 C1中同步插入。
这种情景通常比较简单,若是没有特别的状况,此刻不需对 C1 作主动插入,而是后续被动插入(后面会提到)。可是若是插入 FT 中的数据日后操做只有删除这个动做,而且 FT的数据常常被批量操做,那么我的建议同步执行对 C1的插入操做。
(PS:这里也顺便申明下,若是须要往C1插入,但插入失败,请根据业务场景加入重试机制,后面对Cache的操做均包含这个潜在的动做。至于重试处理失败的状况,如往C1插入一条数据,我的建议是再也不过分处理,最终默认是总体操做成功,并进行对应状态返回。这里注意不要与分布式事务的一致性进行混合类比,后面再也不赘述。)
架构
1.2 准备更新(Update)一条数据
当须要更新 FT 中的一条数据时,意味着以前 C1 中的数据已经无效,而在一个高并发环境中这里没法作到统一的直接更新 C1。首先就须要考虑的是 C1 的数据是主动更新仍是被动更新,主动更新即更新完 FT后,同时将数据覆盖进 C1,而被动更新指的是更新完 FT 后,当即淘汰 C1 中的数据,并等待下次查询时从新写入C1。
只要上述请求动做出现了任何并发,好比两个相同动做,动做1和动做2同时发生请求,那么会出现一个不一致的问题:动做1先操做 FT,动做2后操做 FT,而后动做2先操做了C1,动做1后操做了C1。
这样存在不止一个线程并发的更新 FT 数据时,没法确认更新 FT 的顺序和最终更新 C1 的顺序是否保持一致,结果是必定会出现大量 FT 和 C1 中数据出现幻读,而这个在存在主从Cache的状况下这种几率会大大提高(可参见上一章主从复制的部分)。推荐的方式是,若是不考虑Cache 屡次须要重写的损耗,在没有其余特殊要求下,能够直接淘汰 C1 中的数据,也额外照顾到了Cache在合适的时候彻底命中(Hit)。
其实到这里还没结束,当决定是淘汰 C1 的数据,那么就要选择一个淘汰时机:一种是先更新 FT,而后对C1 执行淘汰;一种则是,先对 C1 执行淘汰,而后才更新FT。
虽然两种方式都有合适的场景,但这里须要权衡一种几率性问题:当对C1执行淘汰时,又并发了一个对C1的查询操做,此时,C1会从DB拉取数据从新写入,那么C1中即为脏数据,当并发越大,存在数据一直“脏”下去的几率更大。因此,这里更推荐的作法是选择前者。
(注意,这里还有一些去讨论的细节并不打算在此话题延伸,好比关于 C1和FT之间的原子性问题,是否能够采用二阶段/三阶段提交等模拟事务方式和对业务形成的影响。)
并发
1.3 开始读取(Read)一条数据
这里就没有太多特别,毕竟应用Cache 的目的就已经说明了读取数据时,只须要遵循“先读Cache再读DB”。即先从C1里拿取数据,若是C1里不存在该数据,则从FT中搜索,搜索完成若是依然不存在该数据,则直接返回Empty状态。若是存在,则同时将该数据保存进C1中,并返回对应状态。
顺带提一下,可能有人会说,在某些场景下,即便 C1中有数据,也要先从 FT里优先获取。我赞同,没错,但注意这里不要混淆讨论的主题了,这本质是属于基于一种业务结果的导向,就相似在传统 RDBMS 读写分离状况下,在关键数据的验证处,直接请求主库获取并操做。因此上面说的其实并无矛盾,咱们讨论时要明确清晰,不要混淆。 运维
1.4 从FT 中删除(Delete)一条数据
与Create相反的操做,经过 SQL 向 FT中移除一条数据:若是移除失败,则不须要对 C1 有任何操做,如删除成功,则将对应C1中数据移除(另外请类比1.2中的一些细节)。
分布式
2、谈谈缓存的穿透雪崩等相关问题 高并发
在项目发展到后期,一些业务场景总体都处于高并发状态,大量QPS对总体业务的负载要求很高,为了不不少时候脱离架构优化的初衷,还须要在项目中作到不少预先性的规避和细节把控。
性能
2.1 优化防止缓存击穿
当请求发来的查询 Key 在 Cache 中存在,但某一时刻数据过时了,而且此时出现了大量并发请求,那么这里由于 Cache 中 Miss,就会统一去 DB 中搜索,直接形成在很短的时间内,DB 的 QPS 压力会陡增。
对于这种问题的预防和优化,每每从两方面入手:一是程序中加小粒度的锁/信号(去年有写过一篇关于商城系统里库存并发管控杂记,里面有具体话题的细节扩展,详见:https://www.cnblogs.com/bsfz/ );二是将 DB的读取延迟 和 Cache的写入时间尽量拉到最低;三是对其中过于热点的数据采起一个较大的过时时间并作必定的随机性(这里非必要,可自行权衡)。其实还有一点,少数状况下,可根据场景是否限制,能够增长适当的到期自动刷新的策略,这里也能够考虑在程序中开启固定的线程通知维护。
2.2 预防大量缓存穿透
当请求发来的查询 Key 在 Cache 中 Miss,天然就会去 DB 里搜索,这里自己没问题,可是假如查询的 Key 在 DB 中也不存在,那么意味着每次请求实际上都是实打实落在了 DB 上。这种问题比较常见,而且即便并发不是很大的时候 DB 的链接数也轻松达到上限,并且自己也不符合咱们设计为了提升QPS的初衷。
对于这种漏洞性问题的解决方式,一样能够从两方面入手:一是程序能够在第一次从DB搜索数据为 NULL 的时候,直接将 NULL 或者一个标识符 Sign 缓存起来,同时我的建议尽可能设置一个小范围的随机过时时间,避免没必要要的长期内存占用;二是程序里限制过滤一些不可能存在的数据KEY,如借鉴 Bloom filter 思想,特别是在前端请求到后端的这里,尽可能进行一次中间判断处理(若有时对不合法KEY直接返回NULL)。
2.3 控制缓存雪崩
这里会有某些细节和上面相似,但不彻底。当Cache出现不可用,再或者大量数据同一场景里同一时刻失效,批量请求直接访问DB,而且此刻也等同于没有任何Cache措施了。
为了规避这种偏极端的问题,主要能够考虑从三个方面入手:一是增长完善Cache 的高可用机制,并最好有单独的运维监控预警;二是相似上面针对Cache的时间再次做随机,特别是包含预热和批量的场景里。(ps:你看不少地方都有相似设计来下降必定几率,我的在设计时,即便是项目初期阶段的简化版本里也会包含进去。);三是,在部分场景增长多级Cache,可是在不少时候会增长其余的问题(如多级以前的同步问题),因此我的推荐优先增长到二级便可,而后稍微调整下时间尽可能不高于一级Cache。
结语
因为我的能力和经验均有限,本身也在持续学习和实践,文中如有不妥之处,恳请指正。 本系列告一段落,正好也要去忙一些事情,暂时可能不写相关的东西了。
我的目前备用地址:
社区1:https://yq.aliyun.com/u/autumnbing
社区2:https://www.cnblogs.com/bsfz/
End.