关于redis:Redis核心技术2630

48次阅读

共计 4219 个字符,预计需要花费 11 分钟才能阅读完成。

26 缓存异样

缓存雪崩、缓存击穿和缓存穿透,这三个问题一旦产生,会导致大量的申请积压到数据库层,导致数据库宕机或故障。

缓存雪崩

缓存雪崩是指大量的利用申请无奈在 Redis 缓存中进行解决,紧接着,利用将大量申请发送到数据库层,导致数据库层的压力激增。

如何发现:
监测 Redis 缓存所在机器和数据库所在机器的负载指标,例如每秒申请数、CPU 利用率、内存利用率等。如果咱们发现 Redis 缓存实例宕机了,而数据库所在机器的负载压力忽然减少(例如每秒申请数激增),此时,就产生缓存雪崩了。

起因一:大量数据同时过期。
解决方案:

  1. 过期工夫加随机数
  2. 服务降级:暂停非核心业务拜访,间接返回预约义信息;外围数据容许持续查问

起因二:Redis 实例宕机
解决方案:

  1. 服务熔断或限流:

    • 服务熔断:暂停拜访,间接返回
    • 限流:申请入口设置每秒申请数量,超出间接回绝
  2. 配置高可用集群

缓存击穿

缓存击穿是指,针对某个拜访十分频繁的热点数据的申请,无奈在缓存中进行解决,紧接着,拜访该数据的大量申请,一下子都发送到了后端数据库,导致了数据库压力激增,会影响数据库解决其余申请。

缓存击穿的状况,常常在热点数据过期生效时产生。

解决方案:热点数据不设置过期工夫。

缓存穿透

缓存穿透是指要拜访的数据既不在 Redis 缓存中,也不在数据库中,导致申请在拜访缓存时,产生缓存缺失,再去拜访数据库时,发现数据库中也没有要拜访的数据。

起因:

  1. 业务层误操作,数据被删除
  2. 歹意攻打,拜访数据库中没有的数据

解决方案:

  1. 缓存空值或缺省值
  2. 布隆过滤器疾速判断
  3. 前端过滤歹意申请

布隆过滤器

布隆过滤器由一个初值都为 0 的 bit 数组和 N 个哈希函数组成,能够用来疾速判断某个数据是否存在。

数据写入时标记:

  1. 应用 N 个哈希函数,别离计算这个数据的哈希值,失去 N 个哈希值。
  2. 们把这 N 个哈希值对 bit 数组的长度取模,失去每个哈希值在数组中的对应地位。
  3. 把对应地位的 bit 位设置为 1,这就实现了在布隆过滤器中标记数据的操作。

查问时执行标记过程,并比照 bit 数组中这 N 个地位上的 bit 值。只有这 N 个 bit 值有一个不为 1,
这就表明布隆过滤器没有对该数据做过标记,所以,查问的数据肯定没有在数据库中保留。

27 缓存净化

在一些场景下,有些数据被拜访的次数非常少,甚至只会被拜访一次。当这些数据服务完拜访申请后,如果还持续留存在缓存中的话,就只会白白占用缓存空间。这种状况,就是缓存净化。

缓存净化会导致大量不再拜访的数据滞留在缓存中,当缓存空间占满,再写入新数据时,把这些数据淘汰须要额定的操作工夫开销,影响利用性能。

解决方案:

  1. 晓得数据被再次拜访的状况,依据拜访工夫设置过期工夫:volatile-ttl
  2. LFU 缓存策略

扫描式单次查问:
对大量的数据进行一次整体读取,每个数据都会被读取,而且只会被读取一次。

因为这些被查问的数据刚刚被拜访过,所以 lru 字段值都很大。在应用 LRU 策略淘汰数据时,这些数据会留存在缓存中很长一段时间,造成缓存净化。

LFU 缓存策略

LFU 缓存策略是在 LRU 策略根底上,为每个数据减少了一个计数器,来统计这个数据的拜访次数。

  • 当应用 LFU 策略筛选淘汰数据时,首先会依据数据的拜访次数进行筛选,把拜访次数最低的数据淘汰出缓存。
  • 如果两个数据的拜访次数雷同,LFU 策略再比拟这两个数据的拜访时效性,把间隔上一次拜访工夫更久的数据淘汰出缓存。

扫描式单次查问的数据因为不会被再次拜访,所以它们的拜访次数不会再减少。因而,LFU 策略会优先把这些拜访次数低的数据淘汰出缓存。这样一来,LFU 策略就能够防止这些数据对缓存造成净化了。

LRU 实现原理:

  • Redis 是用 RedisObject 构造来保留数据的,RedisObject 构造中设置了一个 lru 字段,用来记录数据的拜访工夫戳;
  • Redis 并没有为所有的数据保护一个全局的链表,而是通过随机采样形式,选取肯定数量(例如 10 个)的数据放入候选汇合,后续在候选汇合中依据 lru 字段值的大小进行筛选。

LFU 实现原理:把原来 24bit 大小的 lru 字段,又进一步拆分成了两局部。

  1. ldt 值:lru 字段的前 16bit,示意数据的拜访工夫戳;
  2. 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 单实例的内存容量,造成大内存实例,每个实例能够保留更多的数据,这样一来,在保留雷同的数据总量时,所须要的大内存实例的个数就会缩小,就能够节俭开销。

潜在问题:

  1. 内存快照 RDB 生成和复原效率低
  2. 主从同步时长减少,缓冲区易溢出,导致全量复制

解决方案:
基于 SSD 来实现大容量的 Redis 实例,如 Pika 键值数据库。

29 并发拜访

为了保障并发拜访的正确性,Redis 提供了两种办法,别离是加锁和原子操作。

并发访问控制

指对多个客户端拜访操作同一份数据的过程进行管制,以保障任何一个客户端发送的操作在 Redis 实例上执行时具备互斥性。

并发访问控制对应的操作次要是数据批改操作。当客户端须要批改数据时,根本流程分成两步:

  1. 客户端先把数据读取到本地,在本地进行批改
  2. 批改完数据后写回 Redis

这个流程叫做“读取 – 批改 – 写回”操作(Read-Modify-Write,简称为 RMW 操作)。

当有多个客户端对同一份数据执行 RMW 操作的话,咱们就须要让 RMW 操作波及的代码以原子性形式执行。拜访同一份数据的 RMW 操作代码,就叫做临界区代码。

当有多个客户端并发执行临界区代码时,就会存在一些潜在问题。多个客户端操作不具备互斥行,别离基于雷同的初始值进行批改,而不是基于前一个客户端批改后的值再批改。

原子性操作

为了实现并发管制要求的临界区代码互斥执行,Redis 的原子操作采纳了两种办法:

  1. 把多个操作在 Redis 中实现成一个操作,也就是单命令操作;
  2. 把多个操作写到一个 Lua 脚本中,以原子性形式执行单个 Lua 脚本。

Redis 提供了 INCR/DECR 原子操作。

Lua 脚本:
Redis 会把整个 Lua 脚本作为一个整体执行,在执行的过程中不会被其余命令打断,从而保障了 Lua 脚本中操作的原子性。

毛病:
操作都放在 Lua 脚本中原子执行,会导致 Redis 执行脚本的工夫减少,同样也会升高 Redis 的并发性能。
倡议:
在编写 Lua 脚本时,你要防止把不须要做并发管制的操作写入脚本中。

30 分布式锁

在分布式系统中,当有多个客户端须要获取锁时,咱们须要分布式锁。此时,锁是保留在一个共享存储系统中的,能够被多个客户端共享拜访和获取。

分布式锁的要求:

  1. 加锁和开释锁波及多个操作,实现分布式锁要保障操作的原子性
  2. 共享存储系统保留锁变量,实现分布式锁要保障共享存储系统的可靠性

单机锁

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 实例顺次申请加锁,如果客户端可能和半数以上的实例胜利地实现加锁操作,那么咱们就认为,客户端胜利地取得分布式锁了,否则加锁失败。

加锁流程:

  1. 客户端获取以后工夫
  2. 客户端依序向 N 个 Redis 实例执行加锁操作
  3. 客户端实现所有实例加锁后,计算加锁总耗时,加锁胜利条件:

    1. 客户端从超过半数实例 (N/2+1) 获取到锁
    2. 客户端获取锁的总耗时没有超过锁的无效工夫
  4. 从新计算所的无效工夫:最后无效工夫 – 获取锁的总耗时

开释锁流程:
执行开释锁的 Lua 脚本,留神开释锁时,要对所有节点开释。

正文完
 0