缓存:存储在计算机上一个原始数据复制集,以便于访问。
1 缓存优缺点
1.1 缓存优点:加速读写,降低后端负载。
(1) 加速读写:因为缓存通常都是全内存的(例如Redis、Memcache),而存储层通常读写能力差,通过缓存的使用可以有效的加速读写,优化用户体验。
(2) 降低后端负载:缓存可以减少访问量和复杂计算(例如复杂的SQL语句),在很大程度降低后端的负载。
1.2 缓存缺点:数据不一致、代码维护成本和运维成本。
(1) 数据不一致:缓存层和存储层有一定时间窗口的不一致性。
(2) 代码维护成本和运维成本:加入缓存后,需要同时处缓存层和处理层的逻辑,增加了代码的复杂性,同样也增加了运维成本。
2 缓存穿透
缓存穿透:查询一个一定不存在的数据,由于缓存是不命中时需要从数据库查询,查不到数据则不写入缓存,这将导致不存在的数据每次请求都要到数据库去查询,失去了缓存保护后端存储的意义。
解决缓存穿透措施:缓存空对象和布隆过滤器。
2.1 缓存空对象
如果一个查询返回的数据为空,就将这个空结果进行缓存,设置5分钟的过期时间。
缓存空对象的存在的问题:
(1) 空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的方法就是针对这类数据设置一个较短的过期时间,让其自动剔除。
(2) 缓存层和存储层会有一段时间窗口的不一致,可能对业务有一定的影响。可以通过设置过期时间或者利用消息系统清除缓存中的空对象。
2.2 布隆过滤器
在访问缓存层和存储层之前,将存在的key用布隆过滤器提前保存起来,做第一层拦截。如果一个要查询的key在布隆过滤器中判断肯定不存在,则不会查询直接返回。
布隆过滤器基本原理及要点:位数组+k个独立的hash函数。
一个值通过多个哈希函数计算得到在数组中对应的位,并将该位的标识改为1。如果请求查询一个数,先通过哈希函数计算,如果得到的位存在标识有0,说明这个值肯定不存在,这样可以避免不必要的数据库查询。
如下图,如果有另外一个key,经3个hash函数算出的位分别是2,4,6,因为6位对应的值为0,说明存储层中肯定不存在这样的key,所以直接返回,不用在继续向下执行了。
适用:利用布隆过滤器减少磁盘IO或网络请求,因为一旦一个值必定不存在的话,就可以不用进行后续昂贵的查询操作。
优点:查询快,占用空间小。
缺点:存在误判,删除困难。
如何减少布隆过滤器的误判:增大位数组的长度,增加哈希函数的数量。
3 缓存雪崩
缓存雪崩:缓存层由于某些原因整体crash掉,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃。
3.1 缓存雪崩发生的原因:
(1) Redis服务器宕机,所有的请求都走存储层。
(2) 对设置过期时间的数据,在某段时间内大量的数据同时失效。对于这种情况可以在缓存的时候加上一个随机值,这样会大幅度减少缓存在同一时间过期。
3.2 缓存雪崩解决方法:保证缓存层服务高可用性、熔断、限流和降级。
(1) 保证缓存层服务高可用性。如主从架构、Sentinel或者Redis Cluster都实现了高可用。
(2) 降级、熔断和限流
降级:服务降级是当服务器压力剧增的情况下,根据当前业务情况及流量对一些服务和页面有策略的降级,以此释放服务器资源以保证核心任务的正常运行。
限流:限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。
熔断:一般是指软件系统中,由于某些原因使得服务出现了过载现象,为防止造成整个系统故障,从而采用的一种保护措施,所以很多地方把熔断亦称为过载保护。很多时候刚开始可能只是系统出现了局部的、小规模的故障,然而由于种种原因,故障影响的范围越来越大,最终导致了全局性的后果。
4 热点key重建优化
如果一个key是热点key,并发量非常大,当这个key缓存失效,可能是一个复杂的SQL、多次IO等造成重建缓存不能短时间完成,在失效的瞬间,有大量的线程来重建缓存,造成后端负载加大。
常见的优化方案:互斥锁(mutex key)、永远不过期
4.1 互斥锁
这种解决方案比较简单,就是让一个线程构建缓存,其他线程等待构建缓存的线程完成,重新从缓存中获取数据。
这种方案的优缺点很明显。优点是实现简单,后端负载小。缺点阻塞其他线程,造成等待。
4.2 永远不过期
(1) 从缓存层上看,没有设置过期时间,所以不会出现热点key过期后产生的问题。
(2) 从功能层上看,为每个value设置一个逻辑过期时间,当发现超过逻辑过期时间后,会使用单独的线程区构建线程。
这种方案的优缺点也很明显。优点是杜绝了热点key过期的问题。缺点是在重建缓存期间,会出现数据不一致的情况,这取决于业务是否能容忍这种不一致。
4.3 两种方案的比较
互斥锁:思路简单,能够较好的保证一致性。但是存在死锁和线程阻塞等问题。
永远不过期:没有热点key产生的一系列问题,但是会存在数据不一致的情况,同时代码复杂度增大。
5 无底洞优化
无底洞问题:随着缓存的数据量增大,为了满足业务需求需要增加节点,但是增加节点没有让性能好转反而降低的现象。
5.1 无底洞问题的原因
键值数据库由于通常采用的是哈希函数将key映射到各个节点上,造成key的分布和业务无关,但是由于数据量的持续增加,造成需要添加节点做水平扩容,导致键值分布到更多的节点上。所以在批量操作时需要从不同的节点上获取,相比于单机批量操作只涉及一次网络操作,分布式批量操作会涉及多次网络操作。
无底洞优化思路
(1) 命令本身优化,如优化SQL语句等。
(2) 减少网络通信次数。
(3) 降低接入成本。
下面从减少网络通信次数来优化:
以Redis批量获取n个字符串为例,有三种实现方法:
(1) 客户端n次get:n次网络 +n次get命令本身。
(2) 客户端1次pipeline get:1次网络 + n次get命令本身。
(3) 客户端1次mget:1次网络 + 1次mget命令。
5.2 串行命令
由于n个key是比较均匀地分布在Redis Cluster的各个节点,所以最简单的方法就是逐次执行n个get命令。这种操作时间复杂度较高,它的操作时间 = n次网络时间+n次命令时间。
5.3 串行IO
Redis Cluster使用CRC16算法计算出散列值,再取16383的余数就可以计算出slot值,可以根据这些将属于同一个节点的key进行归档,得到每个节点的key子列表,之后对每个节点执行mget或者pipeline操作,它的操作时间 = node次网络时间 + n次命令时间,网络次数是node的个数。
这是对串行命令的优化,但是节点数太多,还是有一定的性能问题。
5.4 并行IO
此方案是串行IO的改进方案,网络次数虽然还是节点个数,在mget命令执行时使用多线程,使用多线程网络时间变为O(1)。
5.5 hash_tag实现
由于Redis Cluster中计算slot的算法,可以使用hash_tag将多个key强制分配到一个节点上,它的操作时间 = 1次网络时间 + n次命令时间。
如图所示,所有的key都在Redis 2节点。
5.6 四种批量操作解决方案对比
(1) 串行命令
优点:编程简单。
缺点:大量keys请求延迟严重。
网络IO:O(keys)。
(2) 串行IO
优点:编程简单,是串行命令的改进版。
缺点:如果有大量nodes延迟严重。
网络IO:O(nodes)。
(3) 并行IO
优点:利用并行特性,延迟取决于最慢的节点。
缺点:编程复杂,由于多线程,问题定位比较难。
网络IO:O(max_slow(nodes))
(4) hash_tag
优点:性能最高。
缺点:容易造成数据倾斜。
网络IO:O(1)。
本文完
注:本文参考《Redis开发与运维》,如发现错误,请指正!