▌1:极简缓存架构
通过JSR107规范,我们将框架定义为客户端层、缓存提供层、缓存管理层、缓存存储层。其中缓存存储层又分为基本存储层、LRU存储层和Weak存储层,如下图所示。
缓存分层图
其中:
- 客户端层:使用者直接通过该层与数据进行交互。
- 缓存提供层:主要对缓存管理层的生命周期进行维护,负责缓存管理层的创建,保存、获取以及销毁。
- 缓存管理层:主要对缓存客户端的生命周期进行维护,负责缓存客户端的创建,保存、获取以及销毁
- 缓存存储层:负责数据以什么样的形式进行存储。
- 基本存储层:是以普通的ConcurrentHashMap为存储核心,数据不淘汰。
- LRU存储层:是以最近最少用为原则进行的数据存储和缓存淘汰机制。
- Weak存储层:是以弱引用为原则的数据存储和缓存淘汰机制。
▌2:容量评估
缓存系统主要消耗的是服务器的内存,因此,在使用缓存时必须先对应用需要缓存的数据大小进行评估,包括缓存的数据结构、缓存大小、缓存数量、缓存的失效时间,然后根据业务情况自行推算在未来一定时间内的容量的使用情况,根据容量评估的结果来申请和分配缓存资源,否则会造成资源浪费或者缓存空间不够。
▌3:业务分离
建议将使用缓存的业务进行分离,核心业务和非核心业务使用不同的缓存实例,从物理上进行隔离,如果有条件,则请对每个业务使用单独的实例或者集群,以减小应用之间互相影响的可能性。笔者就经常听说有的公司应用了共享缓存,造成缓存数据被覆盖以及缓存数据错乱的线上事故。
▌4:监控为王
所有的缓存实例都需要添加监控,这是非常重要的,我们需要对慢查询、大对象、内存使用情况做可靠的监控。
▌5:失效时间
任何缓存的key都必须设定缓存失效时间,且失效时间不能集中在某一点,否则会导致缓存占满内存或者缓存雪崩。
▌6:大量key同时失效时间的危害
在使用缓存时需要进行缓存设计,要充分考虑如何避免常见的缓存穿透、缓存雪崩、缓存并发等问题,尤其是对于高并发的缓存使用,需要对key的过期时间进行随机设置,例如,将过期时间设置为10秒+random(2),也就是将过期时间随机设置成10~12秒。
笔者曾经见过一个case:在应用程序中对使用的大量缓存key设置了同一个固定的失效时间,当缓存失效时,会造成在一段时间内同时访问数据库,造成数据库的压力较大。
▌7:先更新数据库后更新缓存有啥问题?
想象一下,如果两个线程同时执行更新操作,线程1更新数据库后,线程2也更新了数据库,然后开始写缓存,但线程2先执行了更新缓存的操作,而线程1在执行更新缓存的时候就把线程2更新的数据给覆盖掉了,这样就会出现数据不一致。
▌8:先删缓存, 行不行?
“先删缓存,然后执行数据库事务”也有人讨论这种方案,不过这种操作对于如商品这种查询非常频繁的业务不适用,因为在你删缓存的同时,已经有另一个系统来读缓存了,此时事务还没有提交。当然对于如用户维度的业务是可以考虑的。
▌9:数据库和缓存数据一致性
京东采用了通过canal更新缓存原子性的方法,如下图所示。
最终一致性方案
几个关注点:
❑ 更新数据时使用更新时间戳或者版本对比。
❑ 使用如canal订阅数据库binlog;此处把mysql看成发布者,binlog是发布的内容,canal(canal 是阿里巴巴mysql数据库binlog的增量订阅&消费组件)看成消费者,canal订阅binlog然后更新到Redis。
将更新请求按照相应的规则分散到多个队列,然后每个队列的进行单线程更新,更新时拉取最新的数据保存;更新之前获取相关的锁再进行更新。
▌10.先更新数据库,再删除缓存的一种实践
流程如下图所示:
过程不赘述,只强调一个,数据库update变更会同步发到消息,通过消息去删除缓存。如果删除失败,消息有重试机制保障。另外除了极端情况,缓存更新是比较及时的。
▌11:本地缓存的挑战
如果对性能的要求不是非常高,则尽量使用分布式缓存,而不要使用本地缓存,因为本地缓存在服务的各个节点之间复制,在某一时刻副本之间是不一致的,如果这个缓存代表的是开关,而且分布式系统中的请求有可能会重复,就会导致重复的请求走到两个节点,一个节点的开关是开,一个节点的开关是关,如果请求处理没有做到幂等,就会造成处理重复,在严重情况下会造成资金损失。
▌12:缓存热点与多级缓存
对于分布式缓存,我们需要在Nginx+Lua应用中进行应用缓存来减少Redis集群的访问冲击;即首先查询应用本地缓存,如果命中则直接缓存,如果没有命中则接着查询Redis集群、回源到Tomcat;然后将数据缓存到应用本地。如同14-8所示。