23. 旁路缓存:Redis是如何工作的?
23.1 计算机系统中的三层存储结构
23.2 概念
- 旁路缓存:读取缓存、读取数据库和更新缓存的操作都要在应用程序中来完成。
- Redis缓存类型:只读缓存和读写缓存
23.3 只读缓存
所有最新的数据都在数据库中
23.4 读写缓存
读写请求都会发送到缓存,最新的数据的数据库在redis中,redis是内存数据库,一旦出现掉电和宕机,数据就会丢失。 根据业务应用对数据可靠想和缓存性能的不同要求,会有同步直写和异步写回两种策略。 这两种策略都有各自的优缺点
- 同步直写策略最新的数据库也在MySQL中保证了数据可靠性但增加了缓存的响应延迟。
- 异步写回策略优先了响应延迟,但如果发生了掉电没有被写回数据库,就会有丢失的分险。
23.5 两者的区别
24. 替换策略:缓存满了怎么办?
24.1 背景
八二原理:80%的请求实际只访问了20%的数据,所以用1TB的内存作缓存,并没有必要。 随着缓存的空间容量越来越大。有限的缓存空间不可避免会被写满,为了解决这个问题涉及到缓存数据的淘汰机制。 系统的设计选择是一个权衡的过程,大容量缓存是能带来性能加速的收益,但成本也会很高,小容量不一定就起不到加速访问的效果的。 建议把缓存容量设置为总数据量的15%到30%。 CONFIG SET maxmemory 4gb
24.2 Redis缓存的策略
volatile-ttl:根据过期时间的先后进行删除,越早过期越先被删除
volatile-random:在过期的键值对中,进行随机删除
volatile-lru:使用lru算法筛选了过期时间的键值对
volatile-lfu:使用lfu算法选择设置了过期时间的键值对
24.3 LRU算法原理
Least Recently Used 最不常用的数据会被筛选出来,它认为刚刚被访问的数据,肯定还会被再次被访问把他放在MRU端。用链表管理所有缓存数据,会带来额外的空间开销。之后算法进行了优化。CONFIG SET maxmemory-samples 100
24.4 LFU算法
LFU算法的核心思想是根据历史数据频率来淘汰数据,核心思想是认为从历史数据来看访问频率高那么将来被访问的频率也高呢
24.5 建议
如果你的业务数据有明细的冷热数据区分,建议使用allkeys-lru。可以充分利用lru这一经典缓存算法的优势,把最近最常访问的数据留在缓存中,提升应用的访问性能。 如果业务应用中的数据访问频率相差不大,没有冷热区分,建议使用allkeys-random。 如果业务中有置顶的需求。不给这些置顶数据设置过期时间。
25. 缓存异常(上):如何解决缓存和数据库的数据不一致问题?
25.1 只读缓存
删除/更新缓存,更新/删除数据库。这两个操作如果无法保证原子性,就会出现数据不一致问题。
2.5.1.1 解决办法
2.5.1.1.1 删除缓存值或更新数据库失败 导致的不一致使用重试机制
2.5.1.1.2 删除缓存值、更新数据库的两步操作过程中,有其他线程的并发操作导致的不一致采用延迟双删
情况一:先删除缓存,再更新数据库
延迟双删:线程A更新数据库中的X后的一段时间再去删除缓存。
情况二:先更新数据库值,再删除缓存值
这种情况对业务影响较小
25.2 总结
建议优先使用更新数据库再删除缓存的方法
- 先删除缓存再更新数据库,有可能导致请求因缓存确实而访问数据库,给数据库带来压力
- 如果业务应用中读取数据和写缓存时间不好估算,延迟双删中的等待时间不好设置
26. 缓存异常(下):如何解决缓存雪崩、击穿、穿透难题?
26.1 缓存雪崩
大量的应用无法在Redis缓存中进行处理,导致数据库层的压力激增。 原因(解决方法):
- 缓存中有大量数据同时过期(数据的过期时间增加一个较小的随机数)
- Redis缓存实例发生故障宕机(业务系统重实现服务熔断或请求限流机制;利用检测Redis缓存所在机器和数据库所在机器的负载指标等,发现宕机,数据库所在机器的压力突然增加,启用服务熔断降级;事前预防构建Redis缓存高可靠集群)
26.2 缓存击穿
缓存击穿是指非常频繁的热点数据失效导致数据库层的压力激增。 解决办法就是对于频繁热点的数据不设置过期时间。
26.3 缓存穿透
缓存穿透式指要访问的数据既不在Redis缓存中,也不在数据库中,导致请求访问缓存时,发生缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据。如果有大量并发读写请求,同时给缓存和数据库带来巨大压力。 解决方法
- 缓存空值和缺省值
- 使用布隆过滤器快速判断是否存在,避免从数据库中查询数据是否存在,减轻数据库压力
- 对请求入口前端对请求合法性进行检查
27. 缓存被污染了,该怎么办?
缓存污染指的是大量不再访问的数据滞留在缓存中。
27.1 LRU缓存策略
如果一个数据刚刚被访问,那么这个数据库肯定是热数据。Redis中LRU策略会在每个数据对应的RedisObject结构体中设置一个lru字段来记录数据访问时间戳。进行数据淘汰回淘汰字段值最小的数据。
27.2 LFU缓存策略的优化
Redis从4.0版本开始增加了LFU策略。数据访问的时效性+数据的被访问次数。 当使用 LFU 策略筛选淘汰数据时,首先会根据数据的访问次数进行筛选,把访问次数最低的数据淘汰出缓存。如果两个数据的访问次数相同,LFU 策略再比较这两个数据的访问时效性,把距离上一次访问时间更久的数据淘汰出缓存。 Redis 只是把原来 24bit 大小的 lru 字段,又进一步拆分成了 16bit 的 ldt 和 8bit 的 counter,分别用来表示数据的访问时间戳和访问次数。为了避开 8bit 最大只能记录 255 的限制,LFU 策略设计使用非线性增长的计数器来表示数据的访问次数。
27.3 总结
LRU 策略更加关注数据的时效性,而 LFU 策略更加关注数据的访问频次。 通常情况下,实际应用的负载具有较好的时间局部性,所以 LRU 策略的应用会更加广泛。 但是,在扫描式查询的应用场景中,LFU 策略就可以很好地应对缓存污染问题了,建议你优先使用。
30. 如何使用Redis实现分布式锁?
原则
- 加锁释放锁保证锁操作的原子性
- 共享存储系统的可靠性
30.1 基于单个节点实现分布式锁
加锁包括读取锁变量、检查锁变量值和设置锁变量值三个操作需要原子操作。 释放锁包含读取锁变量值、判断锁变量值和删除锁变量三个操作也需要原则操作。
命令
- SETNX 不存在即设置
- NX 不存在即设置
- SET key value [EX seconds | PX milliseconds] [NX]
30.1.1 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
30.1.2 释放锁
redis-cli --eval unlock.script lock_key , unique_value 其中,KEYS[1]表示 lock_key,ARGV[1]是当前客户端的唯一标识,这两个值都是我们在执行 Lua 脚本时作为参数传入的。
//释放锁 比较unique_value是否相等,避免误释放 if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
30.2 基于多个redis节点实现高可靠的分布式锁
Redis 也提供了 Redlock 算法,用来实现基于多个实例的分布式锁。 这样一来,锁变量由多个实例维护,即使有实例发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。
基本思路:让客户端和多个独立的Redis实例一次请求加锁,客户端能够和半数以上的实例成功地完成加锁操作,就认为成功。
- 客户端获取当前时间
- 客户端按顺序依次向N个Redis实例执行加锁操作
- 客户端完成和所有redis实例的加锁操作,客户端就要计算整个过程的总耗时(客户端从超过半数;获取锁的总耗时没有超过锁的有效时间)
31. 事务机制:Redis能实现ACID属性吗?
事务是数据库的一个重要功能,指的是对数据进行读写的一系列操作提供专门的属性保证如:原子性A、一致性C、隔离性I、持久性D。
31.1 原子性 事务中语法有误得不到保证
一个事务中的多个操作必须都完成,或者都不完成。 Redis对原子性属性保证情况
- 命令入队就报错,会放弃事务执行,保证原子性
- 命令入队没报错,实际执行时报错,不保证原子性
- exec命令执行时实例故障,如果开启aof可以保证
31.2 一致性 redis可以保证
数据库中的数据在事务执行前后是一致的。 Redis对一致性属性保证情况
- 跟上面差不多,在命令执行错误或Redis发生故障的情况下,Redis事务机制对一致性是有保证的
31.3 隔离性 redis可以保证
数据库在执行一个事务时,其他操作无法存取正在执行事务访问的数据。 Redis对隔离性保证情况
- 并发操作还没执行exec,使用watch机制来保证
- 并发操作在exec执行后,不会破坏隔离性
31.4 持久性 redis无法保证的
数据库执行事务后,数据的修改要被持久化下来。 其实redis不管是使用RDB还是AOF,都会会有数据丢失的情况所以事务的持久性得不到保证的。
31.5 总结
32. Redis主从同步与故障切换,有哪些坑?
32.1 主从数据不一致
主从库的命令是异步进行的
- 网络可能会有传输延迟
- 即使收到可能回音正在处理其他复杂度高的命令而阻塞
采用外部程序来监控
32.2 读到过期数据
redis使用两种策略来删除过期的数据:惰性删除和定期删除 定期删除:redis为了避免过多删除操作对性能产生影响,每次随机检查数据的数量并不多,这些数据就会留在redis实例中。
惰性删除:客户端从主库读取留存的过期数据,主库会触发删除操作,客户端不会读到。但是从库不会执行删除操作,如果客户端在从库中访问流程的过期数据,从库不会触发删除操作。
redis3.2之前的版本:从库在服务读取请求,会返回过期数据 redis3.2之后的版本:从库不会删除,会返回空值。
32.3 配置项设置得不合理
protected-mode如果设置了yes导致实例诊间无法进行主从切换 protected-mode no bind 192.168.10.3 192.168.10.4 192.168.10.5
cluster-node-timeout实例响应心跳消息的超时时间建议调大大写 10-20s
32.4 总结
33. 脑裂:一次奇怪的数据丢失
发生脑裂的过程
从库升级为新主库会发送什么
- 哨兵会让原主库执行slave of 命令和新主库进行全量同步
- 原主库清空本地的数据,加载新主库发生的rdb文件
解决办法
总结