上一篇 :17. Redis 分布式锁 - 周阳
下一篇 :18. 面试题简析
文章目录
- 1. 为什么要用缓存?
- 2. redis 和 memcached 有什么区别?
- 3. redis 的线程模型
- 4. 为啥 redis 单线程模型也能效率这么高?
- 5. redis 都有哪些数据类型?分别在哪些场景下使用比较合适?
- 6. redis 过期策略
- 7. 内存淘汰机制
- 8. 手写一个 LRU 算法
- 9. Redis 主从架构
- 9.1 redis 数据复制的核心机制
- 9.2 redis 主从复制的核心原理
- 10. Redis 哨兵集群实现高可用
- 10.1 哨兵的介绍
- 10.2 哨兵的核心知识
- 10.3 redis 哨兵主备切换的数据丢失问题
- 10.4 sdown 和 odown 转换机制
- 10.5 哨兵集群的自动发现机制
- 10.6 slave 配置的自动纠正
- 10.7 slave->master 选举算法
- 10.8 quorum 和 majority
- 10.9 configuration epoch
- 10.10 configuration 传播
- 11. redis 的持久化有哪几种方式?
- 12. redis 集群模式的工作原理能说一下么?
- 12.1 分布式寻址算法
- 12.2 redis cluster 的高可用与主备切换原理
- 12.3 节点间的内部通信机制
- 12.4 面向集群的jedis内部实现原理
- 13. 了解什么是 redis 的雪崩、穿透和击穿?
- 13.1 缓存雪崩
- 13.2 缓存穿透
- 13.3 缓存击穿
- 14. 如何保证缓存与数据库的双写一致性?
- 14.1 Cache Aside Pattern
- 14.2 最初级的缓存不一致问题及解决方案
- 14.3 比较复杂的数据不一致问题分析
- 15. redis 的并发竞争问题是什么?
- 16. 生产环境中的 redis 是怎么部署的?
1. 为什么要用缓存?
-
用缓存,主要有两个用途:高性能、高并发。
-
高性能
- 假设这么个场景,你有个操作,一个请求过来,各种乱七八糟操作 mysql,半天查出来一个结果,耗时 600ms。但是这个结果可能接下来几个小时都不会变了,或者变了也可以不用立即反馈给用户。
- 那么此时就可以上缓存,折腾 600ms 查出来的结果,扔缓存里,一个 key 对应一个 value,下次再有人查,别走 mysql 折腾 600ms 了,直接从缓存里,通过一个 key 查出来一个 value,2ms 搞定。性能提升 300 倍。
- 就是说对于一些需要复杂操作耗时查出来的结果,且确定后面不怎么变化,但是有很多读请求,那么直接将查询出来的结果放在缓存中,后面直接读缓存就好。
-
高并发
- mysql 这么重的数据库,压根儿设计不是让你玩儿高并发的,虽然也可以玩儿,但是天然支持不好。mysql 单机支撑到 2000QPS 也开始容易报警了。
- 所以要是你有个系统,高峰期一秒钟过来的请求有 1万,那一个 mysql 单机绝对会死掉。你这个时候就只能上缓存,把很多数据放缓存,别放 mysql。缓存功能简单,说白了就是 key-value 式操作,单机支撑的并发量轻松一秒几万十几万,支撑高并发 so easy。单机承载并发量是 mysql 单机的几十倍。
- 缓存是走内存的,内存天然就支撑高并发。
2. redis 和 memcached 有什么区别?
-
redis 支持复杂的数据结构
redis 相比 memcached 来说,拥有更多的数据结构,能支持更丰富的数据操作。如果需要缓存能够支持更复杂的结构和操作, redis 会是不错的选择。 -
redis 原生支持集群模式
在 redis3.x 版本中,便能支持 cluster 模式,而 memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据。 -
性能对比
由于 redis 只使用单核,而 memcached 可以使用多核,所以平均每一个核上 redis 在存储小数据时比 memcached 性能更高。而在 100k 以上的数据中,memcached 性能要高于 redis。虽然 redis 最近也在存储大数据的性能上进行优化,但是比起 memcached,还是稍有逊色。
3. redis 的线程模型
- redis 是单线程的模型
- redis 内部使用文件事件处理器,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,将产生事件的 socket 压入内存队列中,事件分派器根据 socket 上的事件类型来选择对应的事件处理器进行处理。
- 文件事件处理器的结构包含 4 个部分:
- 多个 socket
- IO 多路复用程序
- 文件事件分派器
- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
- 多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将产生事件的 socket 放入队列中排队,事件分派器每次从队列中取出一个 socket,根据 socket 的事件类型交给对应的事件处理器进行处理,等到上一个事件处理完成,再从队列中取出下一个处理。
- 来看客户端与 redis 的一次通信过程:
要明白,通信是通过 socket 来完成的,不懂的同学可以先去看一看 socket 网络编程。-
首先,redis 服务端进程初始化的时候,会将
server socket
的 AE_READABLE 事件与连接应答处理器
关联。 -
客户端向
redis
进程的server socket
请求建立连接,- 此时
server socket
会产生一个 AE_READABLE 事件, IO 多路复用程序
监听到server socket
产生的事件后,将该server socket
(上图队列中的‘ss
’) 压入队列中文件事件分派器
从队列中获取server socket
,因为上面将server socket
的 AE_READABLE 与连接应答处理器
关联,所以事件分派器
交给连接应答处理器
。连接应答处理器
会创建一个能与客户端通信的socket01
,并将该socket01
的 AE_READABLE 事件与命令请求处理器
关联。
- 此时
-
假设客户端发送了一个 set key value 请求,
- 此时与客户端连接的
socket01
会产生 AE_READABLE 事件, IO 多路复用程序
将socket01
(上图队列中的‘s1
’) 压入队列,文件事件分派器
从队列中获取到socket01
产生的 AE_READABLE 事件,由于前面socket01
的 AE_READABLE 事件已经与命令请求处理器
关联,所以事件分派器
将事件交给命令请求处理器
来处理。命令请求处理器
读取socket01
的 key value 并在自己内存中完成 key value 的设置。- 操作完成后,它会将
socket01
的 AE_WRITABLE 事件与命令回复处理器
关联。
- 此时与客户端连接的
-
如果此时客户端准备好接收返回结果了,
- 那么
socket01
会产生一个 AE_WRITABLE 事件,同样压入队列中, 事件分派器
找到相关联的命令回复处理器
- 由
命令回复处理器
对socket01
输入本次操作的一个结果,比如 ok - 之后解除
socket01
的 AE_WRITABLE 事件与命令回复处理器
的关联。
- 那么
-
这样便完成了一次通信。关于 Redis 的一次通信过程
-
4. 为啥 redis 单线程模型也能效率这么高?
- 纯内存操作。
- 核心是基于非阻塞的 IO 多路复用机制。
- C 语言实现,一般来说,C 语言实现的程序“距离”操作系统更近,执行速度相对会更快。
- 单线程反而避免了多线程的频繁上下文切换问题,预防了多线程可能产生的竞争问题。
5. redis 都有哪些数据类型?分别在哪些场景下使用比较合适?
redis 主要有以下几种数据类型:
- string
- hash
- list
- set
- sorted set
string
- 这是最简单的类型,就是普通的 set 和 get,做简单的 KV 缓存。
hash
-
这个是类似 map 的一种结构,这个一般就是可以将结构化的数据,比如一个对象(前提是这个对象没嵌套其他的对象)给缓存在 redis 里,然后每次读写缓存的时候,可以就操作 hash 里的某个字段。
hset person name bingo hset person age 20 hset person id 1 hget person name ---- person = { "name": "bingo", "age": 20, "id": 1 }
list
-
list 是有序列表
-
比如可以通过 list 存储一些列表型的数据结构,类似粉丝列表、文章的评论列表之类的东西。
-
比如可以通过 lrange 命令,读取某个闭区间内的元素,可以基于 list 实现分页查询,这个是很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西,性能高,就一页一页走。
# 0开始位置,-1结束位置,结束位置为-1时,表示列表的最后一个位置,即查看所有。 lrange mylist 0 -1
-
比如可以搞个简单的消息队列,从 list 头怼进去,从 list 尾巴那里弄出来。
lpush mylist 1 lpush mylist 2 lpush mylist 3 4 5 # 1 rpop mylist
set
- set 是无序集合,自动去重。
- 直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果你需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。
- 可以基于 set 玩儿交集、并集、差集的操作,比如交集吧,可以把两个人的粉丝列表整一个交集,看看俩人的共同好友是谁?对吧。
sorted set
- sorted set 是排序的 set,去重同时可以排序,写进去的时候给一个分数,自动根据分数排序。
6. redis 过期策略
- redis 过期策略是:定期删除+惰性删除
- 定期删除,指的是 redis 默认是每隔 100ms 就 随机抽取 一些设置了过期时间的 key,检查其是否过期,如果过期就删除。
- 惰性删除,指的是获取 key 的时候,如果此时 key 已经过期,就删除,不会返回任何东西
-
假设 redis 里放了 10w 个 key,都设置了过期时间,你每隔几百毫秒,就检查 10w 个 key,那 redis 基本上就死了,cpu 负载会很高的,消耗在你的检查过期 key 上了。注意,这里可不是每隔 100ms 就遍历所有的设置过期时间的 key,那样就是一场性能上的灾难。实际上 redis 是每隔 100ms 随机抽取一些 key 来检查和删除的。
-
定期删除可能会导致很多过期 key 到了时间并没有被删除掉,那咋整呢?这个时候就用到了惰性删除
-
但是实际上这还是有问题的,如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存里,导致 redis 内存块耗尽了,咋整?
答案是:走内存淘汰机制 👇👇👇👇
7. 内存淘汰机制
redis 内存淘汰机制有以下几个:
- noeviction: 当内存不足以容纳新写入数据时,新写入操作会报错,这个一般没人用吧,实在是太恶心了。
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)。
- allkeys-random:当内存不足以容纳新写入数据时,在键空间中,随机移除某个 key,这个一般没人用吧,为啥要随机,肯定是把最近最少使用的 key 给干掉啊。
- volatile-lru:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最近最少使用的 key(这个一般不太合适)。
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个 key。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的 key 优先移除。
8. 手写一个 LRU 算法
- 你可以现场手写最原始的 LRU 算法,那个代码量太大了,似乎不太现实。
- 不求自己纯手工从底层开始打造出自己的 LRU,但是起码要知道如何利用已有的 JDK 数据结构实现一个 Java 版的 LRU。
class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
9. Redis 主从架构
-
单机的 redis,能够承载的 QPS 大概就在上万到几万不等。对于缓存来说,一般都是用来支撑读高并发的。
-
因此架构做成主从(master-slave)架构,一主多从,主负责写,并且将数据复制到其它的 slave 节点,从节点负责读。所有的读请求全部走从节点。
-
这样也可以很轻松实现水平扩容,支撑读高并发。
9.1 redis 数据复制的核心机制
- redis 采用异步方式复制数据到 slave 节点,不过 redis2.8 开始,slave node 会周期性地确认自己每次复制的数据量;
- 一个 master node 是可以配置多个 slave node 的;
- slave node 也可以连接其他的 slave node;
- slave node 做复制的时候,不会影响 master node 的正常工作;
- slave node 在做复制的时候,也不会打断对自己的查询操作,它会用旧的数据集来提供服务;但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了;
- slave node 主要用来进行横向扩容,做读写分离,扩容的 slave node 可以提高读的吞吐量。
注意
- 如果采用了主从架构,那么建议必须开启 master node 的持久化,不建议用 slave node 作为 master node 的数据热备。因为那样的话,如果 master 宕机重启,启动完成后数据内容是空的,然后经过复制, slave node 的数据也会被清空。
- master 的各种备份方案,也需要做。万一本地的所有文件丢失了,从备份中挑选一份 rdb 去恢复 master,这样才能确保启动的时候,是有数据的
- 即使采用了后续讲解的高可用机制,slave node 可以自动接管 master node,但也可能 sentinel 还没检测到 master failure,master node 就自动重启了,还是可能导致上面所有的 slave node 数据被清空。
9.2 redis 主从复制的核心原理
1. 主从复制流程
-
建立连接
slave
节点启动时,会在自己本地保存master
的 host和ip 信息(这些就是 conf 文件的配置信息),但是复制流程没开始slave
内部有个定时任务,每秒检查是否有新的master
要连接和复制- 如果发现,就跟
master
建立 socket 网络连接 - 如果
master
设置了requirepass
,那么slave
必须发送masterauth
的口令过去进行认证
-
slave
节点会发送一个SYNC
命令给master
节点 -
如果这是
slave
初次连接到master
,那么会触发一次全量复制
。 -
slave
会先将接收到的数据写入本地磁盘
,然后再从本地磁盘
加载到内存
中 -
若连接断开,重新连接后
master
会从上一次最后同步的数据开始同步后面的数据到slave
,就是增量复制
2. 主从复制的断点续传
-
从 redis2.8 开始,就支持主从复制的断点续传
-
如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份。
master node
会在内存中维护一个backlog
,master
和slave
都会保存一个replica offset复制偏移量
还有一个master run id
,offset
就是保存在backlog
中的。- 如果
master
和slave
网络连接断掉了,slave
会让master
从上次offset
开始继续复制, - 如果没有找到对应的
offset
,那么就会执行一次全量复制
。
-
注意:如果根据 host+ip 定位 master node,是不靠谱的,如果 master node 重启或者数据出现了变化,那么 slave node 应该根据不同的 run id 区分。
3. 无磁盘化复制
-
master 在内存中直接创建 RDB,然后发送给 slave,不会在自己本地落地磁盘了。
-
只需要在配置文件中开启 repl-diskless-sync yes 即可。
repl-diskless-sync yes # 等待 5s 后再开始复制,因为要等尽可能多的 slave 连接上,一次性向尽可能多的 slave 同步数据 repl-diskless-sync-delay 5
4. 过期 key 处理
- slave 不会过期 key,只会等待 master 过期 key。
- 如果 master 过期了一个 key,或者通过 LRU 淘汰了一个 key,那么会模拟一条 del 命令发送给 slave。
5. 全量复制
-
master 执行 bgsave ,在本地生成一份 rdb 快照文件。
-
master node 将 rdb 快照文件发送给 slave node,如果 rdb 复制时间超过 60秒(repl-timeout),那么 slave node 就会认为复制失败,可以适当调大这个参数(对于千兆网卡的机器,一般每秒传输 100MB,6G 文件,很可能超过 60s)
-
master node 在生成 rdb 时,会将所有新的写命令缓存在内存中,在 slave node 保存了 rdb 之后,再将缓存再内存中的新的写命令复制给 slave node。
-
如果在复制期间,内存缓冲区在 60 秒内持续消耗超过 64MB,或者一次性超过 256MB,那么停止复制,复制失败。
client-output-buffer-limit slave 256MB 64MB 60
-
slave node 接收到 rdb 之后,清空自己的旧数据,然后重新加载 rdb 到自己的内存中,同时基于旧的数据版本对外提供服务。
-
如果 slave node 开启了 AOF,那么会立即执行 BGREWRITEAOF,重写 AOF。
6. 增量复制
- 如果全量复制过程中,master-slave 网络连接断掉,那么 slave 重新连接 master 时,会触发增量复制。
- master 直接从自己的 backlog 中获取部分丢失的数据,发送给 slave node,默认 backlog 就是 1MB。
- master 就是根据 slave 发送的 psync 中的 runId 和 offset 来从 backlog 中获取数据的。
7. heartbeat心跳
-
主从节点互相都会发送 heartbeat心跳 信息。
-
master 默认每隔 10秒 发送一次,slave node 每隔 1秒 发送一个。
8. 异步复制
- master 每次接收到写命令之后,先在内部写入数据,然后异步发送给 slave node。
10. Redis 哨兵集群实现高可用
10.1 哨兵的介绍
-
sentinel,中文名是哨兵。哨兵是 redis 集群机构中非常重要的一个组件,主要有以下功能:
- 集群监控:负责监控 redis master 和 slave 进程是否正常工作。
- 消息通知:如果某个 redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
- 故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
- 配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
-
哨兵用于实现 redis 集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
- 故障转移时,判断一个 master node 是否宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题。
- 即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了。
10.2 哨兵的核心知识
- 哨兵至少需要 3 个实例,来保证自己的健壮性。
- 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。
- 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
-
哨兵集群必须部署 2 个以上节点,如果哨兵集群仅仅部署了 2 个哨兵实例,quorum = 1。
+----+ +----+ | M1 |---------| R1 | | S1 | | S2 | +----+ +----+
-
配置 quorum=1,如果 master 宕机, s1 和 s2 中只要有 1 个哨兵认为 master 宕机了,就可以进行切换,同时 s1 和 s2 会选举出一个哨兵来执行故障转移。但是同时这个时候,需要 majority,也就是大多数哨兵都是运行的。
>2 个哨兵,majority=2
3 个哨兵,majority=2
4 个哨兵,majority=2
5 个哨兵,majority=3
… -
如果此时仅仅是 M1 进程宕机了,哨兵 s1 正常运行,那么故障转移是 OK 的。但是如果是整个 M1 和 S1 运行的机器宕机了,那么哨兵只有 1 个,此时就没有 majority 来允许执行故障转移,虽然另外一台机器上还有一个 R1,但是故障转移不会执行。
-
经典的 3 节点哨兵集群是这样的:
+----+ | M1 | | S1 | +----+ | +----+ | +----+ | R2 |----+----| R3 | | S2 | | S3 | +----+ +----+
-
配置 quorum=2,如果 M1 所在机器宕机了,那么三个哨兵还剩下 2 个,S2 和 S3 可以一致认为 master 宕机了,然后选举出一个来执行故障转移,同时 3 个哨兵的 majority 是 2,所以还剩下的 2 个哨兵运行着,就可以允许执行故障转移。
10.3 redis 哨兵主备切换的数据丢失问题
1. 主备切换的过程,可能会导致数据丢失:
- 异步复制导致的数据丢失, 客户端向 master 中写数据成功后,就直接返回成功了,然后 master 再异步的向 slave 中同步数据。
- 所以可能有部分数据还没复制到 slave,master 就宕机了,此时这部分数据就丢失了。
2. 脑裂导致的数据丢失
-
脑裂,也就是说,某个 master 所在机器突然脱离了正常的网络,跟其他 slave 机器不能连接,但是实际上 master 还运行着。此时哨兵可能就会认为 master 宕机了,然后开启选举,将其他 slave 切换成了 master。这个时候,集群里就会有两个 master ,也就是所谓的脑裂。
-
此时虽然某个 slave 被切换成了 master,但是可能 client 还没来得及切换到新的 master,还继续向旧 master 写数据。因此旧 master 再次恢复的时候,会被作为一个 slave 挂到新的 master 上去,自己的数据会清空,重新从新的 master 复制数据。而新的 master 并没有后来 client 写入的数据,因此,这部分数据也就丢失了。
数据丢失问题降低损失解决方案
-
进行如下配置:
min-slaves-to-write 1 min-slaves-max-lag 10
-
表示,要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒。
-
如果说一旦所有的 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。
-
-
减少异步复制数据的丢失
- 有了 min-slaves-max-lag 这个配置,就可以确保说,一旦 slave 复制数据和 ack 延时太长,就认为可能 master 宕机后损失的数据太多了,那么就拒绝写请求,这样可以把 master 宕机时由于部分数据未同步到 slave 导致的数据丢失降低的可控范围内。
-
减少脑裂的数据丢失
- 如果一个 master 出现了脑裂,跟其他 slave 丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的 slave 发送数据,而且 slave 超过 10 秒没有给自己 ack 消息,那么就直接拒绝客户端的写请求。因此在脑裂场景下,最多就丢失 10 秒的数据。
-
当上面情况发生,master 拒绝接收请求后:
- 此时可以在客户端做降级,写到本地磁盘中,在客户端对外接收请求再做降级,减缓请求涌入速度
- 或者可以将数据临时存入消息队列,每个一小段时间获取一次,并尝试重新写入 Redis
10.4 sdown 和 odown 转换机制
- sdown 是主观宕机,就一个哨兵如果自己觉得一个 master 宕机了,那么就是主观宕机
- odown 是客观宕机,如果 quorum 数量的哨兵都觉得一个 master 宕机了,那么就是客观宕机
- sdown 达成的条件很简单,如果一个哨兵 ping 一个 master,超过了 is-master-down-after-milliseconds 指定的毫秒数之后,就主观认为 master 宕机了;
- 如果一个哨兵在指定时间内,收到了 quorum 数量的其它哨兵也认为那个 master 是 sdown 的,那么就认为是 odown 了。
10.5 哨兵集群的自动发现机制
哨兵互相之间的发现,是通过 redis 的 pub/sub 系统实现的,每个哨兵都会往 sentinel:hello 这个 channel 里发送一个消息,这时候所有其他哨兵都可以消费到这个消息,并感知到其他的哨兵的存在。
每隔两秒钟,每个哨兵都会往自己监控的某个 master+slaves 对应的 sentinel:hello channel 里发送一个消息,内容是自己的 host、ip 和 runid 还有对这个 master 的监控配置。
每个哨兵也会去监听自己监控的每个 master+slaves 对应的 sentinel:hello channel,然后去感知到同样在监听这个 master+slaves 的其他哨兵的存在。
每个哨兵还会跟其他哨兵交换对 master 的监控配置,互相进行监控配置的同步。
10.6 slave 配置的自动纠正
哨兵会负责自动纠正 slave 的一些配置,比如 slave 如果要成为潜在的 master 候选人,哨兵会确保 slave 复制现有 master 的数据;如果 slave 连接到了一个错误的 master 上,比如故障转移之后,那么哨兵会确保它们连接到正确的 master 上。
10.7 slave->master 选举算法
-
选举会考虑 slave 的一些信息:
- 跟 master 断开连接的时长
- slave 优先级
- 复制 offset
- run id
-
如果一个 slave 跟 master 断开连接的时间已经超过了 down-after-milliseconds 的 10 倍,外加 master 宕机的时长,那么 slave 就被认为不适合选举为 master。
(down-after-milliseconds * 10) + milliseconds_since_master_is_in_SDOWN_state
-
接下来会对剩下的 slave 进行排序:
- 按照 slave 优先级进行排序,slave priority 越低,优先级就越高。
- 如果 slave priority 相同,那么看 replica offset,哪个 slave 复制了越多的数据,offset 越靠后,优先级就越高。
- 如果上面两个条件都相同,那么选择一个 run id 比较小的那个 slave。
10.8 quorum 和 majority
每次一个哨兵要做主备切换,首先需要 quorum 数量的哨兵认为 odown,然后选举出一个哨兵来做切换,这个哨兵还需要得到 majority 哨兵的授权,才能正式执行切换。
如果 quorum < majority,比如 5 个哨兵,majority 就是 3,quorum 设置为 2,那么就 3 个哨兵授权就可以执行切换。
但是如果 quorum >= majority,那么必须 quorum 数量的哨兵都授权,比如 5 个哨兵,quorum 是 5,那么必须 5 个哨兵都同意授权,才能执行切换。
10.9 configuration epoch
哨兵会对一套 redis master+slaves 进行监控,有相应的监控的配置。
执行切换的那个哨兵,会从要切换到的新 master(salve->master)那里得到一个 configuration epoch,这就是一个 version 号,每次切换的 version 号都必须是唯一的。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待 failover-timeout 时间,然后接替继续执行切换,此时会重新获取一个新的 configuration epoch,作为新的 version 号。
10.10 configuration 传播
哨兵完成切换之后,会在自己本地更新生成最新的 master 配置,然后同步给其他的哨兵,就是通过之前说的 pub/sub 消息机制。
这里之前的 version 号就很重要了,因为各种消息都是通过一个 channel 去发布和监听的,所以一个哨兵完成一次新的切换之后,新的 master 配置是跟着新的 version 号的。其他的哨兵都是根据版本号的大小来更新自己的 master 配置的。
11. redis 的持久化有哪几种方式?
持久化的意义在于故障恢复
redis 持久化的两种方式:RDB、AOF
-
通过 RDB 或 AOF,都可以将 redis 内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云等云服务。
-
如果 redis 挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动 redis,redis 就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务。
-
如果同时使用 RDB 和 AOF 两种持久化机制,那么在 redis 重启的时候,会使用 AOF 来重新构建数据,因为 AOF 中的数据更加完整。
1. RDB
-
RDB 持久化机制,是对 redis 中的数据执行周期性的持久化,一般是每隔5分钟持久化一次。
-
优点:
- RDB 会生成多个数据文件,每个数据文件都代表了某一个时刻中 redis 的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去,比如说 Amazon 的 S3 云服务上去,在国内可以是阿里云的 ODPS 分布式存储上,以预定好的备份策略来定期备份 redis 中的数据。
- RDB 对 redis 对外提供的读写服务,影响非常小,可以让 redis 保持高性能,因为 redis 主进程只需要 fork 一个子进程,让子进程执行磁盘 IO 操作来进行 RDB 持久化即可。
- 相对于 AOF 持久化机制来说,直接基于 RDB 数据文件来重启和恢复 redis 进程,更加快速。
-
缺点
- 如果想要在 redis 故障时,尽可能少的丢失数据,那么 RDB 没有 AOF 好。一般来说,RDB 数据快照文件,都是每隔 5 分钟,或者更长时间生成一次,这个时候就得接受一旦 redis 进程宕机,那么会丢失最近 5 分钟的数据。
- RDB 每次在 fork 子进程来执行 RDB 快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒。
2. AOF
-
AOF 机制对每条写入命令作为日志,以 append-only 的模式写入一个日志文件中
-
写文件并不是直接写进磁盘,而是先将每条命令写入 OS Cache,然后每隔 1 秒调用一次 fsync,强制将 OS Cache 中的数据刷入磁盘文件。
-
AOF 是存放每条写命令的,所以会不断的膨胀,但是内存中的数据是定量的,不能无限增长,所以当 AOF 文件大到一定程度后,会进行 rewrite 操作。rewrite 操作会基于当前 Redis 内存中的数据,来重新构造一个更小的
-
优点
- AOF 可以更好的保护数据不丢失,一般 AOF 会每隔 1 秒,通过一个后台线程执行一次fsync操作,最多丢失 1 秒钟的数据。
- AOF 日志文件以 append-only 模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复。
- AOF 日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在 rewrite log 的时候,会对其中的指令进行压缩,创建出一份需要恢复数据的最小日志出来。在创建新日志文件的时候,老的日志文件还是照常写入。当新的 merge 后的日志文件 ready 的时候,再交换新老日志文件即可。
- AOF 日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用 flushall 命令清空了所有数据,只要这个时候后台 rewrite 还没有发生,那么就可以立即拷贝 AOF 文件,将最后一条 flushall 命令给删了,然后再将该 AOF 文件放回去,就可以通过恢复机制,自动恢复所有数据。
-
缺点
- 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
AOF 开启后,支持的写 QPS 会比 RDB 支持的写 QPS 低,因为 AOF 一般会配置成每秒 fsync 一次日志文件,当然,每秒一次 fsync,性能也还是很高的。(如果实时写入,那么 QPS 会大降,redis 性能会大大降低) - 以前 AOF 发生过 bug,就是通过 AOF 记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似 AOF 这种较为复杂的基于命令日志 / merge / 回放的方式,比基于 RDB 每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有 bug。不过 AOF 就是为了避免 rewrite 过程导致的 bug,因此每次 rewrite 并不是基于旧的指令日志进行 merge 的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。
- 对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大。
RDB 和 AOF 到底该如何选择
- 不要仅仅使用 RDB,因为那样会导致你丢失很多数据;
- 也不要仅仅使用 AOF,因为那样有两个问题:第一,你通过 AOF 做冷备,没有 RDB 做冷备来的恢复速度更快;第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug;
- redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
12. redis 集群模式的工作原理能说一下么?
- 之前所说的 主从+哨兵 模式是存在一个瓶颈的,就是从节点存储的数据与主节点正常情况下是完全一致的,也就是说主节点的容量也决定了从节点的容量,即便有多个从节点。
- 自动将数据进行分片,每个 master 上放一部分数据
- 提供内置的高可用支持,部分 master 不可用时,还是可以继续工作的
- 简单说就是,多个 主从+哨兵
12.1 分布式寻址算法
- hash 算法(大量缓存重建)
- 一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)
- redis cluster 的 hash slot 算法
1. hash 算法
- 来了一个 key,首先计算 hash 值,然后对 master 节点数取模。然后根据结果打在不同的 master 节点上。
- 一旦某一个 master 节点宕机,所有请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。这会导致所有的请求过来,绝大部分无法拿到有效的缓存,导致大量的流量涌入数据库。
- 比如原先 key=123,123%3=0,数据存入master1节点,当master2节点宕机后,节点数量为 2,此时来查询 key=123 的数据时,123%2=1,就会去现在master3节点去查询,但是数据并不在这个节点,就会导致 key=123 的数据失效,绝大部分类似
2.一致性 hash 算法
- 一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能确定每个节点在其哈希环上的位置。
- 来了一个 key,首先计算 hash 值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。
- 在一致性哈希算法中,如果一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增加一个节点也同理。
- 然而,一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
3. redis cluster 的 hash slot 算法
- redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。
- redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。
- hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。
- 客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。
- 任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。
12.2 redis cluster 的高可用与主备切换原理
- redis cluster 的高可用的原理,几乎跟哨兵是类似的。
判断节点宕机
- 如果一个节点认为另外一个节点宕机,那么就是 pfail,主观宕机。如果多个节点都认为另外一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎一样。
- 在 cluster-node-timeout 内,某个节点一直没有返回 pong,那么就被认为 pfail。
- 如果一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其他节点,如果超过半数的节点都认为 pfail 了,那么就会变成 fail。
- ( 类似哨兵的 sdown,odown)
从节点过滤
- 对宕机的 master node,从其所有的 slave node 中,选择一个切换成 master node。
- 检查每个 slave node 与 master node 断开连接的时间,如果超过了 cluster-node-timeout * cluster-slave-validity-factor,那么就没有资格切换成 master。
从节点选举
- 每个从节点,都根据自己对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。
- 所有的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,如果大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举通过,那个从节点可以切换成 master。
- 从节点执行主备切换,从节点切换为主节点。
与哨兵比较
- 整个流程跟哨兵相比,非常类似,所以说,redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能。
12.3 节点间的内部通信机制
基本通信原理
-
集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通信。
-
集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型代表,就是大数据领域的 storm。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对所有元数据进行存储维护。
- 好处在于,元数据的读取和更新,时效性非常好,一旦元数据出现了变更,就立即更新到集中式的存储中,其它节点读取的时候就可以感知到
- 缺点在于,所有的元数据的更新压力全部集中在一个地方,可能会导致元数据的存储有压力
-
gossip 协议,所有节点都持有一份元数据,不同的节点如果出现了元数据的变更,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变更。
- 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到所有节点上去更新,降低了压力;
- 缺点在于,元数据的更新有延时,可能导致集群中的一些操作会有一些滞后。
- 在 redis cluster 架构下,每个 redis 要放开两个端口号,比如一个是 6379,另外一个就是 加1w 的端口号,比如 16379。
- 16379 端口号是用来进行节点间通信的,也就是 cluster bus 的东西,cluster bus 的通信
- 每个节点每隔一段时间都会通过 +1w 的端口往另外几个节点发送 ping 消息,同时其它几个节点接收到 ping 之后返回 pong。
- 交换的信息:信息包括故障信息,节点的增加和删除,hash slot 信息等等。
gossip 协议
-
gossip 协议包含多种消息,包含 ping,pong,meet,fail 等等。
-
meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,然后新节点就会开始与其它节点进行通信。
redis-trib.rb add-node
其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入我们的集群。
-
ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
-
pong:返回 ping 和 meeet,包含自己的状态和其它信息,也用于信息广播和更新。
-
fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说,某个节点宕机啦。
ping 消息深入
- ping 时要携带一些元数据,如果很频繁,可能会加重网络负担。
- 每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。当然如果发现某个节点通信延时达到了 cluster_node_timeout / 2,那么立即发送 ping,避免数据交换延时过长,落后的时间太长了。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout 可以调节,如果调得比较大,那么会降低 ping 的频率。
- 每次 ping,会带上自己节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。
12.4 面向集群的jedis内部实现原理
- jedis是redis cluster的java client客户端,jedis cluster api
1. 请求重定向
- 客户端可能会挑选任意一个redis实例去发送命令,每个redis实例接收到命令,都会计算key对应的hash slot
- 如果 hash slot 在当前节点就在当前节点处理,否则返回 moved 给客户端,让客户端进行重定向
cluster keyslot mykey
,可以查看一个key对应的hash slot是什么- 用redis-cli的时候,可以加入-c参数,支持自动的请求重定向,redis-cli接收到moved之后,会自动重定向到对应的节点执行命令
2. hash slot查找
- 节点间通过gossip协议进行数据交换,就知道每个hash slot在哪个节点上
3. smart jedis
-
什么是smart jedis
- 上面我们也说了jedis一开始是基于客户端进行重定向,很消耗网络IO,因为大部分情况下,可能都会出现一次请求重定向,才能找到正确的节点
- 所以大部分的客户端,比如java redis客户端,就是jedis,都是smart的
- 本地维护一份hashslot -> node的映射表,缓存,大部分情况下,直接走本地缓存就可以找到hashslot -> node,不需要通过节点进行moved重定向
-
JedisCluster的工作原理(含如果数据迁移了的数据寻找过程)
- 在JedisCluster初始化的时候,就会随机选择一个node,初始化hashslot -> node映射表,同时为每个节点创建一个JedisPool连接池
- 每次基于JedisCluster执行操作,首先JedisCluster都会在本地计算key的hashslot,然后在本地映射表找到对应的节点
- 如果那个node正好还是持有那个hashslot,那么就ok; 如果说进行了reshard这样的操作,可能hashslot已经不在那个node上了,就会返回moved(表示该hashslot已不在这个node了)
- 如果JedisCluter API发现对应的节点返回moved,那么利用该节点的元数据,更新本地的hashslot -> node映射表缓存
- 重复上面几个步骤,直到找到对应的节点,如果重试超过5次,那么就报错,JedisClusterMaxRedirectionException
- jedis老版本,可能会出现在集群某个节点故障还没完成自动切换恢复时,频繁更新hash slot,频繁ping节点检查活跃,导致大量网络IO开销
- jedis最新版本,对于这些过度的hash slot更新和ping,都进行了优化,避免了类似问题
-
hashslot迁移和ask重定向
- 如果hash slot正在迁移,那么会返回ask重定向给jedis
- jedis接收到ask重定向之后,会重新定位到目标节点去执行,但是因为ask发生在hash slot迁移过程中,所以JedisCluster API收到ask是不会更新hashslot本地缓存
- 只有确定说,hashslot已经迁移完了,返回moved是会更新本地hashslot->node映射表缓存的
13. 了解什么是 redis 的雪崩、穿透和击穿?
13.1 缓存雪崩
-
对于系统 A,假设每天高峰期每秒 5000 个请求,
-
本来缓存在高峰期可以扛住每秒 4000 个请求,但是缓存机器意外发生了全盘宕机。
-
缓存挂了,此时 1 秒 5000 个请求全部落数据库,数据库必然扛不住,它会报一下警,然后就挂了。
-
此时,如果没有采用什么特别的方案来处理这个故障,DBA 重启数据库,但是数据库立马又被新的流量给打死了。
-
这就是缓存雪崩。
-
大约在 3 年前,国内比较知名的一个互联网公司,曾因为缓存事故,导致雪崩,后台系统全部崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。
缓存雪崩的事前事中事后的解决方案如下。
-
事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。
-
事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。
- 用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,如果没查到再查 redis。如果 ehcache 和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。
- 限流组件,可以设置每秒的请求,有多少能通过组件,剩余的未通过的请求,怎么办?走降级!可以返回一些默认的值,或者友情提示,或者空白的值。
限流好处:
- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能通过。
- 只要数据库不死,就是说,对用户来说,一部分的请求都是可以被处理的。
- 只要有一部分的请求可以被处理,就意味着你的系统没死,对用户来说,可能就是点击几次刷不出来页面,但是多点几次,就可以刷出来一次。
-
事后:redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
13.2 缓存穿透
- 对于系统A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。
- 黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。
- 举个栗子。数据库 id 是从 1 开始的,结果黑客发过来的请求 id 全部都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。
- 解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,比如 set -999 UNKNOWN。然后设置一个过期时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效之前,都可以直接从缓存中取数据。
13.3 缓存击穿
- 缓存击穿,就是说某个 key 非常热点,访问非常频繁,处于集中式高并发访问的情况,当这个 key 在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。
- 解决方式也很简单,可以将热点数据设置为永远不过期;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存之后,再释放锁,进而其它请求才能通过该 key 访问数据。
14. 如何保证缓存与数据库的双写一致性?
14.1 Cache Aside Pattern
- 最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。
- 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
- 更新的时候,先更新数据库,然后再删除缓存。
为什么是删除缓存,而不是更新缓存?
- 原因很简单,很多时候,在复杂点的缓存场景,缓存不单单是数据库中直接取出来的值。
- 比如可能更新了某个表的一个字段,然后其对应的缓存,是需要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。
- 另外更新缓存的代价有时候是很高的。是不是说,每次修改数据库的时候,都一定要将其对应的缓存更新一份?也许有的场景是这样,但是对于比较复杂的缓存数据计算的场景,就不是这样了。如果你频繁修改一个缓存涉及的多个表,缓存也频繁更新。但是问题在于,这个缓存到底会不会被频繁访问到?
- 其实删除缓存,而不是更新缓存,就是一个 懒加载 的思想,不要每次都重新做复杂的计算,不管它会不会用到,而是让它到需要被使用的时候再重新计算。像 mybatis,hibernate,都有懒加载思想。
14.2 最初级的缓存不一致问题及解决方案
- 问题:先更新数据库,再删除缓存。如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据就出现了不一致。
- 解决思路:先删除缓存,再更新数据库。如果数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。因为读的时候缓存没有,所以去读了数据库中的旧数据,然后更新到缓存中。
14.3 比较复杂的数据不一致问题分析
数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变更的程序完成了数据库的修改。完了,数据库和缓存中的数据不一样了…
为什么上亿流量高并发场景下,缓存会出现这个问题?
- 只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。
- 如果并发量很低的话,特别是读并发很低,每天访问量就 1 万次,那么很少的情况下,会出现刚才描述的那种不一致的场景。
- 但是问题是,如果每天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的情况。
解决方案:数据库与缓存更新/读取操作进行异步串行化
-
更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个 jvm 内部队列中。
-
一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
-
这里有几个优化点
- 一个队列中,如果更新数据库请求后面跟了好几个读请求,多个连续的读请求,也就包含了多个更新缓存的请求,多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中更新数据库请求后面跟了好几个读请求,那么就不用再放个更新缓存请求操作进去了,直接等待前面的更新操作请求完成,后续读请求直接从缓存读取即可。
- 如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
高并发的场景下,该解决方案要注意的问题:
-
读请求长时阻塞
- 由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回。
- 该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库。务必通过一些模拟真实的测试,看看更新数据的频率是怎样的。
- 另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作。如果一个内存队列里居然会挤压 100 个商品的库存修改操作,每隔库存修改操作要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 10 * 100 = 1000ms = 1s 后,才能得到数据,这个时候就导致读请求的长时阻塞。
- 一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会 hang 多少时间,如果读请求在 200ms 返回,如果你计算过后,哪怕是最繁忙的时候,积压 10 个更新操作,最多等待 200ms,那还可以的。
- 如果一个内存队列中可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少。
- 其实根据之前的项目经验,一般来说,数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的。像这种针对读高并发、读缓存架构的项目,一般来说写请求是非常少的,每秒的 QPS 能到几百就不错了。
- 我们来实际粗略测算一下。
- 如果一秒有 500 的写操作,如果分成 5 个时间片,每 200ms 就 100 个写操作,放到 20 个内存队列中,每个内存队列,可能就积压 5 个写操作。每个写操作性能测试后,一般是在 20ms 左右就完成,那么针对每个内存队列的数据的读请求,也就最多 hang 一会儿,200ms 以内肯定能返回了。
- 经过刚才简单的测算,我们知道,单机支撑的写 QPS 在几百是没问题的,如果写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每个机器 20 个队列。
-
读请求并发量过高
- 这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时 hang 在服务上,看服务能不能扛的住,需要多少机器才能扛住最大的极限情况的峰值。
- 但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大。
-
多服务实例部署的请求路由
- 可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过 Nginx 服务器路由到相同的服务实例上。
- 比如说,对同一个商品的读写请求,全部路由到同一台机器上。可以自己去做服务间的按照某个请求参数的 hash 路由,也可以用 Nginx 的 hash 路由功能等等。
-
热点商品的路由问题,导致请求的倾斜
- 万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能会造成某台机器的压力过大。
- 就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以其实要根据业务系统去看,如果更新频率不是太高的话,这个问题的影响并不是特别大,但是的确可能某些机器的负载会高一些。
15. redis 的并发竞争问题是什么?
- 就是多客户端同时并发写一个 key,可能本来应该先到的数据后到了,导致数据版本错了;或者是多客户端同时获取一个 key,修改值之后再写回去,只要顺序错了,数据就错了。
- 解决:
- 使用 分布式锁 CAS 思想
- 你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。
- 每次要写之前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
16. 生产环境中的 redis 是怎么部署的?
- 仅供参考的简单版本
redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例,每个主实例挂了一个从实例,5 个节点对外提供读写服务,每个节点的读写高峰qps可能可以达到每秒 5 万,5 台机器最多是 25 万读写请求/s。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。
5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。