[框架]缓存设计
second60 20181031
目录
背景:本文基于redis缓存数据库
1 . 缓存层目的
1.1缓存能够加速读写速度
通常数据都在数据库,或读取本地文件,都是在磁盘中,速度相比内存,是非常慢的。缓存是在内存中,速度比内存快N倍。
至于快多少倍,引用google 工程师Jeff Dean 首先在他关于分布式系统的ppt文档列出来的,到处被引用的很多。
1纳秒等于10亿分之一秒,= 10 ^ -9 秒
-----------------------------------------------------------
Numbers Everyone Should Know
L1 cache reference 读取CPU的一级缓存 | 0.5 ns |
Branch mispredict(转移、分支预测) | 5 ns |
L2 cache reference 读取CPU的二级缓存 | 7 ns |
Mutex lock/unlock 互斥锁\解锁 | 100 ns |
Main memory reference 读取内存数据 | 100 ns |
Compress 1K bytes with Zippy 1k字节压缩 | 10 us |
Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节 | 20 us |
Read 1 MB sequentially from memory 从内存顺序读取1MB | 250 us |
Round trip within same datacenter 从一个数据中心往返一次,ping一下 | 500 us |
Disk seek 磁盘搜索 | 10 ms |
Read 1 MB sequentially from network 从网络上顺序读取1兆的数据 | 10 ms |
Read 1 MB sequentially from disk 从磁盘里面读出1MB | 30 ms |
Send packet CA->Netherlands->CA 一个包的一次远程访问 | 150 ms |
我们关注一下内存 和 磁盘的访问速度, 随机访问相差 10ms/ 100ns =10 000 000 ns/100ns = 1000 00倍
总结下,速度顺度:
CPU > register > L1 cache > L2 cache > .. > Ln Cache > RAM(内存) > 磁盘
内存的速度远远大于磁盘,这也是缓存层产生的最主要的原因。
1.2 降低后端负载
加入缓存后,可以大大地减少数据库的访问,从而避免高并发对数据库来的压力,无论是读写次数都会大大地减少,同时增强了应用后台的承载能力。
2 缓存层成本
2.1有无缓存架构对比
左边的是无缓存的架构
右边的是有缓存架构
缓存架构: 即在访问存储层中间,添加一个缓存层,当业务层需要访问存储层时,会先去缓存层查找,如果查到(缓存命中)立即返回业务层,如果无缓冲(缓存未命中),才去存储层查找,如果查找(存储层命中),则写入缓存层,并返回给业务层。如果未查找到,直接通知业务层。
2.2 成本
添加任何功能,都是需要成本的,缓存层也一样,但添加后的用途远远大于成本,那么就是可行的。下面分析下添加缓存后的成本。
2.2.1 数据不一致
因为缓存层和存储层的数据存在一定的更新时间差,所以会在时间窗口内存在不一致性,时间窗口跟更新策略有关。
例:当业务要更新数据时,如果先更新缓存层,再更新存储层,那么在更新缓存层后,更新存储层前,会存在数据的不一致。
2.2.2 代码维护成本
加入了缓存后,需要同时处理缓存层和存储层的逻辑,增大开发维护成本
2.2.3 运维成本
缓存层,可以是单一模式,也可能主从模式,或者是集群模式,无论是哪种植模式,都会要部署,维护,同步,故障处理,恢复等,增加了运维成本。
3 缓存的使用场景
3.1 加速请求响应,减少查数据库时间,优化高并发
操作数据库,一般都是有限制的,连接数限制,网络的限制,磁盘速度的限制,即使一个简单的查询,添加了字典,数据库缓存,但速度也是比不上直接从内存中读取。
当数据库可能会成为整体框架的瓶颈时,就要考虑是否添加缓存层,减轻存储层的压力,提高效率。
3.2 开销大的复杂计算分离
1. 如果操作数据库的计算非常复杂,非常耗性能,可以考虑把复杂的计算用缓存来处理,减少数据库的负担。比如:一个SQL关联很多表,很多分组等计算,可以通过缓存层来计算并缓存结果等
2. 如果业务逻辑涉及的处理非常耗时,比如:计算排行榜,可能要从所有数据中选出TOP100数据,是非常耗时的,或者做一个实时排行榜,可以直接用redis的sorted_set实现,效率非常快。
3.3 通用缓存
1. 在分布式框架中,要缓存公共数据,多个进程或服务共用等,考虑用缓存,避免用网络直接发送读写。
2. 在缓存中,一般都是统一格式,如redis中统一用json等,避免多服务序列化问题。
4 缓存更新策略
缓存中的数据和存储层中的数据可能存在不一致问题,所以需要某些策略进行更新或删除。
4.1 LRU/LFU/FIFO算法剔除
当缓存数据量大于预设的缓存大小时,需要对一些数据进行删除处理。
通常方法有:
LRU - 最近最久未使用优先删除
LFU - 最不经常使用优先删除
FIFO - 先进先出方法删除
4.2 超时删除
给缓存数据设置一个过期时间,在过期时间后,系统自动删除过期数据。
4.3 主动更新
对数据一致性要求高,需要在操作后,立即更新数据到缓存和存储层。
4.4 总结
低一致性:建议配置最大内存和淘汰策略
高一致性业务:使用超时剔除和主动更新
5 缓存粒度
添加了缓存层,缓存是在内存当中,所以他的容量是一定的。但相对于,数据库而言,可能数据量非常大,不可能全部加载到缓存层。(当然数据小的时候可以)因此,要考虑缓存内容维度的问题,也叫缓存粒度。
比如:很大的数据表,当缓存时,你是缓存整条记录,还是只缓存用到的某几个字段。
select * from user where id={id}
全部缓存到redis中
set user:{id} ‘select * from user where id={id}’
缓存重要的列
set user:{id} ‘select uid,username,..... from user where id = {id}’
数据类型 | 通用性 | 占用空间(内存+网络) | 代码维护 |
全部缓存 | 高 | 大 | 简单 |
部份缓存 | 低 | 小 | 复杂 |
6 缓存优化
6.1 缓存穿透优化
什么是缓存穿透?
缓存穿透指查询一个不存在的数据,缓存层和存储层都不命中(穿透)。
6.1.1 缓存穿透的问题
缓存穿透,意味着很次还是要去存储层查找,失去了缓存层的意义,同时加大了后端的负载(即要查缓存层,又要查偏存储层),如果出现大量的穿透, 甚至可能造成存储层瓶颈或宕机。
6.1.2 缓存穿透的原因
1. 自身业务代码出现问题
正常来说不应该出现穿透存储层问题。大部份数据有效性检查,都应在逻辑入口处检查并处理完,当然也有个别情况。所以业务出现不正常的批量穿透,应当查出问题原因。
2.恶意攻击/爬虫等大量空命中。
某些接口被恶意循环攻击,一般出现这问题,业务代码是一方面,别一方面,应该从入口处来限制或禁用攻击。
6.1.3 如何处理缓存穿透
1 对自身业务逻辑的入口限制
当出现缓存命中率低,存储命中率低时,先对出问题的业务进行查找出原因,然后对入口出进行限制。如果限制不了,再用下面的方法。
2 缓存空对象
当缓存未命中,存储层也未命中时,我们把空对象(可能是一个查询id),缓存到缓存层,下次再缓存中查找时,直接返回空对象。避免了再查存储层。
缓存空对象产生的问题:
- 空对象做了缓存,占用了内存空间(如果是攻击,问题更严重),解决方法就是设置过期时间。
- 缓存层和存储层会有一段时间数据不一致,可能会对业务有影响。但可以代码主动存储来避免。
3 布隆过滤器拦截(https://en.wikipedia.org/wiki/Bloom_filter)
一开始,大家都可能不知道啥叫布隆过滤器,其实就是布隆想出来的一个想法。就是预先把所有的key存储起来,当作一个过滤器,过滤器有的就通行,没有的就不存在。
布隆过滤器怎么存?
可以用位的方式,每位表示一个id,存储到一段预分配的内存中。可以定时更新布隆过滤器,也可以实时更新。
如:利用Redis的Bitmaps实现布隆过滤器
适用于数据命中不高,数据相对固定,实时性代的场景,代码维护复杂,但缓存空间小。
解决缓存穿透 | 适用场景 | 维护成本 |
缓存空对象 | 数据命中不高 数据频繁变化实时性高 | 代码简单 需过多缓存空间 数据不一致 |
布隆过滤器 | 数据命中不高 数据相对固定实时性低 | 代码维护复杂 缓存空间小 |
6.2 缓存的无底洞现象
什么是缓存的无底洞呢?
当缓存节点达到一定量后,继续增加,会发现性能不但没有好转反而降了,称为无底洞现象。(主要针对集群场景)
6.2.1 无底洞现象的原因
在集群环境中,key都是分散到各个节点,但由于数据量和访问量的增长,大量节点做水平扩容,导致键值分布到更多不同的节点,当批量操作时,可能从不同节点上获取信息。
在单节点情况下,时间 = 一次网络往返 + 一次存取的时间
但多节点情况存取的时候,时间 = 多次网络往返 + 多次存取时间
由上面分析,无底洞现象,存在于集群多节点环境,批量操作中。涉及节点越多,效率就会越慢。节点多不代表高性能,投入越多不一定产出越多,也就是所谓的无底洞。
6.2.2 无底洞优化
从上面可以看出,优化无非两方面:网络往返次数和节点操作次数。减少网络往返次数和节点操作次数,可以提高效率。
结合redis cluster说明
1. 串行命令(多次get, mget)
假如要得到n个key,分别在m个节点上,那么
操作时间 = n*网络往返 + n *命令时间
缺点:效率最差
2. 串行IO
假如要得到n个key, 分别在m个分点上,那么,可以先计算出key分别在哪几个节点上。再用mget或pipeline操作。
操作时间 = node次网络往返 + n次命令时间
缺点:效率差
比1优.
3. 并行IO
改用多线程执行,网络操作时间为O(1)
操作时间= 1次最慢节点网络返回 + n次命令
缺点:代码复杂
4. hast_tag实现
采用hash_tag,把多个key存储在一个节点上,取数据时,相当于在单节点操作。
操作时间 = 1次网络 + n次命令时间
优点:效率最好
缺点:可能造成分布不均问题
6.2.3 方案对比
方案 | 优点 | 缺点 | 网络IO |
串行命令 | 编程简单 如果keys少,性能可满足 | 大量keys请求延迟严重 | O(keys) |
串行IO | 编程简单 少量节点,性能能满足 | 大量node延迟严重 | O(nodes) |
并行IO | 利用并行特性,延迟取决非于最慢的决点 | 编程复杂 多线程,定位难 | O(max_slow(nodes)) |
hash_tag | 性能最高 | 业务维护成本较高 容易数据倾斜 |
|
6.3 缓存雪崩
什么是缓存雪崩?
由于缓存层承载着大量的请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务(如很多key同时过期,缓存服务失效),于是所有请求都会到达存储层,存储层调用暴增,可能会造成存储层宕机情况。称为缓存雪崩!!指缓存层失效后,流量会像奔逃的野牛一样,打向后端存储。
6.3.1 缓存雪崩优化
1 保证缓存层的高可用性。
缓存层宕机,那么是非常可怕的,一定要避免。如:机房宕掉,网络不络问题等。
redis中提供了sentinel(哨兵) -可以临视和切换主从
提供了cluster(集群)-水平扩展和故障迁移
2 依赖隔离组件为后端限流并降级。
当故障出现时,可以降级采用替代方案处理。
当故障出现时,可以限流处理,限定最大处理的峰值。即使业务处理慢,也不会使整个业务不可用。
3 提前演练
在项目上线前,演练缓存层宕掉后,后端的负载以及可能出现的问题,在此基础上做一些预案。(A方案不可用时采用B计划)
6.4 热点key
热点key 即是一个使用非常频繁的key, 并发量非常大。
通常会用 缓存 + 过期时间 的策略来加速数据的读写,又保证数据的定期更新。
6.4.1 热点key问题
当一个热点key某一时间失效期间,如果大量并发,将会造成后端负载的一时瓶颈甚至崩溃。
重建热点key又不能在短时间内完成,可能一个复杂的计算,耗时上分数。
6.4.2 热点key解决
减少重建缓存的次数
数据尽可能一致
1 重建热点key时,加互斥锁
2 永久热点key,永不过期
a 从缓存层看,无过期时间
b 从功能层看,key过期了,会重建。但重建过程中,会数据不一致。
c 独立逻辑去更新过期的key,不是业务里去重建。