26 缓存异样
缓存雪崩、缓存击穿和缓存穿透,这三个问题一旦产生,会导致大量的申请积压到数据库层,导致数据库宕机或故障。
缓存雪崩
缓存雪崩是指大量的利用申请无奈在 Redis 缓存中进行解决,紧接着,利用将大量申请发送到数据库层,导致数据库层的压力激增。
如何发现:
监测 Redis 缓存所在机器和数据库所在机器的负载指标,例如每秒申请数、CPU 利用率、内存利用率等。如果咱们发现 Redis 缓存实例宕机了,而数据库所在机器的负载压力忽然减少(例如每秒申请数激增),此时,就产生缓存雪崩了。
起因一:大量数据同时过期。
解决方案:
- 过期工夫加随机数
- 服务降级:暂停非核心业务拜访,间接返回预约义信息;外围数据容许持续查问
起因二:Redis 实例宕机
解决方案:
-
服务熔断或限流:
- 服务熔断:暂停拜访,间接返回
- 限流:申请入口设置每秒申请数量,超出间接回绝
- 配置高可用集群
缓存击穿
缓存击穿是指,针对某个拜访十分频繁的热点数据的申请,无奈在缓存中进行解决,紧接着,拜访该数据的大量申请,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库解决其余申请。
缓存击穿的状况,常常在热点数据过期生效时产生。
解决方案:热点数据不设置过期工夫。
缓存穿透
缓存穿透是指要拜访的数据既不在 Redis 缓存中,也不在数据库中,导致申请在拜访缓存时,产生缓存缺失,再去拜访数据库时,发现数据库中也没有要拜访的数据。
起因:
- 业务层误操作,数据被删除
- 歹意攻打,拜访数据库中没有的数据
解决方案:
- 缓存空值或缺省值
- 布隆过滤器疾速判断
- 前端过滤歹意申请
布隆过滤器
布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,能够用来疾速判断某个数据是否存在。
数据写入时标记:
- 应用 N 个哈希函数,别离计算这个数据的哈希值,失去 N 个哈希值。
- 们把这 N 个哈希值对 bit 数组的长度取模,失去每个哈希值在数组中的对应地位。
- 把对应地位的 bit 位设置为 1,这就实现了在布隆过滤器中标记数据的操作。
查问时执行标记过程,并比照 bit 数组中这 N 个地位上的 bit 值。只有这 N 个 bit 值有一个不为 1,
这就表明布隆过滤器没有对该数据做过标记,所以,查问的数据肯定没有在数据库中保留。
27 缓存净化
在一些场景下,有些数据被拜访的次数非常少,甚至只会被拜访一次。当这些数据服务完拜访申请后,如果还持续留存在缓存中的话,就只会白白占用缓存空间。这种状况,就是缓存净化。
缓存净化会导致大量不再拜访的数据滞留在缓存中,当缓存空间占满,再写入新数据时,把这些数据淘汰须要额定的操作工夫开销,影响利用性能。
解决方案:
- 晓得数据被再次拜访的状况,依据拜访工夫设置过期工夫:volatile-ttl
- LFU 缓存策略
扫描式单次查问:
对大量的数据进行一次整体读取,每个数据都会被读取,而且只会被读取一次。
因为这些被查问的数据刚刚被拜访过,所以 lru 字段值都很大。在应用 LRU 策略淘汰数据时,这些数据会留存在缓存中很长一段时间,造成缓存净化。
LFU 缓存策略
LFU 缓存策略是在 LRU 策略根底上,为每个数据减少了一个计数器,来统计这个数据的拜访次数。
- 当应用 LFU 策略筛选淘汰数据时,首先会依据数据的拜访次数进行筛选,把拜访次数最低的数据淘汰出缓存。
- 如果两个数据的拜访次数雷同,LFU 策略再比拟这两个数据的拜访时效性,把间隔上一次拜访工夫更久的数据淘汰出缓存。
扫描式单次查问的数据因为不会被再次拜访,所以它们的拜访次数不会再减少。因而,LFU 策略会优先把这些拜访次数低的数据淘汰出缓存。这样一来,LFU 策略就能够防止这些数据对缓存造成净化了。
LRU 实现原理:
- Redis 是用 RedisObject 构造来保留数据的,RedisObject 构造中设置了一个 lru 字段,用来记录数据的拜访工夫戳;
- Redis 并没有为所有的数据保护一个全局的链表,而是通过随机采样形式,选取肯定数量(例如 10 个)的数据放入候选汇合,后续在候选汇合中依据 lru 字段值的大小进行筛选。
LFU 实现原理:把原来 24bit 大小的 lru 字段,又进一步拆分成了两局部。
- ldt 值:lru 字段的前 16bit,示意数据的拜访工夫戳;
- counter 值:lru 字段的后 8bit,示意数据的拜访次数。
总结一下:当 LFU 策略筛选数据时,Redis 会在候选汇合中,依据数据 lru 字段的后 8bit 抉择拜访次数起码的数据进行淘汰。当拜访次数雷同时,再依据 lru 字段的前 16bit 值大小,抉择拜访工夫最长远的数据进行淘汰。
LFU 应用了非线性递增的计数器办法,通过设置 lfu_log_factor 配置项,来管制计数器值减少的速度;lfu_log_factor=100 时,理论拜访次数小于 10M 的不同数据都能够通过 counter 值辨别进去。
LFU 策略时还设计了一个 counter 值的衰减机制,应用衰减因子配置项 lfu_decay_time 来管制拜访次数的衰减。假如 lfu_decay_time 取值为 1,如果数据在 N 分钟内没有被拜访,那么它的拜访次数就要减 N。
如果业务利用中有短时高频拜访的数据的话,倡议把 lfu_decay_time 值设置为 1,这样一来,LFU 策略在它们不再被拜访后,会较快地衰减它们的拜访次数,尽早把它们从缓存中淘汰进来,防止缓存净化。
小结
LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的拜访频次。
通常状况下,理论利用的负载具备较好的工夫局部性,所以 LRU 策略的利用会更加宽泛。
然而,在扫描式查问的利用场景中,LFU 策略就能够很好地应答缓存净化问题了,倡议你优先应用。
28 大容量实例
Redis 切片集群,把数据扩散保留到多个实例上,如果要保留的数据总量很大,然而每个实例保留的数据量较小的话,就会导致集群的实例规模减少,这会让集群的运维治理变得复杂,减少开销。
减少 Redis 单实例的内存容量,造成大内存实例,每个实例能够保留更多的数据,这样一来,在保留雷同的数据总量时,所须要的大内存实例的个数就会缩小,就能够节俭开销。
潜在问题:
- 内存快照 RDB 生成和复原效率低
- 主从同步时长减少,缓冲区易溢出,导致全量复制
解决方案:
基于 SSD 来实现大容量的 Redis 实例,如 Pika 键值数据库。
29 并发拜访
为了保障并发拜访的正确性,Redis 提供了两种办法,别离是加锁和原子操作。
并发访问控制
指对多个客户端拜访操作同一份数据的过程进行管制,以保障任何一个客户端发送的操作在 Redis 实例上执行时具备互斥性。
并发访问控制对应的操作次要是数据批改操作。当客户端须要批改数据时,根本流程分成两步:
- 客户端先把数据读取到本地,在本地进行批改
- 批改完数据后写回 Redis
这个流程叫做“读取 – 批改 – 写回”操作(Read-Modify-Write,简称为 RMW 操作)。
当有多个客户端对同一份数据执行 RMW 操作的话,咱们就须要让 RMW 操作波及的代码以原子性形式执行。拜访同一份数据的 RMW 操作代码,就叫做临界区代码。
当有多个客户端并发执行临界区代码时,就会存在一些潜在问题。多个客户端操作不具备互斥行,别离基于雷同的初始值进行批改,而不是基于前一个客户端批改后的值再批改。
原子性操作
为了实现并发管制要求的临界区代码互斥执行,Redis 的原子操作采纳了两种办法:
- 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;
- 把多个操作写到一个 Lua 脚本中,以原子性形式执行单个 Lua 脚本。
Redis 提供了 INCR/DECR 原子操作。
Lua 脚本:
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其余命令打断,从而保障了 Lua 脚本中操作的原子性。
毛病:
操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的工夫减少,同样也会升高 Redis 的并发性能。
倡议:
在编写 Lua 脚本时,你要防止把不须要做并发管制的操作写入脚本中。
30 分布式锁
在分布式系统中,当有多个客户端须要获取锁时,咱们须要分布式锁。此时,锁是保留在一个共享存储系统中的,能够被多个客户端共享拜访和获取。
分布式锁的要求:
- 加锁和开释锁波及多个操作,实现分布式锁要保障操作的原子性
- 共享存储系统保留锁变量,实现分布式锁要保障共享存储系统的可靠性
单机锁
Redis 能够应用一个键值对 lock_key:0 来保留锁变量,其中,键是 lock_key,也是锁变量的名称,锁变量的初始值是 0。
加锁时客户端先读取 lock_key 的值,发现 lock_key 为 0,所以,Redis 就把 lock_key 的 value 置为 1,示意曾经加锁了。开释锁就是间接把锁变量值设置为 0。
// 加锁
SET key value [EX seconds | PX milliseconds] [NX]
// 业务逻辑
DO THINGS
// 开释锁
DEL lock_key
NX 选项:SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作。
EX 或 PX 选项:设置键值对的过期工夫。
危险 1:加锁后产生异样,没有开释锁导致阻塞。
解决办法:给锁变量设置过期工夫。
危险 2:客户端 A 加的锁被客户端 B 删掉 DEL
解决办法:每个客户端的锁设一个惟一值 uuid
加锁示例:
// 加锁, unique_value 作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
解锁脚本 unlock.script:
// 开释锁 比拟 unique_value 是否相等,防止误开释
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
解锁命令:
redis-cli --eval unlock.script lock_key , unique_value
分布式锁
为了防止 Redis 实例故障而导致的锁无奈工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。
基本思路是让客户端和多个独立的 Redis 实例顺次申请加锁,如果客户端可能和半数以上的实例胜利地实现加锁操作,那么咱们就认为,客户端胜利地取得分布式锁了,否则加锁失败。
加锁流程:
- 客户端获取以后工夫
- 客户端依序向 N 个 Redis 实例执行加锁操作
-
客户端实现所有实例加锁后,计算加锁总耗时,加锁胜利条件:
- 客户端从超过半数实例 (N/2+1) 获取到锁
- 客户端获取锁的总耗时没有超过锁的无效工夫
- 从新计算所的无效工夫:最后无效工夫 – 获取锁的总耗时
开释锁流程:
执行开释锁的 Lua 脚本,留神开释锁时,要对所有节点开释。