Redis面试题
2022-07-14 19:32:31 0 举报
AI智能生成
Redis面试题
作者其他创作
大纲/内容
Redis基础问题
什么是Redis?
Redis 本质上是一个Key-Value 类型的内存数据库,很像memcached,整个数据库统统
加载在内存当中进行操作,定期通过异步操作把数据库数据flush 到硬盘上进行保存。因为
是纯内存操作,Redis 的性能非常出色,每秒可以处理超过10 万次读写操作,是已知性能
最快的Key-Value DB。
Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个
value 的最大限制是1GB,不像memcached 只能保存1MB 的数据,因此Redis 可以用
来实现很多有用的功能,比方说用他的List 来做FIFO 双向链表,实现一个轻量级的高性能
消息队列服务,用他的Set 可以做高性能的tag 系统等等。另外Redis 也可以对存入的
Key-Value 设置expire 时间,因此也可以被当作一个功能加强版的memcached 来用。
Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因
此Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
加载在内存当中进行操作,定期通过异步操作把数据库数据flush 到硬盘上进行保存。因为
是纯内存操作,Redis 的性能非常出色,每秒可以处理超过10 万次读写操作,是已知性能
最快的Key-Value DB。
Redis 的出色之处不仅仅是性能,Redis 最大的魅力是支持保存多种数据结构,此外单个
value 的最大限制是1GB,不像memcached 只能保存1MB 的数据,因此Redis 可以用
来实现很多有用的功能,比方说用他的List 来做FIFO 双向链表,实现一个轻量级的高性能
消息队列服务,用他的Set 可以做高性能的tag 系统等等。另外Redis 也可以对存入的
Key-Value 设置expire 时间,因此也可以被当作一个功能加强版的memcached 来用。
Redis 的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因
此Redis 适合的场景主要局限在较小数据量的高性能操作和运算上。
Redis 的全称是什么?
Remote Dictionary Server。
Remote Dictionary Server。
Redis 官方为什么不提供Windows 版本?
因为目前Linux 版本已经相当稳定,而且用户量很大,无需开发windows 版本,反而会带来
兼容性等问题。
因为目前Linux 版本已经相当稳定,而且用户量很大,无需开发windows 版本,反而会带来
兼容性等问题。
Redis 主要消耗什么物理资源?
内存。
内存。
为什么Redis 需要把所有数据放到内存中?
Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。
所以Redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O 速度为严重
影响Redis 的性能。在内存越来越便宜的今天,Redis 将会越来越受欢迎。
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
Redis 为了达到最快的读写速度将数据都读到内存中,并通过异步的方式将数据写入磁盘。
所以Redis 具有快速和数据持久化的特征。如果不将数据放在内存中,磁盘I/O 速度为严重
影响Redis 的性能。在内存越来越便宜的今天,Redis 将会越来越受欢迎。
如果设置了最大使用的内存,则数据已有记录数达到内存限值后不能继续插入新值。
Redis 如何设置密码及验证密码?
设置密码:config set requirepass 123456
授权密码:auth 123456
授权密码:auth 123456
怎么测试Redis 的连通性?
ping
Redis 是单线程还是多线程?
这个问题应该已经看到过无数次了,最近 redis 6 出来之后又被翻出来了。
redis 4.0 之前,redis 是完全单线程的。
redis 4.0 时,redis 引入了多线程,但是额外的线程只是用于后台处理,例如:删除对象,核心流程还是完全单线程的。
这也是为什么有些人说 4.0 是单线程的,因为他们指的是核心流程是单线程的。
这边的核心流程指的是 redis 正常处理客户端请求的流程,通常包括:接收命令、解析命令、执行命令、返回结果等。
而在最近,redis 6.0 版本又一次引入了多线程概念,与 4.0 不同的是,这次的多线程会涉及到上述的核心流程。
redis 6.0 中,多线程主要用于网络 I/ O 阶段,也就是接收命令和写回结果阶段,而在执行命令阶段,还是由单线程串行执行。
由于执行时还是串行,因此无需考虑并发安全问题。
值得注意的时,redis 中的多线程组不会同时存在“读”和“写”,这个多线程组只会同时“读”或者同时“写”。
redis 6.0 加入多线程 I/O 之后,处理命令的核心流程如下:
1、当有读事件到来时,主线程将该客户端连接放到全局等待读队列
2、读取数据:
1)主线程将等待读队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
2)同时主线程也会自己负责处理一个客户端连接的读事件;
3)当主线程处理完该连接的读事件后,会自旋等待所有 I/O 线程处理完毕
3、命令执行:
主线程按照事件被加入全局等待读队列的顺序(这边保证了执行顺序是正确的),串行执行客户端命令,然后将客户端连接放到全局等待写队列
4、写回结果:跟等待读队列处理类似,主线程将等待写队列的客户端连接使用轮询调度算法分配给 I/O 线程处理,
同时自己也会处理一个,当主线程处理完毕后,会自旋等待所有 I/O 线程处理完毕,最后清空队列。
大致流程图如下:
redis 4.0 之前,redis 是完全单线程的。
redis 4.0 时,redis 引入了多线程,但是额外的线程只是用于后台处理,例如:删除对象,核心流程还是完全单线程的。
这也是为什么有些人说 4.0 是单线程的,因为他们指的是核心流程是单线程的。
这边的核心流程指的是 redis 正常处理客户端请求的流程,通常包括:接收命令、解析命令、执行命令、返回结果等。
而在最近,redis 6.0 版本又一次引入了多线程概念,与 4.0 不同的是,这次的多线程会涉及到上述的核心流程。
redis 6.0 中,多线程主要用于网络 I/ O 阶段,也就是接收命令和写回结果阶段,而在执行命令阶段,还是由单线程串行执行。
由于执行时还是串行,因此无需考虑并发安全问题。
值得注意的时,redis 中的多线程组不会同时存在“读”和“写”,这个多线程组只会同时“读”或者同时“写”。
redis 6.0 加入多线程 I/O 之后,处理命令的核心流程如下:
1、当有读事件到来时,主线程将该客户端连接放到全局等待读队列
2、读取数据:
1)主线程将等待读队列的客户端连接通过轮询调度算法分配给 I/O 线程处理;
2)同时主线程也会自己负责处理一个客户端连接的读事件;
3)当主线程处理完该连接的读事件后,会自旋等待所有 I/O 线程处理完毕
3、命令执行:
主线程按照事件被加入全局等待读队列的顺序(这边保证了执行顺序是正确的),串行执行客户端命令,然后将客户端连接放到全局等待写队列
4、写回结果:跟等待读队列处理类似,主线程将等待写队列的客户端连接使用轮询调度算法分配给 I/O 线程处理,
同时自己也会处理一个,当主线程处理完毕后,会自旋等待所有 I/O 线程处理完毕,最后清空队列。
大致流程图如下:
大致流程图
大致流程图
为什么 Redis 是单线程?
在 redis 6.0 之前,redis 的核心操作是单线程的。
因为 redis 是完全基于内存操作的,通常情况下 CPU 不会是 redis 的瓶颈,redis 的瓶颈最有可能是机器内存的大小或者网络带宽。
既然 CPU 不会成为瓶颈,那就顺理成章地采用单线程方案了,因为如果使用多线程会更复杂,同时需引入上下文切换、加锁等,会带来额外性能消耗。
而随着近些年互联网的不断发展,大家对于缓存的性能要求也越来越高了,因此 redis 也开始在逐渐往多线程方向发展。
最近的 6.0 版本就对核心流程引入了多线程,主要用于解决 redis 在网络 I/O 上的性能瓶颈。而对于核心的命令执行阶段,目前还是单线程的。
因为 redis 是完全基于内存操作的,通常情况下 CPU 不会是 redis 的瓶颈,redis 的瓶颈最有可能是机器内存的大小或者网络带宽。
既然 CPU 不会成为瓶颈,那就顺理成章地采用单线程方案了,因为如果使用多线程会更复杂,同时需引入上下文切换、加锁等,会带来额外性能消耗。
而随着近些年互联网的不断发展,大家对于缓存的性能要求也越来越高了,因此 redis 也开始在逐渐往多线程方向发展。
最近的 6.0 版本就对核心流程引入了多线程,主要用于解决 redis 在网络 I/O 上的性能瓶颈。而对于核心的命令执行阶段,目前还是单线程的。
Redis 为什么使用单进程、单线程也很快?
主要有以下几点:
1、基于内存的操作
2、使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器
3、单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。
4、以上这三点是 redis 性能高的主要原因,其他的还有一些小优化,例如:对数据结构进行了优化,简单动态字符串、压缩列表等。
1、基于内存的操作
2、使用了 I/O 多路复用模型,select、epoll 等,基于 reactor 模式开发了自己的网络事件处理器
3、单线程可以避免不必要的上下文切换和竞争条件,减少了这方面的性能消耗。
4、以上这三点是 redis 性能高的主要原因,其他的还有一些小优化,例如:对数据结构进行了优化,简单动态字符串、压缩列表等。
Redis 的网络事件处理器(Reactor 模式)
redis 基于 reactor 模式开发了自己的网络事件处理器,
由 4 个部分组成:套接字、I/O 多路复用程序、文件事件分派器(dispatcher)、以及事件处理器。
图解Reactor 模式
子主题
套接字:
socket 连接,也就是客户端连接。
当一个套接字准备好执行连接、写入、读取、关闭等操作时, 就会产生一个相应的文件事件。
因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。
I/ O 多路复用程序:
提供 select、epoll、evport、kqueue 的实现,会根据当前系统自动选择最佳的方式。
负责监听多个套接字,当套接字产生事件时,会向文件事件分派器传送那些产生了事件的套接字。
当多个文件事件并发出现时, I/O 多路复用程序会将所有产生事件的套接字都放到一个队列里面,
然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字:
当上一个套接字产生的事件被处理完毕之后,才会继续传送下一个套接字。
文件事件分派器:
接收 I/O 多路复用程序传来的套接字, 并根据套接字产生的事件的类型, 调用相应的事件处理器。
事件处理器:
事件处理器就是一个个函数, 定义了某个事件发生时, 服务器应该执行的动作。
例如:建立连接、命令查询、命令写入、连接关闭等等。
socket 连接,也就是客户端连接。
当一个套接字准备好执行连接、写入、读取、关闭等操作时, 就会产生一个相应的文件事件。
因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。
I/ O 多路复用程序:
提供 select、epoll、evport、kqueue 的实现,会根据当前系统自动选择最佳的方式。
负责监听多个套接字,当套接字产生事件时,会向文件事件分派器传送那些产生了事件的套接字。
当多个文件事件并发出现时, I/O 多路复用程序会将所有产生事件的套接字都放到一个队列里面,
然后通过这个队列,以有序、同步、每次一个套接字的方式向文件事件分派器传送套接字:
当上一个套接字产生的事件被处理完毕之后,才会继续传送下一个套接字。
文件事件分派器:
接收 I/O 多路复用程序传来的套接字, 并根据套接字产生的事件的类型, 调用相应的事件处理器。
事件处理器:
事件处理器就是一个个函数, 定义了某个事件发生时, 服务器应该执行的动作。
例如:建立连接、命令查询、命令写入、连接关闭等等。
Redis技术选型
在选择缓存时,什么时候选择redis,什么时候选择memcached
两者都是非关系型内存键值数据库,现在公司一般都是用 Redis 来实现缓存,
而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:
而且 Redis 自身也越来越强大了!Redis 与 Memcached 主要有以下不同:
适用场景
选择Redis的情况
1、复杂数据结构,value的数据是哈希,列表,集合,有序集合等这种情况下,会选择redis, 因为memcache无法满足这些数据结构,最典型的的使用场景是,用户订单列表,用户消息,帖子评论等。
2、需要进行数据的持久化功能,但是注意,不要把redis当成数据库使用,如果redis挂了,内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有cache预热的过程。对于只读和数据一致性要求不高的场景可以采用持久化存储
3、高可用,redis支持集群,可以实现主动复制,读写分离,而对于memcache如果想要实现高可用,需要进行二次开发。
4、存储的内容比较大,memcache存储的value最大为1M。
2、需要进行数据的持久化功能,但是注意,不要把redis当成数据库使用,如果redis挂了,内存能够快速恢复热数据,不会将压力瞬间压在数据库上,没有cache预热的过程。对于只读和数据一致性要求不高的场景可以采用持久化存储
3、高可用,redis支持集群,可以实现主动复制,读写分离,而对于memcache如果想要实现高可用,需要进行二次开发。
4、存储的内容比较大,memcache存储的value最大为1M。
复杂数据结构,有持久化,高可用需求,value存储内容较大
选择Memcached的场景
1、纯KV,数据量非常大的业务,使用memcache更合适,原因是,
a)memcache的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间,redis是临时申请空间,可能导致碎片化。
b)虚拟内存使用,memcache将所有的数据存储在物理内存里,redis有自己的vm机制,理论上能够存储比物理内存更多的数据,当数据超量时,引发swap,把冷数据刷新到磁盘上,从这点上,数据量大时,memcache更快
c)网络模型,memcache使用非阻塞的IO复用模型,redis也是使用非阻塞的IO复用模型,但是redis还提供了一些非KV存储之外的排序,聚合功能,复杂的CPU计算,会阻塞整个IO调度,从这点上由于redis提供的功能较多,memcache更快些
d) 线程模型,memcache使用多线程,主线程监听,worker子线程接受请求,执行读写,这个过程可能存在锁冲突。
redis使用的单线程,虽然无锁冲突,但是难以利用多核
的特性提升吞吐量。
1、纯KV,数据量非常大的业务,使用memcache更合适,原因是,
a)memcache的内存分配采用的是预分配内存池的管理方式,能够省去内存分配的时间,redis是临时申请空间,可能导致碎片化。
b)虚拟内存使用,memcache将所有的数据存储在物理内存里,redis有自己的vm机制,理论上能够存储比物理内存更多的数据,当数据超量时,引发swap,把冷数据刷新到磁盘上,从这点上,数据量大时,memcache更快
c)网络模型,memcache使用非阻塞的IO复用模型,redis也是使用非阻塞的IO复用模型,但是redis还提供了一些非KV存储之外的排序,聚合功能,复杂的CPU计算,会阻塞整个IO调度,从这点上由于redis提供的功能较多,memcache更快些
d) 线程模型,memcache使用多线程,主线程监听,worker子线程接受请求,执行读写,这个过程可能存在锁冲突。
redis使用的单线程,虽然无锁冲突,但是难以利用多核
的特性提升吞吐量。
纯key-value,数据量非常大,并发量非常大的业务
类型
Redis
1. 支持内存 2. 非关系型数据库
Memcached
1. 支持内存 2. 键值对形式 3.缓存形式
数据存储类型
Redis
1. String 2. List 3.Set 4.Hash 5.Sort Set【俗称ZSet】
Memcached
1. 文本型 2. 二进制类型
查询【操作】类型
Redis
1. 批量操作 2. 事务支持 3. 每个类型不同的CRUD
Memcached
1.常用的CRUD 2.少量的其他命令
附加功能
Redis
1. 发布/订阅模式 2.主从分区3. 序列化支持 4. 脚本支持【Lua脚本】
Memcached
1. 多线程服务支持
网络IO模型
Redis
1. 单线程的多路 IO复用模型
Memcached
1. 多线程,非阻塞IO模式
事件库
Redis
自封转简易事件库AeEvent
Memcached
贵族血统的LibEvent事件库
持久化支持
Redis
1. RDB 2.AOF
Memcached
不支持
集群模式
Redis
原生支持cluster 模式,可以实现主从复制,读写分离
Memcached
没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据
内存管理机制
Redis
在 Redis中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value交换到磁盘
Memcached
Memcached 的数据则会一直在内存中,Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。
但是这种方式会使得内存的利用率不高,例如块的大小为 128bytes,只存储 100bytes 的数据,那么剩下的28 bytes就浪费掉了。
但是这种方式会使得内存的利用率不高,例如块的大小为 128bytes,只存储 100bytes 的数据,那么剩下的28 bytes就浪费掉了。
Redis 相比memcached 有哪些优势?
(1) memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
(2) redis的速度比memcached快很多
(3) redis可以持久化其数据
(2) redis的速度比memcached快很多
(3) redis可以持久化其数据
Redis 和 Memcached 的比较
1)数据结构:memcached 支持简单的 key-value 数据结构,而 redis 支持丰富的数据结构:String、List、Set、Hash、 SortedSet 等。
2)数据存储:memcached 和 redis 的数据都是全部在内存中。
网上有一种说法 “当物理内存用完时,Redis 可以将一些很久没用到的 value 交换到磁盘,同时在内存中清除”,
这边指的是 redis 里的虚拟内存(Virtual Memory)功能,该功能在 Redis 2.0 被引入,
但是在 Redis 2.4 中被默认关闭,并标记为废弃,而在后续版中被完全移除。
3)持久化:memcached 不支持持久化,redis 支持将数据持久化到磁盘
4)灾难恢复:实例挂掉后,memcached 数据不可恢复,redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题
5)事件库:memcached 使用 Libevent 事件库,redis 自己封装了简易事件库 AeEvent
6)过期键删除策略:memcached 使用惰性删除,redis 使用惰性删除+定期删除
7)内存驱逐(淘汰)策略:memcached 主要为 LRU 算法,redis 当前支持 8 种淘汰策略,见本文第 16 题
8)性能比较
2)数据存储:memcached 和 redis 的数据都是全部在内存中。
网上有一种说法 “当物理内存用完时,Redis 可以将一些很久没用到的 value 交换到磁盘,同时在内存中清除”,
这边指的是 redis 里的虚拟内存(Virtual Memory)功能,该功能在 Redis 2.0 被引入,
但是在 Redis 2.4 中被默认关闭,并标记为废弃,而在后续版中被完全移除。
3)持久化:memcached 不支持持久化,redis 支持将数据持久化到磁盘
4)灾难恢复:实例挂掉后,memcached 数据不可恢复,redis 可通过 RDB、AOF 恢复,但是还是会有数据丢失问题
5)事件库:memcached 使用 Libevent 事件库,redis 自己封装了简易事件库 AeEvent
6)过期键删除策略:memcached 使用惰性删除,redis 使用惰性删除+定期删除
7)内存驱逐(淘汰)策略:memcached 主要为 LRU 算法,redis 当前支持 8 种淘汰策略,见本文第 16 题
8)性能比较
- 按“CPU 单核” 维度比较:由于 Redis 只使用单核,而 Memcached 可以使用多核,所以在比较上:在处理小数据时,平均每一个核上 Redis 比 Memcached 性能更高,而在 100k 左右的大数据时, Memcached 性能要高于 Redis。
- 按“实例”维度进行比较:由于 Memcached 多线程的特性,在 Redis 6.0 之前,通常情况下 Memcached 性能是要高于 Redis 的,同时实例的 CPU 核数越多,Memcached 的性能优势越大。
- 至于网上说的 redis 的性能比 memcached 快很多,这个说法就离谱。
Redis与Memcached的区别?
为什么高并发下有时单线程的redis比多线程的memcached效率要高?
为什么高并发下有时单线程的redis比多线程的memcached效率要高?
区别:
1.mc可缓存图片和视频。rd支持除k/v更多的数据结构;
2.rd可以使用虚拟内存,rd可持久化和aof灾难恢复,rd通过主从支持数据备份;
3.rd可以做消息队列。
原因:mc多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
1.mc可缓存图片和视频。rd支持除k/v更多的数据结构;
2.rd可以使用虚拟内存,rd可持久化和aof灾难恢复,rd通过主从支持数据备份;
3.rd可以做消息队列。
原因:mc多线程模型引入了缓存一致性和锁,加锁带来了性能损耗。
Redis 与其他key-value 存储有什么不同?
Redis 有着更为复杂的数据结构并且提供对他们的原子性操作,这是一个不同于其他数据库
的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外
的抽象。
Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡
内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上
相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis 可以做很多内部复杂性
很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需
要进行随机访问。
的进化路径。Redis 的数据类型都是基于基本数据结构的同时对程序员透明,无需进行额外
的抽象。
Redis 运行在内存中但是可以持久化到磁盘,所以在对不同数据集进行高速读写时需要权衡
内存,应为数据量不能大于硬件内存。在内存数据库方面的另一个优点是, 相比在磁盘上
相同的复杂的数据结构,在内存中操作起来非常简单,这样Redis 可以做很多内部复杂性
很强的事情。同时,在磁盘格式方面他们是紧凑的以追加的方式产生的,因为他们并不需
要进行随机访问。
Redis数据类型底层数据结构
Redis 常见的数据结构
基础的 5 种: String:字符串,最基础的数据类型。 List:列表。 Hash:哈希对象。 Set:集合。 Sorted Set:有序集合,Set 的基础上加了个分值。
高级的 4 种:
确值,而是一个带有 0.81%标准差(standard error)的近似值。所以,HyperLogLog 适用于一些对于统计结果精
确度要求不是特别高的场景,例如网站的 UV 统计。
位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
- HyperLogLog:
确值,而是一个带有 0.81%标准差(standard error)的近似值。所以,HyperLogLog 适用于一些对于统计结果精
确度要求不是特别高的场景,例如网站的 UV 统计。
- Geo:
位置的距离、根据给定地理位置坐标获取指定范围内的地理位置集合。
- Bitmap:位图。
- Stream:
提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
Redis当中有哪些数据结构
字符串String、字典Hash、列表List、集合Set、有序集合SortedSet。
如果是高级用户,那么还会有,如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
如果是高级用户,那么还会有,如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLog、Geo、Pub/Sub。
Redis 支持哪几种数据类型?
String、List、Set、Sorted Set、hashes
一个字符串类型的值能存储最大容量是多少?
512M
512M
一个Redis 实例最多能存放多少的keys?
List、Set、Sorted Set 他们最多能存放多少元素?
理论上Redis 可以处理多达232 的keys,并且在实际中进行了测试,每个实例至少存放了2
亿5 千万的keys。我们正在测试一些较大的值。
任何list、set、和sorted set 都可以放232 个元素。
换句话说,Redis 的存储极限是系统中的可用内存值。
List、Set、Sorted Set 他们最多能存放多少元素?
理论上Redis 可以处理多达232 的keys,并且在实际中进行了测试,每个实例至少存放了2
亿5 千万的keys。我们正在测试一些较大的值。
任何list、set、和sorted set 都可以放232 个元素。
换句话说,Redis 的存储极限是系统中的可用内存值。
假如Redis里面有1亿个key,其中有10w个key是以某个固定的已知的前缀开头的,如果将它们全部找出来?
使用keys指令可以扫出指定模式的key列表。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。
keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。
这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,
但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?
这个时候你要回答redis关键的一个特性:redis的单线程的。
keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。
这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,
但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。
Redis 里面有 1 亿个 key,其中有 10 个 key 是包含 java,如何将它们全部找出来?
1)keys *java* 命令,该命令性能很好,但是在数据量特别大的时候会有性能问题
2)scan 0 MATCH *java* 命令,基于游标的迭代器,更好的选择
SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新
的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表
示迭代已结束。
SCAN 命令是一个基于游标的迭代器(cursor based iterator): SCAN 命令每次被调用之后, 都会向用户返回一个新
的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
当 SCAN 命令的游标参数被设置为 0 时, 服务器将开始一次新的迭代, 而当服务器向用户返回值为 0 的游标时, 表
示迭代已结束。
Redis 的字符串(SDS)和 C语言的字符串区别
C字符串
获取字符串长度的复杂度为 O(N)
API 是不安全的,可能会造成缓冲区溢出
修改字符串长度 N 次必然需要执行 N 次内存重分配
只能保存文本数据
可以使用所有的<string.h>库中的函数
SDS
获取字符串长度的复杂度为 O(1)
API 是安全的,不会造成缓冲区溢出
修改字符串长度 N 次最多需要执行 N 次内存重分配
可以保存文本数据或者二进制数据
可以使用一部分<string.h>库中的函数
Sorted Set 底层数据结构
Sorted Set(有序集合)当前有两种编码:ziplist、skiplist
ziplist:
使用压缩列表实现
当保存的元素长度都小于 64 字节,同时数量小于 128 时,使用该编码方式,否则会使用 skiplist。
这两个参数可以通过 zset-max-ziplist-entries、zset-max-ziplist-value 来自定义修改。
当保存的元素长度都小于 64 字节,同时数量小于 128 时,使用该编码方式,否则会使用 skiplist。
这两个参数可以通过 zset-max-ziplist-entries、zset-max-ziplist-value 来自定义修改。
子主题
skiplist:
zset 实现
一个 zset 同时包含一个字典(dict)和一个跳跃表(zskiplist)
一个 zset 同时包含一个字典(dict)和一个跳跃表(zskiplist)
子主题
Sorted Set 为什么同时使用字典和跳跃表?
主要是为了提升性能。
- 单独使用字典:
- 单独使用跳跃表:
Sorted Set 为什么使用跳跃表,而不是红黑树?
主要有以下几个原因:
1)跳表的性能和红黑树差不多。
2)跳表更容易实现和调试。
网上有同学说是因为作者不会红黑树,我觉得挺有可能的。
1)跳表的性能和红黑树差不多。
2)跳表更容易实现和调试。
网上有同学说是因为作者不会红黑树,我觉得挺有可能的。
Hash 对象底层结构
Hash 对象当前有两种编码:ziplist、hashtable
ziplist:
使用压缩列表实现
每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的
表尾,然后再将保存了值的节点推入到压缩列表表尾。
因此:
1)保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
2)先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。
每当有新的键值对要加入到哈希对象时,程序会先将保存了键的节点推入到压缩列表的
表尾,然后再将保存了值的节点推入到压缩列表表尾。
因此:
1)保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;
2)先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加的会被放在表尾方向。
子主题
hashtable:
使用字典作为底层实现,
哈希对象中的每个键值对都使用一个字典键值来保存,
跟 java 中的 HashMap类似。
哈希对象中的每个键值对都使用一个字典键值来保存,
跟 java 中的 HashMap类似。
子主题
Hash 对象的扩容流程
hash 对象在扩容时使用了一种叫“渐进式 rehash”的方式,步骤如下:
1)计算新表 size、掩码,为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2)将 rehash 索引计数器变量 rehashidx 的值设置为 0,表示 rehash 正式开始。
3)在 rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触
发额外的 rehash 操作,在源码中的 _dictRehashStep 方法。
_dictRehashStep:从名字也可以看出来,大意是 rehash 一步,也就是 rehash 一个索引位置。
该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有
节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx
属性的值加一。
4)将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis
要一直持有两个哈希表吗?
答案当然不是的。我们知道,redis 除了文件事件外,还有时间事件,redis 会定期触发时间事件,这些时间事件用
于执行一些后台操作,其中就包含 rehash 操作:当 redis 发现有字典正在进行 rehash 操作时,会花费 1 毫秒的
时间,一起帮忙进行 rehash。
5)随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完
成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。
1)计算新表 size、掩码,为新表 ht[1] 分配空间,让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2)将 rehash 索引计数器变量 rehashidx 的值设置为 0,表示 rehash 正式开始。
3)在 rehash 进行期间,每次对字典执行添加、删除、査找、更新操作时,程序除了执行指定的操作以外,还会触
发额外的 rehash 操作,在源码中的 _dictRehashStep 方法。
_dictRehashStep:从名字也可以看出来,大意是 rehash 一步,也就是 rehash 一个索引位置。
该方法会从 ht[0] 表的 rehashidx 索引位置上开始向后查找,找到第一个不为空的索引位置,将该索引位置的所有
节点 rehash 到 ht[1],当本次 rehash 工作完成之后,将 ht[0] 索引位置为 rehashidx 的节点清空,同时将 rehashidx
属性的值加一。
4)将 rehash 分摊到每个操作上确实是非常妙的方式,但是万一此时服务器比较空闲,一直没有什么操作,难道 redis
要一直持有两个哈希表吗?
答案当然不是的。我们知道,redis 除了文件事件外,还有时间事件,redis 会定期触发时间事件,这些时间事件用
于执行一些后台操作,其中就包含 rehash 操作:当 redis 发现有字典正在进行 rehash 操作时,会花费 1 毫秒的
时间,一起帮忙进行 rehash。
5)随着操作的不断执行,最终在某个时间点上,ht[0] 的所有键值对都会被 rehash 至 ht[1],此时 rehash 流程完
成,会执行最后的清理工作:释放 ht[0] 的空间、将 ht[0] 指向 ht[1]、重置 ht[1]、重置 rehashidx 的值为 -1。
渐进式 rehash 的优点
渐进式 rehash 的好处在于它采取分而治之的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、
删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量。
在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间,
字典的删除、査找、更新等操作会在两个哈希表上进行。例如,要在字典里面査找一个键的话,程序会先在 ht[0] 里
面进行査找,如果没找到的话,就会继续到 ht[1] 里面进行査找,诸如此类。
另外,在渐进式 rehash 执行期间,新增的键值对会被直接保存到 ht[1], ht[0] 不再进行任何添加操作,这样就保证
了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。
删除、查找和更新操作上,从而避免了集中式 rehash 而带来的庞大计算量。
在进行渐进式 rehash 的过程中,字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间,
字典的删除、査找、更新等操作会在两个哈希表上进行。例如,要在字典里面査找一个键的话,程序会先在 ht[0] 里
面进行査找,如果没找到的话,就会继续到 ht[1] 里面进行査找,诸如此类。
另外,在渐进式 rehash 执行期间,新增的键值对会被直接保存到 ht[1], ht[0] 不再进行任何添加操作,这样就保证
了 ht[0] 包含的键值对数量会只减不增,并随着 rehash 操作的执行而最终变成空表。
rehash 流程在数据量大的时候会有什么问题吗
(Hash 对象的扩容流程在数据量大的时候会有什么问题吗)
(Hash 对象的扩容流程在数据量大的时候会有什么问题吗)
1)扩容期开始时,会先给 ht[1] 申请空间,所以在整个扩容期间,会同时存在 ht[0] 和 ht[1],会占用额外的空间。
2)扩容期间同时存在 ht[0] 和 ht[1],查找、删除、更新等操作有概率需要操作两张表,耗时会增加。
3)redis 在内存使用接近 maxmemory 并且有设置驱逐策略的情况下,出现 rehash 会使得内存占用超过 maxmemory,触发驱逐淘汰操作,
导致 master/slave 均有有大量的 key 被驱逐淘汰,从而出现 master/slave 主从不一致。
2)扩容期间同时存在 ht[0] 和 ht[1],查找、删除、更新等操作有概率需要操作两张表,耗时会增加。
3)redis 在内存使用接近 maxmemory 并且有设置驱逐策略的情况下,出现 rehash 会使得内存占用超过 maxmemory,触发驱逐淘汰操作,
导致 master/slave 均有有大量的 key 被驱逐淘汰,从而出现 master/slave 主从不一致。
Redis的淘汰策略
Redis 删除过期键的策略
(缓存失效策略、数据过期策略)
(缓存失效策略、数据过期策略)
定时删除:
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
对内存最友好,对 CPU 时间最不友好。
惰性删除:
放任键过期不管,但是每次获取键时,都检査键是否过期,
如果过期的话,就删除该键;
如果没有过期,就返回该键。
对 CPU 时间最优化,对内存最不友好。
定期删除:
每隔一段时间,默认 100ms,程序就对数据库进行一次检査,删除里面的过期键。
至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。
前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
Redis 使用惰性删除和定期删除
在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。
对内存最友好,对 CPU 时间最不友好。
惰性删除:
放任键过期不管,但是每次获取键时,都检査键是否过期,
如果过期的话,就删除该键;
如果没有过期,就返回该键。
对 CPU 时间最优化,对内存最不友好。
定期删除:
每隔一段时间,默认 100ms,程序就对数据库进行一次检査,删除里面的过期键。
至 于要删除多少过期键,以及要检査多少个数据库,则由算法决定。
前两种策略的折中,对 CPU 时间和内存的友好程度较平衡。
Redis 使用惰性删除和定期删除
Redis 有哪几种数据淘汰策略?
noeviction:返回错误当内存限制达到并且客户端尝试执行会让更多内存被使用的命令(大部分的写入指令,但DEL 和几个例外)
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
allkeys-lru: 尝试回收最少使用的键(LRU),使得新添加的数据有空间存放。
volatile-lru: 尝试回收最少使用的键(LRU),但仅限于在过期集合的键,使得新添加的数据有空间存放。
allkeys-random: 回收随机的键使得新添加的数据有空间存放。
volatile-random: 回收随机的键使得新添加的数据有空间存放,但仅限于在过期集合的键。
volatile-ttl: 回收在过期集合的键,并且优先回收存活时间(TTL)较短的键,使得新添加的数据有空间存放。
Redis key 的过期时间和永久有效分别怎么设置?
EXPIRE 和PERSIST 命令。
Redis的数据淘汰策略有哪些
voltile-lru 从已经设置过期时间的数据集中挑选最近最少使用的数据淘汰
voltile-ttl 从已经设置过期时间的数据库集当中挑选将要过期的数据
voltile-random 从已经设置过期时间的数据集任意选择淘汰数据
allkeys-lru 从数据集中挑选最近最少使用的数据淘汰
allkeys-random 从数据集中任意选择淘汰的数据
no-eviction 禁止驱逐数据
voltile-lru 从已经设置过期时间的数据集中挑选最近最少使用的数据淘汰
voltile-ttl 从已经设置过期时间的数据库集当中挑选将要过期的数据
voltile-random 从已经设置过期时间的数据集任意选择淘汰数据
allkeys-lru 从数据集中挑选最近最少使用的数据淘汰
allkeys-random 从数据集中任意选择淘汰的数据
no-eviction 禁止驱逐数据
redis过期策略都有哪些?LRU算法知道吗?写一下java代码实现?
过期策略:
定时过期:一key一定时器
惰性过期:只有使用key时才判断key是否已过期,过期则清除。
定期过期:前两者折中。
LRU:new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, true);
//第三个参数置为true,代表linkedlist按访问顺序排序,可作为LRU缓存;
//设为false代表按插入顺序排序,可作为FIFO缓存
LRU算法实现:
1.通过双向链表来实现,新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。
LinkedHashMap:
HashMap和双向链表合二为一即是LinkedHashMap。
HashMap是无序的,LinkedHashMap通过维护一个额外的双向链表保证了迭代顺序。
该迭代顺序可以是插入顺序(默认),也可以是访问顺序。
过期策略:
定时过期:一key一定时器
惰性过期:只有使用key时才判断key是否已过期,过期则清除。
定期过期:前两者折中。
LRU:new LinkedHashMap<K, V>(capacity, DEFAULT_LOAD_FACTORY, true);
//第三个参数置为true,代表linkedlist按访问顺序排序,可作为LRU缓存;
//设为false代表按插入顺序排序,可作为FIFO缓存
LRU算法实现:
1.通过双向链表来实现,新数据插入到链表头部;
2.每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3.当链表满的时候,将链表尾部的数据丢弃。
LinkedHashMap:
HashMap和双向链表合二为一即是LinkedHashMap。
HashMap是无序的,LinkedHashMap通过维护一个额外的双向链表保证了迭代顺序。
该迭代顺序可以是插入顺序(默认),也可以是访问顺序。
MySQL 里有2000w 数据,Redis 中只存20w 的数据,
如何保证Redis 中的数据都是热点数据?
如何保证Redis 中的数据都是热点数据?
Redis 内存数据集大小上升到一定大小的时候,就会施行数据淘汰策略。
Redis 回收进程如何工作的?
1. 一个客户端运行了新的命令,添加了新的数据。
2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
3. 一个新的命令被执行,等等。
4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
2. Redis检查内存使用情况,如果大于maxmemory的限制, 则根据设定好的策略进行回收。
3. 一个新的命令被执行,等等。
4. 所以我们不断地穿越内存限制的边界,通过不断达到边界然后不断地回收回到边界以下。
如果一个命令的结果导致大量内存被使用(例如很大的集合的交集保存到一个新的键),不用多久内存限制就会被这个内存使用量超越。
Redis 回收使用的是什么算法?
LRU 算法
Redis 的内存淘汰(驱逐)策略
当 redis 的内存空间(maxmemory 参数配置)已经用满时,redis 将根据配置的驱逐策略(maxmemory-policy 参数配置),进行相应的动作。
网上很多资料都是写 6 种,但是其实当前 redis 的淘汰策略已有 8 种,多余两种是 Redis 4.0 新增的,基于 LFU(Least Frequently Used)算法实现的。
网上很多资料都是写 6 种,但是其实当前 redis 的淘汰策略已有 8 种,多余两种是 Redis 4.0 新增的,基于 LFU(Least Frequently Used)算法实现的。
- noeviction:默认策略,不淘汰任何 key,直接返回错误
- allkeys-lru:在所有的 key 中,使用 LRU 算法淘汰部分 key
- allkeys-lfu:在所有的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
- allkeys-random:在所有的 key 中,随机淘汰部分 key
- volatile-lru:在设置了过期时间的 key 中,使用 LRU 算法淘汰部分 key
- volatile-lfu:在设置了过期时间的 key 中,使用 LFU 算法淘汰部分 key,该算法于 Redis 4.0 新增
- volatile-random:在设置了过期时间的 key 中,随机淘汰部分 key
- volatile-ttl:在设置了过期时间的 key 中,挑选 TTL(time to live,剩余时间)短的 key 淘汰
Redis 的 LRU 算法怎么实现的?
Redis 在 redisObject 结构体中定义了一个长度 24 bit 的 unsigned 类型的字段(unsigned lru:LRU_BITS),在 LRU 算
法中用来存储对象最后一次被命令程序访问的时间。
具体的 LRU 算法经历了两个版本。
法中用来存储对象最后一次被命令程序访问的时间。
具体的 LRU 算法经历了两个版本。
版本 1:随机选取 N 个淘汰法
最初 Redis 是这样实现的:随机选 N(默认 5) 个 key,把空闲时间(idle time)最大的那个 key 移除。
这边的 N可通过 maxmemory-samples 配置项修改。
就是这么简单,简单得让人不敢相信了,而且十分有效。
但是这个算法有个明显的缺点:每次都是随机从 N 个里选择 1 个,并没有利用前一轮的历史信息。
其实在上一轮移除 key 的过程中,其实是知道了 N 个 key 的 idle time 的情况的,
那在下一轮移除 key 时,其实可以利用上一轮的这些信息。
这也是 Redis 3.0 的优化思想。
这边的 N可通过 maxmemory-samples 配置项修改。
就是这么简单,简单得让人不敢相信了,而且十分有效。
但是这个算法有个明显的缺点:每次都是随机从 N 个里选择 1 个,并没有利用前一轮的历史信息。
其实在上一轮移除 key 的过程中,其实是知道了 N 个 key 的 idle time 的情况的,
那在下一轮移除 key 时,其实可以利用上一轮的这些信息。
这也是 Redis 3.0 的优化思想。
版本 2:Redis 3.0 对 LRU 算法进行改进,引入了缓冲池(pool,默认 16)的概念
当每一轮移除 key 时,拿到了 N(默认 5)个 key 的 idle time,遍历处理这 N 个 key,
如果 key 的 idle time 比pool 里面的 key 的 idle time 还要大,就把它添加到 pool 里面去。
当 pool 放满之后,每次如果有新的 key 需要放入,需要将 pool 中 idle time 最小的一个 key 移除。
这样相当于 pool 里面始终维护着还未被淘汰的 idle time 最大的 16 个 key。
当我们每轮要淘汰的时候,直接从 pool 里面取出 idle time 最大的 key(只取 1 个),将之淘汰掉。
整个流程相当于随机取 5 个 key 放入 pool,然后淘汰 pool 中空闲时间最大的 key,
然后再随机取 5 个 key 放入 pool,继续淘汰 pool 中空闲时间最大的 key,一直持续下去。
在进入淘汰前会计算出需要释放的内存大小,然后就一直循环上述流程,直至释放足够的内存。
如果 key 的 idle time 比pool 里面的 key 的 idle time 还要大,就把它添加到 pool 里面去。
当 pool 放满之后,每次如果有新的 key 需要放入,需要将 pool 中 idle time 最小的一个 key 移除。
这样相当于 pool 里面始终维护着还未被淘汰的 idle time 最大的 16 个 key。
当我们每轮要淘汰的时候,直接从 pool 里面取出 idle time 最大的 key(只取 1 个),将之淘汰掉。
整个流程相当于随机取 5 个 key 放入 pool,然后淘汰 pool 中空闲时间最大的 key,
然后再随机取 5 个 key 放入 pool,继续淘汰 pool 中空闲时间最大的 key,一直持续下去。
在进入淘汰前会计算出需要释放的内存大小,然后就一直循环上述流程,直至释放足够的内存。
Redis分区
Redis 是单线程的,如何提高多核CPU 的利用率?
可以在同一个服务器部署多个Redis 的实例,并把他们当作不同的服务器来使用,
在某些时候,无论如何一个服务器是不够的,
所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。
在某些时候,无论如何一个服务器是不够的,
所以,如果你想使用多个CPU,你可以考虑一下分片(shard)。
为什么要做Redis 分区?
分区可以让Redis 管理更大的内存,Redis 将可以使用所有机器的内存。
如果没有分区,你最多只能使用一台机器的内存。
分区使Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。
如果没有分区,你最多只能使用一台机器的内存。
分区使Redis 的计算能力通过简单地增加计算机得到成倍提升,Redis 的网络带宽也会随着计算机和网卡的增加而成倍增长。
你知道有哪些Redis 分区实现方案?
Redis 和memcached 的一种代理实现就是Twemproxy
是直接将请求从一个Redis 节点转发到另一个Redis 节点,而是在客户端的帮助下直接
redirected 到正确的Redis 节点。
- 客户端分区就是在客户端就已经决定数据会被存储到哪个Redis 节点或者从哪个Redis 节点读取。
- 代理分区意味着客户端将请求发送给代理,然后代理决定去哪个节点写数据或者读数据。
Redis 和memcached 的一种代理实现就是Twemproxy
- 查询路由(Query routing) 的意思是客户端随机地请求任意一个Redis 实例,然后由Redis
是直接将请求从一个Redis 节点转发到另一个Redis 节点,而是在客户端的帮助下直接
redirected 到正确的Redis 节点。
Twemproxy 是什么?
Twemproxy 是Twitter 维护的(缓存)代理系统,代理Memcached 的ASCII 协议和Redis协议。
它是单线程程序,使用c 语言编写,运行起来非常快。
它是采用Apache 2.0 license的开源软件。
Twemproxy 支持自动分区,如果其代理的其中一个Redis 节点不可用时,会自动将该节点排除
(这将改变原来的keys-instances 的映射关系,所以你应该仅在把Redis 当缓存时使用Twemproxy)。
Twemproxy 本身不存在单点问题,因为你可以启动多个Twemproxy 实例,然后让你的客户端去连接任意一个Twemproxy 实例。
Twemproxy 是Redis 客户端和服务器端的一个中间层,由它来处理分区功能应该不算复杂,并且应该算比较可靠的。
它是单线程程序,使用c 语言编写,运行起来非常快。
它是采用Apache 2.0 license的开源软件。
Twemproxy 支持自动分区,如果其代理的其中一个Redis 节点不可用时,会自动将该节点排除
(这将改变原来的keys-instances 的映射关系,所以你应该仅在把Redis 当缓存时使用Twemproxy)。
Twemproxy 本身不存在单点问题,因为你可以启动多个Twemproxy 实例,然后让你的客户端去连接任意一个Twemproxy 实例。
Twemproxy 是Redis 客户端和服务器端的一个中间层,由它来处理分区功能应该不算复杂,并且应该算比较可靠的。
Redis 分区有什么缺点?
- 涉及多个key 的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存
- 同时操作多个key,则不能使用Redis 事务.
- 分区使用的粒度是key,不能使用一个非常长的排序key 存储一个数据集
(The partitioning granularity is the key, so it is not possible to shard a dataset with a single huge key like a very big sorted set). - 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis 实例和主机同时收集RDB / AOF 文件。
- 分区时动态扩容或缩容可能非常复杂。Redis 集群在运行时增加或者删除Redis 节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
Redis使用场景
Redis 有哪些适合的场景?
(1)、会话缓存(Session Cache)
最常用的一种使用Redis 的情景是会话缓存(session cache)。
用Redis 缓存会话比其他存储(如Memcached)的优势在于:Redis 提供持久化。
当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着Redis 这些年的改进,很容易找到怎么恰当的使用Redis 来缓存会话的文档。
甚至广为人知的商业平台Magento 也提供Redis 的插件。
用Redis 缓存会话比其他存储(如Memcached)的优势在于:Redis 提供持久化。
当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着Redis 这些年的改进,很容易找到怎么恰当的使用Redis 来缓存会话的文档。
甚至广为人知的商业平台Magento 也提供Redis 的插件。
(2)、全页缓存(FPC)
除基本的会话token 之外,Redis 还提供很简便的FPC 平台。
回到一致性问题,即使重启了Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP 本地FPC。
再次以Magento 为例,Magento 提供一个插件来使用Redis 作为全页缓存后端。
此外,对WordPress 的用户来说,Pantheon 有一个非常好的插件wp-Redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
回到一致性问题,即使重启了Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似PHP 本地FPC。
再次以Magento 为例,Magento 提供一个插件来使用Redis 作为全页缓存后端。
此外,对WordPress 的用户来说,Pantheon 有一个非常好的插件wp-Redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。
(3)、队列
Reids 在内存存储引擎领域的一大优点是提供list 和set 操作,这使得Redis 能作为一个很好的消息队列平台来使用。
Redis 作为队列使用的操作,就类似于本地程序语言(如Python)对list 的push/pop 操作。
Google 搜索“Redis queues”,能找到大量开源项目,项目的目的就是利用Redis 创建非常好的后端工具,以满足各种队列需求。
例如,Celery有一个后台就是使用Redis 作为broker,你可以从这里去查看。
Redis 作为队列使用的操作,就类似于本地程序语言(如Python)对list 的push/pop 操作。
Google 搜索“Redis queues”,能找到大量开源项目,项目的目的就是利用Redis 创建非常好的后端工具,以满足各种队列需求。
例如,Celery有一个后台就是使用Redis 作为broker,你可以从这里去查看。
(4)、排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。
集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。
所以, 我们要从排序集合中获取到排名最靠前的10 个用户– 我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES
Agora Games 就是一个很好的例子,用Ruby 实现的,它的排行榜就是使用Redis 来存储数据的,你可以在这里看到。
集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。
所以, 我们要从排序集合中获取到排名最靠前的10 个用户– 我们称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:ZRANGE user_scores 0 10 WITHSCORES
Agora Games 就是一个很好的例子,用Ruby 实现的,它的排行榜就是使用Redis 来存储数据的,你可以在这里看到。
(5)、发布/订阅
最后(但肯定不是最不重要的)是Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。
我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis 的发布/订阅功能来建立聊天系统!
(不,这是真的,你可以去核实)
我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用Redis 的发布/订阅功能来建立聊天系统!
(不,这是真的,你可以去核实)
使用redis如何设计分布式锁?说一下实现思路?使用zk可以吗?如何实现?这两种有什么区别?
redis:
1.线程A setnx(上锁的对象,超时时的时间戳t1),如果返回true,获得锁。
2.线程B 用get获取t1,与当前时间戳比较,判断是是否超时,没超时false,若超时执行第3步;
3.计算新的超时时间t2,使用getset命令返回t3(该值可能其他线程已经修改过),如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了。
4.获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。
2.线程B 用get获取t1,与当前时间戳比较,判断是是否超时,没超时false,若超时执行第3步;
3.计算新的超时时间t2,使用getset命令返回t3(该值可能其他线程已经修改过),如果t1==t3,获得锁,如果t1!=t3说明锁被其他线程获取了。
4.获取锁后,处理完业务逻辑,再去判断锁是否超时,如果没超时删除锁,如果已超时,不用处理(防止删除其他线程的锁)。
zk:
1.客户端对某个方法加锁时,在zk上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点node1;
2.客户端获取该路径下所有已经创建的子节点,如果发现自己创建的node1的序号是最小的,就认为这个客户端获得了锁。
3.如果发现node1不是最小的,则监听比自己创建节点序号小的最大的节点,进入等待。
4.获取锁后,处理完逻辑,删除自己创建的node1即可。
2.客户端获取该路径下所有已经创建的子节点,如果发现自己创建的node1的序号是最小的,就认为这个客户端获得了锁。
3.如果发现node1不是最小的,则监听比自己创建节点序号小的最大的节点,进入等待。
4.获取锁后,处理完逻辑,删除自己创建的node1即可。
区别:zk性能差一些,开销大,实现简单。
Redis实现分布式锁
Redis为单进程单线程模式,采用队列模式将并发访问变成串行访问,且多客户
端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。
若给定的 key 已经存在,则SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。
当且仅当 key 不存在,将 key 的值设为 value。
若给定的 key 已经存在,则SETNX 不做任何动作
SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
返回值:设置成功,返回 1 。设置失败,返回 0 。
子主题
使用SETNX完成同步锁的流程及事项如下:
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回
0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除
使用SETNX命令获取锁,若返回0(key已存在,锁已存在)则获取失败,反之获取成功
为了防止获取锁后程序出现异常,导致其他线程/进程调用SETNX命令总是返回
0而进入死锁状态,需要为该key设置一个“合理”的过期时间释放锁,使用DEL命令将锁数据删除
Redis 实现分布式锁
1)加锁
加锁通常使用 set 命令来实现,伪代码如下:
1. set key value PXmilliseconds NX
几个参数的意义如下:
SET key value NX 效果等同于 SETNXkey value。
PX、expireTime 参数则是用于解决没有解锁导致的死锁问题。
因为如果没有过期时间,万一程序员写的代码有 bug导致没有解锁操作,则就出现了死锁,因此该参数起到了一个“兜底”的作用。
NX参数用于保证在多个线程并发 set 下,只会有 1 个线程成功,起到了锁的“唯一”性。
1. set key value PXmilliseconds NX
几个参数的意义如下:
- key、value:键值对
- PXmilliseconds:设置键的过期时间为 milliseconds 毫秒。
- NX:只在键不存在时,才对键进行设置操作。
SET key value NX 效果等同于 SETNXkey value。
PX、expireTime 参数则是用于解决没有解锁导致的死锁问题。
因为如果没有过期时间,万一程序员写的代码有 bug导致没有解锁操作,则就出现了死锁,因此该参数起到了一个“兜底”的作用。
NX参数用于保证在多个线程并发 set 下,只会有 1 个线程成功,起到了锁的“唯一”性。
2)解锁
解锁需要两步操作:
1)查询当前“锁”是否还是我们持有,因为存在过期时间,所以可能等你想解锁的时候,“锁”已经到期,然后被其他
线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”
2)如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。
由于当前 Redis 还没有原子命令直接支持这两步操作,所以当前通常是使用 Lua 脚本来执行解锁操作,Redis 会保
证脚本里的内容执行是一个原子操作。
线程获取了,所以我们在解锁前需要先判断自己是否还持有“锁”
2)如果“锁”还是我们持有,则执行解锁操作,也就是删除该键值对,并返回成功;否则,直接返回失败。
由于当前 Redis 还没有原子命令直接支持这两步操作,所以当前通常是使用 Lua 脚本来执行解锁操作,Redis 会保
证脚本里的内容执行是一个原子操作。
脚本代码如下,逻辑比较简单:
解锁脚本代码
两个参数的意义如下:
KEYS[1]:我们要解锁的 key
ARGV[1]:我们加锁时的 value,用于判断当“锁”是否还是我们持有,如果被其他线程持有了,value 就会发生变化。
上述方法是 Redis 当前实现分布式锁的主流方法,可能会有一些小优区别,但是核心都是这个思路。
看着好像没啥毛病,但是真的是这个样子吗?让我们继续往下看。
ARGV[1]:我们加锁时的 value,用于判断当“锁”是否还是我们持有,如果被其他线程持有了,value 就会发生变化。
上述方法是 Redis 当前实现分布式锁的主流方法,可能会有一些小优区别,但是核心都是这个思路。
看着好像没啥毛病,但是真的是这个样子吗?让我们继续往下看。
Redis 分布式锁过期了,还没处理完怎么办
为了防止死锁,我们会给分布式锁加一个过期时间,但是万一这个时间到了,我们业务逻辑还没处理完,怎么办?
首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。
之后,我们再来考虑对这个问题进行兜底设计。
关于这个问题,目前常见的解决方法有两种:
1. 守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每 1/3 的锁时间检查 1 次),如果线程还持有锁,则刷新过期时间。
2. 超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。
同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等。
首先,我们在设置过期时间时要结合业务场景去考虑,尽量设置一个比较合理的值,就是理论上正常处理的话,在这个过期时间内是一定能处理完毕的。
之后,我们再来考虑对这个问题进行兜底设计。
关于这个问题,目前常见的解决方法有两种:
1. 守护线程“续命”:额外起一个线程,定期检查线程是否还持有锁,如果有则延长过期时间。
Redisson 里面就实现了这个方案,使用“看门狗”定期检查(每 1/3 的锁时间检查 1 次),如果线程还持有锁,则刷新过期时间。
2. 超时回滚:当我们解锁时发现锁已经被其他线程获取了,说明此时我们执行的操作已经是“不安全”的了,此时需要进行回滚,并返回失败。
同时,需要进行告警,人为介入验证数据的正确性,然后找出超时原因,是否需要对超时时间进行优化等等。
守护线程续命的方案有什么问题吗
Redisson 使用看门狗(守护线程)“续命”的方案在大多数场景下是挺不错的,也被广泛应用于生产环境,但是在极端情况下还是会存在问题。
问题例子如下:
1. 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点
2. 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障
3. redis 触发故障转移,其中一个 slave 升级为新的 master
4. 此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁
5. 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据
解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。
当前比较主流的解法和思路有两种:
1)Redis 作者提出的 RedLock;
2)Zookeeper 实现的分布式锁。
问题例子如下:
1. 线程 1 首先获取锁成功,将键值对写入 redis 的 master 节点
2. 在 redis 将该键值对同步到 slave 节点之前,master 发生了故障
3. redis 触发故障转移,其中一个 slave 升级为新的 master
4. 此时新的 master 并不包含线程 1 写入的键值对,因此线程 2 尝试获取锁也可以成功拿到锁
5. 此时相当于有两个线程获取到了锁,可能会导致各种预期之外的情况发生,例如最常见的脏数据
解决方法:上述问题的根本原因主要是由于 redis 异步复制带来的数据不一致问题导致的,因此解决的方向就是保证数据的一致。
当前比较主流的解法和思路有两种:
1)Redis 作者提出的 RedLock;
2)Zookeeper 实现的分布式锁。
RedLock
首先,该方案也是基于文章开头的那个方案(set 加锁、lua 脚本解锁)进行改良的,所以 antirez 只描述了差异的
地方,大致方案如下。
假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁,客户端应该执行以下操作:
1. 获取当前时间,以毫秒为单位。
2. 依次尝试从 5 个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向 Redis 请求获取锁时,客户端
应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时
时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞
状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁。
3. 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是
3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。
4. 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5. 如果由于某些原因未能获得锁(无法在至少 N/2+1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),
客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功,防止某些节点获取
到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
地方,大致方案如下。
假设我们有 N 个 Redis 主节点,例如 N = 5,这些节点是完全独立的,我们不使用复制或任何其他隐式协调系统,
为了取到锁,客户端应该执行以下操作:
1. 获取当前时间,以毫秒为单位。
2. 依次尝试从 5 个实例,使用相同的 key 和随机值(例如 UUID)获取锁。当向 Redis 请求获取锁时,客户端
应该设置一个超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为 10 秒,则超时
时间应该在 5-50 毫秒之间。这样可以防止客户端在试图与一个宕机的 Redis 节点对话时长时间处于阻塞
状态。如果一个实例不可用,客户端应该尽快尝试去另外一个 Redis 实例请求获取锁。
3. 客户端通过当前时间减去步骤 1 记录的时间来计算获取锁使用的时间。当且仅当从大多数(N/2+1,这里是
3 个节点)的 Redis 节点都取到锁,并且获取锁使用的时间小于锁失效时间时,锁才算获取成功。
4. 如果取到了锁,其真正有效时间等于初始有效时间减去获取锁所使用的时间(步骤 3 计算的结果)。
5. 如果由于某些原因未能获得锁(无法在至少 N/2+1 个 Redis 实例获取锁、或获取锁的时间超过了有效时间),
客户端应该在所有的 Redis 实例上进行解锁(即便某些 Redis 实例根本就没有加锁成功,防止某些节点获取
到锁但是客户端没有得到响应而导致接下来的一段时间不能被重新获取锁)。
可以看出,该方案为了解决数据不一致的问题,直接舍弃了异步复制,只使用 master 节点,同时由于舍弃了 slave,为了保证可用性,引入了 N 个节点,官方建议是 5。
该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:
1)该方案的成本似乎有点高,需要使用 5 个实例;
2)该方案一样存在问题。
该方案主要存以下问题:
1. 严重依赖系统时钟。如果线程 1 从 3 个实例获取到了锁,但是这 3 个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有 3 个实例是空闲的,则线程 2 也可以获取到锁,则可能出现两个线程同时持有锁了。
2. 如果线程 1 从 3 个实例获取到了锁,但是万一其中有 1 台重启了,则此时又有 3 个实例是空闲的,则线程 2也可以获取到锁,此时又出现两个线程同时持有锁了。针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多。
该方案看着挺美好的,但是实际上我所了解到的在实际生产上应用的不多,主要有两个原因:
1)该方案的成本似乎有点高,需要使用 5 个实例;
2)该方案一样存在问题。
该方案主要存以下问题:
1. 严重依赖系统时钟。如果线程 1 从 3 个实例获取到了锁,但是这 3 个实例中的某个实例的系统时间走的稍微快一点,则它持有的锁会提前过期被释放,当他释放后,此时又有 3 个实例是空闲的,则线程 2 也可以获取到锁,则可能出现两个线程同时持有锁了。
2. 如果线程 1 从 3 个实例获取到了锁,但是万一其中有 1 台重启了,则此时又有 3 个实例是空闲的,则线程 2也可以获取到锁,此时又出现两个线程同时持有锁了。针对以上问题其实后续也有人给出一些相应的解法,但是整体上来看还是不够完美,所以目前实际应用得不是那么多。
什么是 RedLock
基于 Redis 实现分布式锁的方式
基于 Redis 实现分布式锁的方式
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫Redlock,此种方式比原先的单节点的方法更安全。
它可以保证以下特性:
1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务
它可以保证以下特性:
1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务
如何解决 Redis 的并发竞争 Key 问题
所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,
但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。
(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想为:
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:https://www.jianshu.com/p/8bddd381de06
但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。
(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
基于zookeeper临时有序节点可以实现的分布式锁。
大致思想为:
每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。
判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。
当释放锁的时候,只需将这个瞬时节点删除即可。
同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。
完成业务流程后,删除对应的子节点释放锁。
在实践中,当然是从以可靠性为主。所以首推Zookeeper。
参考:https://www.jianshu.com/p/8bddd381de06
使用Redis做过异步队列吗,是如何实现的
使用list类型保存数据信息,rpush生产消息,lpop消费消息,
当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,
如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
当lpop没有消息时,可以sleep一段时间,然后再检查有没有信息,
如果不想sleep的话,可以使用blpop, 在没有信息的时候,会一直阻塞,直到信息的到来。
redis可以通过pub/sub主题订阅模式实现一个生产者,多个消费者,当然也存在一定的缺点,当消费者下线时,生产的消息会丢失。
使用过 Redis 做消息队列么?
Redis 本身提供了一些组件来实现消息队列的功能,但是多多少少都存在一些缺点,相比于市面上成熟的消息队列,
例如 Kafka、Rocket MQ 来说并没有优势,因此目前我们并没有使用 Redis 来做消息队列。
关于 Redis 做消息队列的常见方案主要有以下:
1)Redis 5.0 之前可以使用 List(blocking)、Pub/Sub 等来实现轻量级的消息发布订阅功能组件,
但是这两种实现方式都有很明显的缺点,两者中相对完善的 Pub/Sub 的主要缺点就是消息无法持久化,
如果出现网络断开、Redis 宕机等,消息就会被丢弃。
2)为了解决 Pub/Sub 模式等的缺点,Redis 在 5.0 引入了全新的 Stream,Stream 借鉴了很多 Kafka 的设计思想,有以下几个特点:
但是相比于 Kafka,其实没有优势,同时存在:尚未经过大量验证、成本较高、不支持分区(partition)、无法支持大规模数据等问题。
例如 Kafka、Rocket MQ 来说并没有优势,因此目前我们并没有使用 Redis 来做消息队列。
关于 Redis 做消息队列的常见方案主要有以下:
1)Redis 5.0 之前可以使用 List(blocking)、Pub/Sub 等来实现轻量级的消息发布订阅功能组件,
但是这两种实现方式都有很明显的缺点,两者中相对完善的 Pub/Sub 的主要缺点就是消息无法持久化,
如果出现网络断开、Redis 宕机等,消息就会被丢弃。
2)为了解决 Pub/Sub 模式等的缺点,Redis 在 5.0 引入了全新的 Stream,Stream 借鉴了很多 Kafka 的设计思想,有以下几个特点:
- 提供了消息的持久化和主备复制功能,可以让任何客户端访问任何时刻的数据,并且能记住每一个客户端的访问位置,还能保证消息不丢失。
- 引入了消费者组的概念,不同组接收到的数据完全一样(前提是条件一样),但是组内的消费者则是竞争关系。
但是相比于 Kafka,其实没有优势,同时存在:尚未经过大量验证、成本较高、不支持分区(partition)、无法支持大规模数据等问题。
Redis如何实现延时队列
使用sortedset,使用时间戳做score, 消息内容作为key,调用zadd来生产消息,消费者使用zrangbyscore获取n秒之前的数据做轮询处理。
Redis 在项目中的使用场景
- 缓存(核心)、
- 分布式锁(set + lua 脚本)、
- 排行榜(zset)、
- 计数(incrby)、
- 消息队列(stream)、
- 地理位置(geo)、
- 访客统计(hyperloglog)
Redis事务
怎么理解Redis 事务?
事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的
过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
过程中,不会被其他客户端发送来的命令请求所打断。
事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。
Redis 事务相关的命令有哪几个?
MULTI、EXEC、DISCARD、WATCH
Redis 事务的实现
一个事务从开始到结束通常会经历以下 3 个阶段:
1)事务开始:multi 命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过 flags 属性标识。
2)命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作:
不过 redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,redis 会保证一个 Lua 脚本里的所有命令的原子性。
1)事务开始:multi 命令将执行该命令的客户端从非事务状态切换至事务状态,底层通过 flags 属性标识。
2)命令入队:当客户端处于事务状态时,服务器会根据客户端发来的命令执行不同的操作:
- exec、discard、watch、multi 命令会被立即执行
- 其他命令不会立即执行,而是将命令放入到一个事务队列,然后向客户端返回 QUEUED 回复。
不过 redis 的事务并不推荐在实际中使用,如果要使用事务,推荐使用 Lua 脚本,redis 会保证一个 Lua 脚本里的所有命令的原子性。
Redis集群
Redis 集群方案应该怎么做?都有哪些方案?
1.twemproxy
大概概念是,它类似于一个代理方式,使用方法和普通Redis 无任何区别,
设置好它下属的多个Redis 实例后, 使用时在本需要连接Redis 的地方改为连接twemproxy,
它会以一个代理的身份接收请求并使用一致性hash 算法,将请求转接到具体Redis,将结果再返回twemproxy。
使用方式简便(相对Redis 只需修改连接端口),对旧项目扩展的首选。
问题:twemproxy 自身单端口实例的压力,使用一致性hash 后,对Redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
设置好它下属的多个Redis 实例后, 使用时在本需要连接Redis 的地方改为连接twemproxy,
它会以一个代理的身份接收请求并使用一致性hash 算法,将请求转接到具体Redis,将结果再返回twemproxy。
使用方式简便(相对Redis 只需修改连接端口),对旧项目扩展的首选。
问题:twemproxy 自身单端口实例的压力,使用一致性hash 后,对Redis 节点数量改变时候的计算值的改变,数据无法自动移动到新的节点。
2. codis
目前用的最多的集群方案
基本和twemproxy 一致的效果,但它支持在节点数量改变情况下,旧节点数据可恢复到新hash 节点。
3. Redis cluster3.0 自带的集群
特点在于他的分布式算法不是一致性hash,而是hash槽的概念,以及自身支持节点设置从节点。
具体看官方文档介绍。
4.在业务代码层实现,起几个毫无关联的Redis 实例,在代码层,对key 进行hash 计算,然后去对应的Redis 实例操作数据。
这种方式对hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。
这种方式对hash 层代码要求比较高,考虑部分包括,节点失效后的替代算法方案,数据震荡后的自动脚本恢复,实例的监控,等等。
Redis 集群方案什么情况下会导致整个集群不可用?
有A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点B 失败了,那么整个集群就
会以为缺少5501-11000 这个范围的槽而不可用。
有A,B,C 三个节点的集群,在没有复制模型的情况下,如果节点B 失败了,那么整个集群就
会以为缺少5501-11000 这个范围的槽而不可用。
redis主从复制如何实现的?redis的集群模式如何实现?redis的key是如何寻址的?
主从复制实现:主节点将自己内存中的数据做一份快照,将快照发给从节点,从节点将数据恢复到内存中。之后再每次增加新数据的时候,主节点以类似于mysql的二进制日志方式将语句发送给从节点,从节点拿到主节点发送过来的语句进行重放。
分片方式:
-客户端分片
-基于代理的分片
● Twemproxy
● codis
-路由查询分片
● Redis-cluster(本身提供了自动将数据分散到Redis Cluster不同节点的能力,整个数据集合的某个数据子集存储在哪个节点对于用户来说是透明的)
redis-cluster分片原理:Cluster中有一个16384长度的槽(虚拟槽),编号分别为0-16383。每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,可以由用户指定,也可以在初始化的时候自动生成,只有Master才拥有槽的所有权。Master节点维护着一个16384/8字节的位序列,Master节点用bit来标识对于某个槽自己是否拥有。比如对于编号为1的槽,Master只要判断序列的第二位(索引从0开始)是不是为1即可。这种结构很容易添加或者删除节点。比如如果我想新添加个节点D, 我需要从节点A、B、 C中得部分槽到D上。
主从复制实现:主节点将自己内存中的数据做一份快照,将快照发给从节点,从节点将数据恢复到内存中。之后再每次增加新数据的时候,主节点以类似于mysql的二进制日志方式将语句发送给从节点,从节点拿到主节点发送过来的语句进行重放。
分片方式:
-客户端分片
-基于代理的分片
● Twemproxy
● codis
-路由查询分片
● Redis-cluster(本身提供了自动将数据分散到Redis Cluster不同节点的能力,整个数据集合的某个数据子集存储在哪个节点对于用户来说是透明的)
redis-cluster分片原理:Cluster中有一个16384长度的槽(虚拟槽),编号分别为0-16383。每个Master节点都会负责一部分的槽,当有某个key被映射到某个Master负责的槽,那么这个Master负责为这个key提供服务,至于哪个Master节点负责哪个槽,可以由用户指定,也可以在初始化的时候自动生成,只有Master才拥有槽的所有权。Master节点维护着一个16384/8字节的位序列,Master节点用bit来标识对于某个槽自己是否拥有。比如对于编号为1的槽,Master只要判断序列的第二位(索引从0开始)是不是为1即可。这种结构很容易添加或者删除节点。比如如果我想新添加个节点D, 我需要从节点A、B、 C中得部分槽到D上。
Redis 集群的主从复制模型是怎样的?
为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主
从复制模型,每个节点都会有N-1 个复制品.
从复制模型,每个节点都会有N-1 个复制品.
redis 主从复制的核心原理
当启动一个 slave node 的时候,它会发送一个 PSYNC 命令给 master node。
如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。
slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
如果这是 slave node 初次连接到 master node,那么会触发一次 full resynchronization 全量复制。此时 master 会启动一个后台线程,开始生成一份RDB 快照文件,同时还会将从客户端 client 新收到的所有写命令缓存在内存中。RDB 文件生成完毕后, master 会将这个 RDB 发送给 slave,slave 会先写入本地磁盘,然后再从本地磁盘加载到内存中,接着 master 会将内存中缓存的写命令发送到 slave,slave 也会同步这些数据。
slave node 如果跟 master node 有网络故障,断开了连接,会自动重连,连接之后 master node 仅会复制给 slave 部分缺少的数据。
redis master slave replication
过程原理
1. 当从库和主库建立MS关系后,会向主数据库发送SYNC命令
2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
2. 主库接收到SYNC命令后会开始在后台保存快照(RDB持久化过程),并将期间接收到的写命令缓存起来
3. 当快照完成后,主Redis会将快照文件和所有缓存的写命令发送给从Redis
4. 从Redis接收到后,会载入快照文件并且执行收到的缓存的命令
5. 之后,主Redis每当接收到写命令时就会将命令发送从Redis,从而保证数据的一致
缺点
所有的slave节点数据的复制和同步都由master节点来处理,会造成master节点压力太大,使用主从从结构来解决
Redis 集群之间是如何复制的?
异步复制
说说Redis 哈希槽的概念?
Redis 集群没有使用一致性hash,而是引入了哈希槽的概念,
Redis 集群有16384 个哈希槽,
每个key 通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。
Redis 集群有16384 个哈希槽,
每个key 通过CRC16 校验后对16384 取模来决定放置哪个槽,集群的每个节点负责一部分hash 槽。
Redis 集群会有写操作丢失吗?为什么?
Redis 并不能保证数据的强一致性,这意味这在实际中集群在特定的条件下可能会丢失写操作。
Redis 集群最大节点个数是多少?
16384 个。
Redis 集群如何选择数据库?
Redis 集群目前无法做数据库选择,默认在0 数据库。
分布式Redis 是前期做还是后期规模上来了再做好?为什么?
既然Redis 是如此的轻量(单实例只使用1M 内存),为防止以后的扩容,最好的办法就是一开始就启动较多实例。
即便你只有一台服务器,你也可以一开始就让Redis 以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
一开始就多设置几个Redis 实例,例如32 或者64 个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
这样当数据不断增长,需要更多Redis 服务器时,需要做的就仅是将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。
一旦你添加了另一台服务器,你需要将你一半的Redis 实例从第一台机器迁移到第二台机器。
即便你只有一台服务器,你也可以一开始就让Redis 以分布式的方式运行,使用分区,在同一台服务器上启动多个实例。
一开始就多设置几个Redis 实例,例如32 或者64 个实例,对大多数用户来说这操作起来可能比较麻烦,但是从长久来看做这点牺牲是值得的。
这样当数据不断增长,需要更多Redis 服务器时,需要做的就仅是将Redis实例从一台服务迁移到另外一台服务器而已(而不用考虑重新分区的问题)。
一旦你添加了另一台服务器,你需要将你一半的Redis 实例从第一台机器迁移到第二台机器。
生产环境中的 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左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,但是分配给 redis 进程的是10g内存,
一般线上生产环境,redis 的内存尽量不要超过 10g,超过 10g 可能会有问题。5 台机器对外提供读写,一共有 50g 内存。
因为每个主实例都挂了一个从实例,所以是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。
你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是10kb。100 条数据是 1mb,10 万条数据是 1g。
常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500左右的请求量。
其实大型的公司,会有基础架构的 team 负责缓存集群的运维。
Redis 集群方案什么情况下会导致整个集群不可用?
有 A,B,C三个节点的集群 ,在没有复制模型的情况下 ,如果节点 B失败了,那么整个集群就
会以为缺少 55015501-110011000这个范围的槽而不可用。
会以为缺少 55015501-110011000这个范围的槽而不可用。
Redis 怎么保证高可用、有哪些集群模式
主从复制、哨兵模式、集群模式。
主从复制
在当前最新的 Redis 6.0 中,主从复制的完整过程如下:
1)开启主从复制
通常有以下三种方式:
为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。
2)建立套接字(socket)连接
slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套
接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。
3)发送 PING 命令
slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。
4)身份验证
slave 向 master 发送 AUTH password 命令来进行身份验证。
5)发送端口信息
在身份验证通过后后, slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端
状态的 slave_listening_port 属性中。
6)发送 IP 地址
如果配置了 slave_announce_ip,则 slave 向 master 发送 slave_announce_ip 配置的 IP 地址, master 收到后记
录在 slave 所对应的客户端状态的 slave_ip 属性。
该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网 IP。
7)发送 CAPA
CAPA 全称是 capabilities,这边表示的是同步复制的能力。slave 会在这一阶段发送 capa 告诉 master 自己具备的
(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。
8)数据同步
slave 将向 master 发送 PSYNC 命令, master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策
略进行数据的同步。
9)命令传播
当完成了同步之后,就会进入命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只
要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 一直保持一致了。
以部分重同步为例,主从复制的核心步骤流程图如下:
通常有以下三种方式:
- 在 slave 直接执行命令:slaveof <masterip> <masterport>
- 在 slave 配置文件中加入:slaveof <masterip> <masterport>
- 使用启动命令:--slaveof <masterip> <masterport>
为了兼容旧版本,通过配置的方式仍然支持 slaveof,但是通过命令的方式则不行了。
2)建立套接字(socket)连接
slave 将根据指定的 IP 地址和端口,向 master 发起套接字(socket)连接,master 在接受(accept) slave 的套
接字连接之后,为该套接字创建相应的客户端状态,此时连接建立完成。
3)发送 PING 命令
slave 向 master 发送一个 PING 命令,以检査套接字的读写状态是否正常、 master 能否正常处理命令请求。
4)身份验证
slave 向 master 发送 AUTH password 命令来进行身份验证。
5)发送端口信息
在身份验证通过后后, slave 将向 master 发送自己的监听端口号, master 收到后记录在 slave 所对应的客户端
状态的 slave_listening_port 属性中。
6)发送 IP 地址
如果配置了 slave_announce_ip,则 slave 向 master 发送 slave_announce_ip 配置的 IP 地址, master 收到后记
录在 slave 所对应的客户端状态的 slave_ip 属性。
该配置是用于解决服务器返回内网 IP 时,其他服务器无法访问的情况。可以通过该配置直接指定公网 IP。
7)发送 CAPA
CAPA 全称是 capabilities,这边表示的是同步复制的能力。slave 会在这一阶段发送 capa 告诉 master 自己具备的
(同步)复制能力, master 收到后记录在 slave 所对应的客户端状态的 slave_capa 属性。
8)数据同步
slave 将向 master 发送 PSYNC 命令, master 收到该命令后判断是进行部分重同步还是完整重同步,然后根据策
略进行数据的同步。
9)命令传播
当完成了同步之后,就会进入命令传播阶段,这时 master 只要一直将自己执行的写命令发送给 slave ,而 slave 只
要一直接收并执行 master 发来的写命令,就可以保证 master 和 slave 一直保持一致了。
以部分重同步为例,主从复制的核心步骤流程图如下:
子主题
哨兵
哨兵(Sentinel)是 Redis 的高可用性解决方案:由一个或多个 Sentinel 实例组成的 Sentinel 系统可以监视任意多
个主服务器,以及这些主服务器属下的所有从服务器。
Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然
后由新的主服务器代替已下线的主服务器继续处理命令请求。
1)哨兵故障检测
检查主观下线状态
在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他
Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。
如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所
对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。
检查客观下线状态
当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服
务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。
当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器
置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。
2)哨兵故障转移流程
当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。核心流程如下:
1. 发起一次选举,选举出领头 Sentinel
2. 领头 Sentinel 在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。
3. 领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。
4. 领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。
个主服务器,以及这些主服务器属下的所有从服务器。
Sentinel 可以在被监视的主服务器进入下线状态时,自动将下线主服务器的某个从服务器升级为新的主服务器,然
后由新的主服务器代替已下线的主服务器继续处理命令请求。
1)哨兵故障检测
检查主观下线状态
在默认情况下,Sentinel 会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器、从服务器、其他
Sentinel 在内)发送 PING 命令,并通过实例返回的 PING 命令回复来判断实例是否在线。
如果一个实例在 down-after-miliseconds 毫秒内,连续向 Sentinel 返回无效回复,那么 Sentinel 会修改这个实例所
对应的实例结构,在结构的 flags 属性中设置 SRI_S_DOWN 标识,以此来表示这个实例已经进入主观下线状态。
检查客观下线状态
当 Sentinel 将一个主服务器判断为主观下线之后,为了确定这个主服务器是否真的下线了,它会向同样监视这一服
务器的其他 Sentinel 进行询问,看它们是否也认为主服务器已经进入了下线状态(可以是主观下线或者客观下线)。
当 Sentinel 从其他 Sentinel 那里接收到足够数量(quorum,可配置)的已下线判断之后,Sentinel 就会将服务器
置为客观下线,在 flags 上打上 SRI_O_DOWN 标识,并对主服务器执行故障转移操作。
2)哨兵故障转移流程
当哨兵监测到某个主节点客观下线之后,就会开始故障转移流程。核心流程如下:
1. 发起一次选举,选举出领头 Sentinel
2. 领头 Sentinel 在已下线主服务器的所有从服务器里面,挑选出一个从服务器,并将其升级为新的主服务器。
3. 领头 Sentinel 将剩余的所有从服务器改为复制新的主服务器。
4. 领头 Sentinel 更新相关配置信息,当这个旧的主服务器重新上线时,将其设置为新的主服务器的从服务器。
集群模式
哨兵模式最大的缺点就是所有的数据都放在一台服务器上,无法较好的进行水平扩展。
为了解决哨兵模式存在的问题,集群模式应运而生。
在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。
为了解决哨兵模式存在的问题,集群模式应运而生。
在高可用上,集群基本是直接复用的哨兵模式的逻辑,并且针对水平扩展进行了优化。
集群模式具备的特点如下:
1. 采取去中心化的集群模式,将数据按槽存储分布在多个 Redis 节点上。集群共有 16384 个槽,每个节点负责处理部分槽。
2. 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383。
3. 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
4. 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。
在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,下面的架构图就是这样设计的。
5. 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
2. 使用 CRC16 算法来计算 key 所属的槽:crc16(key,keylen) & 16383。
3. 所有的 Redis 节点彼此互联,通过 PING-PONG 机制来进行节点间的心跳检测。
4. 分片内采用一主多从保证高可用,并提供复制和故障恢复功能。
在实际使用中,通常会将主从分布在不同机房,避免机房出现故障导致整个分片出问题,下面的架构图就是这样设计的。
5. 客户端与 Redis 节点直连,不需要中间代理层(proxy)。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。
集群的架构图如下所示:
子主题
集群选举
故障转移的第一步就是选举出新的主节点,以下是集群选举新的主节点的方法:
1)当从节点发现自己正在复制的主节点进入已下线状态时,会发起一次选举:将 currentEpoch(配置纪元)加 1,
然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票
权的主节点向这个从节点投票。
2)其他节点收到消息后,会判断是否要给发送消息的节点投票,判断流程如下:
1. 当前节点是 slave,或者当前节点是 master,但是不负责处理槽,则当前节点没有投票权,直接返回。
2. 请求节点的 currentEpoch 小于当前节点的 currentEpoch,校验失败返回。
因为发送者的状态与当前集群状态不一致,可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可。
3. 当前节点在该 currentEpoch 已经投过票,校验失败返回。
4. 请求节点是 master,校验失败返回。
5. 请求节点的 master 为空,校验失败返回。
6. 请求节点的 master 没有故障,并且不是手动故障转移,校验失败返回。因为手动故障转移是可以在 master正常的情况下直接发起的。
7. 上一次为该 master 的投票时间,在 cluster_node_timeout 的 2 倍范围内,校验失败返回。
这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移
8. 请求节点宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更大的 configEpoch,如果不是,校验失败返回。
如果通过以上所有校验,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。
3)每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少个主节点的支持。
4)如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N/2+1 张支持票时,这个从节点就会当选为新的主节点。
因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N 个主节点进行投票,那么具有大于等于 N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
5)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
这个选举新主节点的方法和选举领头 Sentinel 的方法非常相似,因为两者都是基于 Raft 算法的领头选举(leader election)方法来实现的。
然后向集群广播一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST 消息,要求所有收到这条消息、并且具有投票
权的主节点向这个从节点投票。
2)其他节点收到消息后,会判断是否要给发送消息的节点投票,判断流程如下:
1. 当前节点是 slave,或者当前节点是 master,但是不负责处理槽,则当前节点没有投票权,直接返回。
2. 请求节点的 currentEpoch 小于当前节点的 currentEpoch,校验失败返回。
因为发送者的状态与当前集群状态不一致,可能是长时间下线的节点刚刚上线,这种情况下,直接返回即可。
3. 当前节点在该 currentEpoch 已经投过票,校验失败返回。
4. 请求节点是 master,校验失败返回。
5. 请求节点的 master 为空,校验失败返回。
6. 请求节点的 master 没有故障,并且不是手动故障转移,校验失败返回。因为手动故障转移是可以在 master正常的情况下直接发起的。
7. 上一次为该 master 的投票时间,在 cluster_node_timeout 的 2 倍范围内,校验失败返回。
这个用于使获胜从节点有时间将其成为新主节点的消息通知给其他从节点,从而避免另一个从节点发起新一轮选举又进行一次没必要的故障转移
8. 请求节点宣称要负责的槽位,是否比之前负责这些槽位的节点,具有相等或更大的 configEpoch,如果不是,校验失败返回。
如果通过以上所有校验,那么主节点将向要求投票的从节点返回一条 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,表示这个主节点支持从节点成为新的主节点。
3)每个参与选举的从节点都会接收 CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK 消息,并根据自己收到了多少条这种消息来统计自己获得了多少个主节点的支持。
4)如果集群里有 N 个具有投票权的主节点,那么当一个从节点收集到大于等于 N/2+1 张支持票时,这个从节点就会当选为新的主节点。
因为在每一个配置纪元里面,每个具有投票权的主节点只能投一次票,所以如果有 N 个主节点进行投票,那么具有大于等于 N/2+1 张支持票的从节点只会有一个,这确保了新的主节点只会有一个。
5)如果在一个配置纪元里面没有从节点能收集到足够多的支持票,那么集群进入一个新的配置纪元,并再次进行选举,直到选出新的主节点为止。
这个选举新主节点的方法和选举领头 Sentinel 的方法非常相似,因为两者都是基于 Raft 算法的领头选举(leader election)方法来实现的。
如何保证集群在线扩容的安全性?
(Redis 集群要增加分片,槽的迁移怎么保证无损)
(Redis 集群要增加分片,槽的迁移怎么保证无损)
例如:集群已经对外提供服务,原来有 3 分片,准备新增 2 个分片,怎么在不下线的情况下,无损的从原有的 3 个分片指派若干个槽给这 2 个分片?
Redis 使用了 ASK 错误来保证在线扩容的安全性。
Redis 使用了 ASK 错误来保证在线扩容的安全性。
在槽的迁移过程中若有客户端访问,依旧先访问源节点,源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。
如果没找到,说明该键可能已经被迁移到目标节点了,源节点将向客户端返回一个 ASK 错误,该错误会指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
ASK 错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时。源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。否则,这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
如果没找到,说明该键可能已经被迁移到目标节点了,源节点将向客户端返回一个 ASK 错误,该错误会指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
ASK 错误
在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面。当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于正在被迁移的槽时。源节点会先在自己的数据库里面査找指定的键,如果找到的话,就直接执行客户端发送的命令。否则,这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令,从而获取到结果。
Redis持久化
Redis 提供了哪几种持久化方式?
RDB 持久化方式能够在指定的时间间隔能对你的数据进行快照存储.
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来
恢复原始的数据,AOF 命令以Redis 协议追加保存每次写的操作到文件末尾.Redis 还能对
AOF 文件进行后台重写,使得AOF 文件的体积不至于过大.
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
你也可以同时开启两种持久化方式, 在这种情况下, 当Redis 重启的时候会优先载入AOF
文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB 文件保存的数
据集要完整.
最重要的事情是了解RDB 和AOF 持久化方式的不同,让我们以RDB 持久化方式开始。
AOF 持久化方式记录每次对服务器写的操作,当服务器重启的时候会重新执行这些命令来
恢复原始的数据,AOF 命令以Redis 协议追加保存每次写的操作到文件末尾.Redis 还能对
AOF 文件进行后台重写,使得AOF 文件的体积不至于过大.
如果你只希望你的数据在服务器运行的时候存在,你也可以不使用任何持久化方式.
你也可以同时开启两种持久化方式, 在这种情况下, 当Redis 重启的时候会优先载入AOF
文件来恢复原始的数据,因为在通常情况下AOF 文件保存的数据集要比RDB 文件保存的数
据集要完整.
最重要的事情是了解RDB 和AOF 持久化方式的不同,让我们以RDB 持久化方式开始。
如何选择合适的持久化方式?
一般来说, 如果想达到足以媲美PostgreSQL 的数据安全性, 你应该同时使用两种持久
化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以
只使用RDB 持久化。
有很多用户都只使用AOF 持久化,但并不推荐这种方式:因为定时生成RDB 快照
(snapshot)非常便于进行数据库备份, 并且RDB 恢复数据集的速度也要比AOF 恢复
的速度要快,除此之外, 使用RDB 还可以避免之前提到的AOF 程序的bug。
化功能。如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失,那么你可以
只使用RDB 持久化。
有很多用户都只使用AOF 持久化,但并不推荐这种方式:因为定时生成RDB 快照
(snapshot)非常便于进行数据库备份, 并且RDB 恢复数据集的速度也要比AOF 恢复
的速度要快,除此之外, 使用RDB 还可以避免之前提到的AOF 程序的bug。
Redis 持久化数据和缓存怎么做扩容?
如果Redis 被当做缓存使用,使用一致性哈希实现动态扩容缩容。
如果Redis 被当做一个持久化存储使用,必须使用固定的keys-to-nodes 映射关系,节点的
数量一旦确定不能变化。否则的话(即Redis 节点需要动态变化的情况),必须使用可以在运
行时进行数据再平衡的一套系统,而当前只有Redis 集群可以做到这样。
如果Redis 被当做一个持久化存储使用,必须使用固定的keys-to-nodes 映射关系,节点的
数量一旦确定不能变化。否则的话(即Redis 节点需要动态变化的情况),必须使用可以在运
行时进行数据再平衡的一套系统,而当前只有Redis 集群可以做到这样。
知道redis的持久化吗?底层如何实现的?有什么优点缺点?
RDB(Redis DataBase:在不同的时间点将redis的数据生成的快照同步到磁盘等介质上):内存到硬盘的快照,定期更新。
缺点:耗时,耗性能(fork+io操作),易丢失数据。
AOF(Append Only File:将redis所执行过的所有指令都记录下来,在下次redis重启时,只需要执行指令就可以了):写日志。
缺点:体积大,恢复速度慢。
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会消耗比较长的时间,不够实时,在停机的时候会导致大量的数据丢失,需要aof来配合,在redis实例重启时,优先使用aof来恢复内存的状态,如果没有aof日志,就会使用rdb文件来恢复。Redis会定期做aof重写,压缩aof文件日志大小。Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。
bgsave的原理,fork和cow, fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
缺点:耗时,耗性能(fork+io操作),易丢失数据。
AOF(Append Only File:将redis所执行过的所有指令都记录下来,在下次redis重启时,只需要执行指令就可以了):写日志。
缺点:体积大,恢复速度慢。
bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会消耗比较长的时间,不够实时,在停机的时候会导致大量的数据丢失,需要aof来配合,在redis实例重启时,优先使用aof来恢复内存的状态,如果没有aof日志,就会使用rdb文件来恢复。Redis会定期做aof重写,压缩aof文件日志大小。Redis4.0之后有了混合持久化的功能,将bgsave的全量和aof的增量做了融合处理,这样既保证了恢复的效率又兼顾了数据的安全性。
bgsave的原理,fork和cow, fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
Redis 的持久化机制有哪几种,各自的实现原理和优缺点?
Redis 的持久化机制有:RDB、AOF、混合持久化(RDB+AOF,Redis 4.0 引入)。
1)RDB
描述:类似于快照。
在某个时间点,将 Redis 在内存中的数据库状态(数据库的键值对等信息)保存到磁盘里面。
RDB 持久化功能生成的 RDB 文件是经过压缩的二进制文件。
详细描述
命令:有两个 Redis 命令可以用于生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE。
开启:使用 save point 配置,满足 save point 条件后会触发 BGSAVE 来存储一次快照,这边的 save point 检查就是在上文提到的 serverCron 中进行。
save point 格式:save <seconds> <changes>,含义是 Redis 如果在 seconds 秒内数据发生了 changes 次改变,就保存快照文件。
例如 Redis 默认就配置了以下 3 个:
1)注释掉所有 save point 配置可以关闭 RDB 持久化。
2)在所有 save point 配置后增加:save "",该配置可以删除所有之前配置的 save point。
save " "
SAVE:
生成 RDB 快照文件,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。
BGSAVE:
fork 子进程来生成 RDB 快照文件,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求,
详细过程如下图:
fork:
在 Linux 系统中,调用 fork() 时,会创建出一个新进程,称为子进程,子进程会拷贝父进程的 page table。
如果进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。
如果 Redis 占用的内存很大,那么在 fork 子进程时,则会出现明显的停顿现象。
开启:使用 save point 配置,满足 save point 条件后会触发 BGSAVE 来存储一次快照,这边的 save point 检查就是在上文提到的 serverCron 中进行。
save point 格式:save <seconds> <changes>,含义是 Redis 如果在 seconds 秒内数据发生了 changes 次改变,就保存快照文件。
例如 Redis 默认就配置了以下 3 个:
- save 900 1 #900 秒内有 1 个 key 发生了变化,则触发保存 RDB 文件
- save 300 10 #300 秒内有 10 个 key 发生了变化,则触发保存 RDB 文件
- save 60 10000 #60 秒内有 10000 个 key 发生了变化,则触发保存 RDB 文件
1)注释掉所有 save point 配置可以关闭 RDB 持久化。
2)在所有 save point 配置后增加:save "",该配置可以删除所有之前配置的 save point。
save " "
SAVE:
生成 RDB 快照文件,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,所以通常不会直接使用该命令。
BGSAVE:
fork 子进程来生成 RDB 快照文件,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请求,
详细过程如下图:
fork:
在 Linux 系统中,调用 fork() 时,会创建出一个新进程,称为子进程,子进程会拷贝父进程的 page table。
如果进程占用的内存越大,进程的 page table 也会越大,那么 fork 也会占用更多的时间。
如果 Redis 占用的内存很大,那么在 fork 子进程时,则会出现明显的停顿现象。
子主题
RDB 的优点
1)RDB 文件是是经过压缩的二进制文件,占用空间很小,它保存了 Redis 某个时间点的数据集,很适合用于做备
份。 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB
文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
2)RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将
它传送到别的数据中心。
3)RDB 可以最大化 redis 的性能。父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进
程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
4)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
份。 比如说,你可以在最近的 24 小时内,每小时备份一次 RDB 文件,并且在每个月的每一天,也备份一个 RDB
文件。这样的话,即使遇上问题,也可以随时将数据集还原到不同的版本。
2)RDB 非常适用于灾难恢复(disaster recovery):它只有一个文件,并且内容都非常紧凑,可以(在加密后)将
它传送到别的数据中心。
3)RDB 可以最大化 redis 的性能。父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进
程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
4)RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
RDB 的缺点
1)RDB 在服务器故障时容易造成数据的丢失。RDB 允许我们通过修改 save point 配置来控制持久化的频率。但是,
因为 RDB 文件需要保存整个数据集的状态, 所以它是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能
产生影响。所以通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢
失 5 分钟数据。
2)RDB 保存时使用 fork 子进程进行数据的持久化,如果数据比较大的话,fork 可能会非常耗时,造成 Redis 停
止处理服务 N 毫秒。如果数据集很大且 CPU 比较繁忙的时候,停止服务的时间甚至会到一秒。
3)Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频
繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的 2 倍。刚 fork 时,主进程和子进程共
享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,
如果所有的页面都被修改,则此时的内存占用是原先的 2 倍。
因为 RDB 文件需要保存整个数据集的状态, 所以它是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能
产生影响。所以通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢
失 5 分钟数据。
2)RDB 保存时使用 fork 子进程进行数据的持久化,如果数据比较大的话,fork 可能会非常耗时,造成 Redis 停
止处理服务 N 毫秒。如果数据集很大且 CPU 比较繁忙的时候,停止服务的时间甚至会到一秒。
3)Linux fork 子进程采用的是 copy-on-write 的方式。在 Redis 执行 RDB 持久化期间,如果 client 写入数据很频
繁,那么将增加 Redis 占用的内存,最坏情况下,内存的占用将达到原先的 2 倍。刚 fork 时,主进程和子进程共
享内存,但是随着主进程需要处理写操作,主进程需要将修改的页面拷贝一份出来,然后进行修改。极端情况下,
如果所有的页面都被修改,则此时的内存占用是原先的 2 倍。
2)AOF
描述:保存 Redis 服务器所执行的所有写操作命令来记录数据库状态,并在服务器启动时,通过重新执行这些命令
来还原数据集。
开启:AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。
关闭:使用配置 appendonly no 可以关闭 AOF 持久化。
来还原数据集。
开启:AOF 持久化默认是关闭的,可以通过配置:appendonly yes 开启。
关闭:使用配置 appendonly no 可以关闭 AOF 持久化。
AOF 持久化功能的实现可以分为三个步骤:
命令追加
当 AOF 持久化功能打开时,服务器在执行完一个写命令之后,会将被执行的写命令追加到服务器状态的 aof 缓冲区(aof_buf)的末尾。
文件写入
文件同步
文件写入与文件同步:可能有人不明白为什么将 aof_buf 的内容写到磁盘上需要两步操作,这边简单解释一下。
Linux 操作系统中为了提升性能,使用了页缓存(page cache)。
当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,
而是在 page cache 中,为了将 page cache 中的数据真正落盘,需要执行 fsync / fdatasync 命令来强制刷盘。
这边的文件同步做的就是刷盘操作,或者叫文件刷盘可能更容易理解一些。
在文章开头,我们提过 serverCron 时间事件中会触发 flushAppendOnlyFile 函数,
该函数会根据服务器配置的appendfsync 参数值,来决定是否将 aof_buf 缓冲区的内容写入和保存到 AOF 文件。
当我们将 aof_buf 的内容写到磁盘上时,此时数据并没有真正的落盘,
而是在 page cache 中,为了将 page cache 中的数据真正落盘,需要执行 fsync / fdatasync 命令来强制刷盘。
这边的文件同步做的就是刷盘操作,或者叫文件刷盘可能更容易理解一些。
在文章开头,我们提过 serverCron 时间事件中会触发 flushAppendOnlyFile 函数,
该函数会根据服务器配置的appendfsync 参数值,来决定是否将 aof_buf 缓冲区的内容写入和保存到 AOF 文件。
appendfsync 参数有三个选项:
1. always:每处理一个命令都将 aof_buf 缓冲区中的所有内容写入并同步到 AOF 文件,即每个命令都刷盘。
2. everysec:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超
过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是异步的,由一个后台线程专门负责执行,
即每秒刷盘 1 次。
3. no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作
系统来决定。即不执行刷盘,让操作系统自己执行刷盘。
2. everysec:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件,如果上次同步 AOF 文件的时间距离现在超
过一秒钟, 那么再次对 AOF 文件进行同步, 并且这个同步操作是异步的,由一个后台线程专门负责执行,
即每秒刷盘 1 次。
3. no:将 aof_buf 缓冲区中的所有内容写入到 AOF 文件, 但并不对 AOF 文件进行同步, 何时同步由操作
系统来决定。即不执行刷盘,让操作系统自己执行刷盘。
AOF 的优点
1. AOF 比 RDB 可靠。你可以设置不同的 fsync 策略:no、everysec 和 always。默认是 everysec,在这种配
置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。
2. AOF文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘
已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
3. 当 AOF文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最
小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追
加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
4. AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非
常容易被人读懂, 对文件进行分析(parse)也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷
掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并
重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
置下,redis 仍然可以保持良好的性能,并且就算发生故障停机,也最多只会丢失一秒钟的数据。
2. AOF文件是一个纯追加的日志文件。即使日志因为某些原因而包含了未写入完整的命令(比如写入时磁盘
已满,写入中途停机等等), 我们也可以使用 redis-check-aof 工具也可以轻易地修复这种问题。
3. 当 AOF文件太大时,Redis 会自动在后台进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最
小命令集合。整个重写是绝对安全,因为重写是在一个新的文件上进行,同时 Redis 会继续往旧的文件追
加数据。当新文件重写完毕,Redis 会把新旧文件进行切换,然后开始把数据写到新文件上。
4. AOF 文件有序地保存了对数据库执行的所有写入操作以 Redis 协议的格式保存, 因此 AOF 文件的内容非
常容易被人读懂, 对文件进行分析(parse)也很轻松。如果你不小心执行了 FLUSHALL 命令把所有数据刷
掉了,但只要 AOF 文件没有被重写,那么只要停止服务器, 移除 AOF 文件末尾的 FLUSHALL 命令, 并
重启 Redis , 就可以将数据集恢复到 FLUSHALL 执行之前的状态。
AOF 的缺点
1. 对于相同的数据集,AOF 文件的大小一般会比 RDB 文件大。
2. 根据所使用的 fsync 策略,AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的
性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。
3. AOF 在过去曾经发生过这样的 bug :因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集
恢复成保存时的原样。(举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug ) 。虽然这种 bug
在 AOF 文件中并不常见, 但是相较而言, RDB 几乎是不可能出现这种 bug 的。
2. 根据所使用的 fsync 策略,AOF 的速度可能会比 RDB 慢。通常 fsync 设置为每秒一次就能获得比较高的
性能,而关闭 fsync 可以让 AOF 的速度和 RDB 一样快。
3. AOF 在过去曾经发生过这样的 bug :因为个别命令的原因,导致 AOF 文件在重新载入时,无法将数据集
恢复成保存时的原样。(举个例子,阻塞命令 BRPOPLPUSH 就曾经引起过这样的 bug ) 。虽然这种 bug
在 AOF 文件中并不常见, 但是相较而言, RDB 几乎是不可能出现这种 bug 的。
3)混合持久化
描述:混合持久化并不是一种全新的持久化方式,而是对已有方式的优化。混合持久化只发生于 AOF 重写过程。
使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
整体格式为:[RDBfile][AOFtail]
使用了混合持久化,重写后的新 AOF 文件前半段是 RDB 格式的全量数据,后半段是 AOF 格式的增量数据。
整体格式为:[RDBfile][AOFtail]
开启:混合持久化的配置参数为 aof-use-rdb-preamble,配置为 yes 时开启混合持久化,在 redis 4 刚引入时,默
认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。
关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。
混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子
进程先将当前全量数据以 RDB 方式写入新的 AOF文件,然后再将 AOF重写缓冲区(aof_rewrite_buf_blocks)的增
量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
优点:结合 RDB 和 AOF的优点, 更快的重写和恢复。
缺点:AOF文件里面的 RDB部分不再是 AOF 格式,可读性差。
认是关闭混合持久化的,但是在 redis 5 中默认已经打开了。
关闭:使用 aof-use-rdb-preamble no 配置即可关闭混合持久化。
混合持久化本质是通过 AOF 后台重写(bgrewriteaof 命令)完成的,不同的是当开启混合持久化时,fork 出的子
进程先将当前全量数据以 RDB 方式写入新的 AOF文件,然后再将 AOF重写缓冲区(aof_rewrite_buf_blocks)的增
量命令以 AOF 方式写入到文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
优点:结合 RDB 和 AOF的优点, 更快的重写和恢复。
缺点:AOF文件里面的 RDB部分不再是 AOF 格式,可读性差。
为什么需要 AOF重写
AOF 持久化是通过保存被执行的写命令来记录数据库状态的,随着写入命令的不断增加,AOF 文件中的内容会越来越多,文件的体积也会越来越大。
如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,
并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。
举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录。
然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下, 对 AOF 文件进行重建(rebuild)。
如果不加以控制,体积过大的 AOF 文件可能会对 Redis 服务器、甚至整个宿主机造成影响,
并且 AOF 文件的体积越大,使用 AOF 文件来进行数据还原所需的时间就越多。
举个例子, 如果你对一个计数器调用了 100 次 INCR , 那么仅仅是为了保存这个计数器的当前值, AOF 文件就需要使用 100 条记录。
然而在实际上, 只使用一条 SET 命令已经足以保存计数器的当前值了, 其余 99 条记录实际上都是多余的。
为了处理这种情况, Redis 引入了 AOF 重写:可以在不打断服务端处理请求的情况下, 对 AOF 文件进行重建(rebuild)。
介绍下 AOF重写的过程、AOF 后台重写存在的问题、
如何解决 AOF 后台重写存在的数据不一致问题
如何解决 AOF 后台重写存在的数据不一致问题
描述:Redis 生成新的 AOF 文件来代替旧 AOF 文件,这个新的 AOF 文件包含重建当前数据集所需的最少命令。
具体过程是遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这
个键值对的多条命令。
命令:有两个 Redis 命令可以用于触发 AOF 重写,一个是 BGREWRITEAOF 、另一个是 REWRITEAOF 命令;
开启:AOF 重写由两个参数共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同时满足这两个条
件,则触发 AOF 后台重写 BGREWRITEAOF。
具体过程是遍历所有数据库的所有键,从数据库读取键现在的值,然后用一条命令去记录键值对,代替之前记录这
个键值对的多条命令。
命令:有两个 Redis 命令可以用于触发 AOF 重写,一个是 BGREWRITEAOF 、另一个是 REWRITEAOF 命令;
开启:AOF 重写由两个参数共同控制,auto-aof-rewrite-percentage 和 auto-aof-rewrite-min-size,同时满足这两个条
件,则触发 AOF 后台重写 BGREWRITEAOF。
/ / 当前 AOF 文件比上次重写后的 AOF 文件大小的增长比例超过 100
auto-aof-rewrite-percentage 100
/ / 当前 AOF 文件的文件大小大于 64MB
auto-aof-rewrite-min-size 64mb
auto-aof-rewrite-percentage 100
/ / 当前 AOF 文件的文件大小大于 64MB
auto-aof-rewrite-min-size 64mb
关闭:auto-aof-rewrite-percentage 0,指定 0 的百分比,以禁用自动 AOF 重写功能。
auto-aof-rewrite-percentage 0
auto-aof-rewrite-percentage 0
REWRITEAOF:进行 AOF 重写,但是会阻塞主进程,服务器将无法处理客户端发来的命令请求,通常不会直接使用
该命令。
BGREWRITEAOF:fork 子进程来进行 AOF 重写,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请
求。
REWRITEAOF和 BGREWRITEAOF 的关系与 SAVE和 BGSAVE 的关系类似。
该命令。
BGREWRITEAOF:fork 子进程来进行 AOF 重写,阻塞只会发生在 fork 子进程的时候,之后主进程可以正常处理请
求。
REWRITEAOF和 BGREWRITEAOF 的关系与 SAVE和 BGSAVE 的关系类似。
AOF 后台重写存在的问题
AOF 后台重写使用子进程进行从写,解决了主进程阻塞的问题,但是仍然存在另一个问题:子进程在进行 AOF 重
写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的
数据库状态和重写后的 AOF 文件保存的数据库状态不一致。
写期间,服务器主进程还需要继续处理命令请求,新的命令可能会对现有的数据库状态进行修改,从而使得当前的
数据库状态和重写后的 AOF 文件保存的数据库状态不一致。
如何解决 AOF 后台重写存在的数据不一致问题
为了解决上述问题,Redis 引入了 AOF 重写缓冲区(aof_rewrite_buf_blocks),这个缓冲区在服务器创建子进程之
后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓
冲区。
这样一来可以保证:
1、现有 AOF 文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
2、从创建子进程开始,也就是 AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。
这样,当子进程完成 AOF 重写工作后,父进程会在 serverCron 中检测到子进程已经重写结束,则会执行以下工作:
1、将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当
前的数据库状态一致。
2、对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。
之后,父进程就可以继续像往常一样接受命令请求了。
后开始使用,当 Redis 服务器执行完一个写命令之后,它会同时将这个写命令追加到 AOF 缓冲区和 AOF 重写缓
冲区。
这样一来可以保证:
1、现有 AOF 文件的处理工作会如常进行。这样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。
2、从创建子进程开始,也就是 AOF 重写开始,服务器执行的所有写命令会被记录到 AOF 重写缓冲区里面。
这样,当子进程完成 AOF 重写工作后,父进程会在 serverCron 中检测到子进程已经重写结束,则会执行以下工作:
1、将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中,这时新 AOF 文件所保存的数据库状态将和服务器当
前的数据库状态一致。
2、对新的 AOF 文件进行改名,原子的覆盖现有的 AOF 文件,完成新旧两个 AOF 文件的替换。
之后,父进程就可以继续像往常一样接受命令请求了。
RDB、AOF、混合持久,我应该用哪一个?
一般来说, 如果想尽量保证数据安全性, 你应该同时使用 RDB 和 AOF 持久化功能,同时可以开启混合持久化。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
如果你的数据是可以丢失的,则可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。
使用 Redis 通常都是为了提升性能,而如果为了不丢失数据而将 appendfsync 设置为 always 级别时,对 Redis 的
性能影响是很大的,在这种不能接受数据丢失的场景,其实可以考虑直接选择 MySQL 等类似的数据库。
如果你非常关心你的数据, 但仍然可以承受数分钟以内的数据丢失, 那么你可以只使用 RDB 持久化。
如果你的数据是可以丢失的,则可以关闭持久化功能,在这种情况下,Redis 的性能是最高的。
使用 Redis 通常都是为了提升性能,而如果为了不丢失数据而将 appendfsync 设置为 always 级别时,对 Redis 的
性能影响是很大的,在这种不能接受数据丢失的场景,其实可以考虑直接选择 MySQL 等类似的数据库。
同时开启 RDB 和 AOF,服务重启时如何加载
简单来说,如果同时启用了 AOF 和 RDB,Redis 重新启动时,会使用 AOF 文件来重建数据集,因为通常来说, AOF的数据会更完整。
而在引入了混合持久化之后,使用 AOF 重建数据集时,会通过文件开头是否为“REDIS”来判断是否为混合持久化。
完整流程如下图所示:
而在引入了混合持久化之后,使用 AOF 重建数据集时,会通过文件开头是否为“REDIS”来判断是否为混合持久化。
完整流程如下图所示:
子主题
Redis客户端(常用工具)
支持一致性哈希的客户端有哪些?
Redis-rb、PRedis 等。
Redis 的 Java 客户端有哪些?官方推荐哪个?
Redis 支持的Java 客户端都有哪些?官方推荐用哪个?
Redisson、Jedis、lettuce 等等,官方推荐使用Redisson。
Redis 官网展示的 Java 客户端如下图所示,其中官方推荐的是标星的 3 个:Jedis、Redisson 和 lettuce。
子主题
Redis 和Redisson 有什么关系?
Redisson 是一个高级的分布式协调Redis 客服端,能帮助用户在分布式环境中轻松实现一些Java 的对象
(Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet,
Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque,
BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong,
CountDownLatch, Publish / Subscribe, HyperLogLog)。
(Bloom filter, BitSet, Set, SetMultimap, ScoredSortedSet, SortedSet,
Map, ConcurrentMap, List, ListMultimap, Queue, BlockingQueue, Deque,
BlockingDeque, Semaphore, Lock, ReadWriteLock, AtomicLong,
CountDownLatch, Publish / Subscribe, HyperLogLog)。
Jedis 与Redisson 对比有什么优缺点?
Jedis 是Redis 的Java 实现的客户端,其API 提供了比较全面的Redis 命令的支持;
Redisson 实现了分布式和可扩展的Java 数据结构,和Jedis 相比,功能较为简单,不支
持字符串操作,不支持排序、事务、管道、分区等Redis 特性。Redisson 的宗旨是促进使
用者对Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redisson 实现了分布式和可扩展的Java 数据结构,和Jedis 相比,功能较为简单,不支
持字符串操作,不支持排序、事务、管道、分区等Redis 特性。Redisson 的宗旨是促进使
用者对Redis 的关注分离,从而让使用者能够将精力更集中地放在处理业务逻辑上。
Redis 常见性能问题
Redis常见的性能问题和解决方案?
1、master最好不要做持久化工作,如RDB内存快照和AOF日志文件
2、如果数据比较重要,某个slave开启AOF备份,策略设置成每秒同步一次
3、为了主从复制的速度和连接的稳定性,master和Slave最好在一个局域网内
4、尽量避免在压力大得主库上增加从库
5、主从复制不要采用网状结构,尽量是线性结构,Master<--Slave1<----Slave2 ....
(1) Master 最好不要做任何持久化工作,如RDB 内存快照和AOF 日志文件
(2) 如果数据比较重要,某个Slave 开启AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master 和Slave 最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2<- Slave3...
这样的结构方便解决单点故障问题,实现Slave 对Master 的替换。如果Master 挂了,可以立刻启用Slave1 做Master,其他不变。
(2) 如果数据比较重要,某个Slave 开启AOF 备份数据,策略设置为每秒同步一次
(3) 为了主从复制的速度和连接的稳定性,Master 和Slave 最好在同一个局域网内
(4) 尽量避免在压力很大的主库上增加从库
(5) 主从复制不要用图状结构,用单向链表结构更为稳定,即:Master <- Slave1 <- Slave2<- Slave3...
这样的结构方便解决单点故障问题,实现Slave 对Master 的替换。如果Master 挂了,可以立刻启用Slave1 做Master,其他不变。
1、master最好不要做持久化工作,如RDB内存快照和AOF日志文件
2、如果数据比较重要,某个slave开启AOF备份,策略设置成每秒同步一次
3、为了主从复制的速度和连接的稳定性,master和Slave最好在一个局域网内
4、尽量避免在压力大得主库上增加从库
5、主从复制不要采用网状结构,尽量是线性结构,Master<--Slave1<----Slave2 ....
1. Master最好不要做任何持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化。
2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
4. 尽量避免在压力较大的主库上增加从库
5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,
这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
2. 如果数据比较关键,某个Slave开启AOF备份数据,策略为每秒同步一次。
3. 为了主从复制的速度和连接的稳定性,Slave和Master最好在同一个局域网内。
4. 尽量避免在压力较大的主库上增加从库
5. Master调用BGREWRITEAOF重写AOF文件,AOF在重写的时候会占大量的CPU和内存资源,导致服务load过高,出现短暂服务暂停现象。
6. 为了Master的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关系为:Master<–Slave1<–Slave2<–Slave3…,
这样的结构也方便解决单点故障问题,实现Slave对Master的替换,也即,如果Master挂了,可以立马启用Slave1做Master,其他不变。
Redis 的内存用完了会发生什么?
如果达到设置的上限,Redis 的写命令会返回错误信息(但是读命令还可以正常返回。)或
者你可以将Redis 当缓存来使用配置淘汰机制,当Redis 达到内存上限时会冲刷掉旧的内容。
者你可以将Redis 当缓存来使用配置淘汰机制,当Redis 达到内存上限时会冲刷掉旧的内容。
Redis 的内存占用情况怎么样?
给你举个例子: 100 万个键值对(键是0 到999999 值是字符串“hello world”)在我的
32 位的Mac 笔记本上用了100MB。同样的数据放到一个key 里只需要16MB, 这是
因为键值有一个很大的开销。在Memcached 上执行也是类似的结果,但是相对Redis
的开销要小一点点,因为Redis 会记录类型信息引用计数等等。
当然,大键值对时两者的比例要好很多。
64 位的系统比32 位的需要更多的内存开销,尤其是键值对都较小时,这是因为64 位的系
统里指针占用了8 个字节。但是,当然,64 位系统支持更大的内存,所以为了运行大型
的Redis 服务器或多或少的需要使用64 位的系统。
32 位的Mac 笔记本上用了100MB。同样的数据放到一个key 里只需要16MB, 这是
因为键值有一个很大的开销。在Memcached 上执行也是类似的结果,但是相对Redis
的开销要小一点点,因为Redis 会记录类型信息引用计数等等。
当然,大键值对时两者的比例要好很多。
64 位的系统比32 位的需要更多的内存开销,尤其是键值对都较小时,这是因为64 位的系
统里指针占用了8 个字节。但是,当然,64 位系统支持更大的内存,所以为了运行大型
的Redis 服务器或多或少的需要使用64 位的系统。
Redis 如何做内存优化?
尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,
所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web 系统中有一个
用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户
的所有信息存储到一张散列表里面.
所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web 系统中有一个
用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户
的所有信息存储到一张散列表里面.
都有哪些办法可以降低Redis 的内存使用情况呢?
如果你使用的是32 位的Redis 实例,可以好好利用Hash,list,sorted set,set 等集合类型数据,
因为通常情况下很多小的Key-Value 可以用更紧凑的方式存放到一起。
因为通常情况下很多小的Key-Value 可以用更紧凑的方式存放到一起。
修改配置不重启Redis 会实时生效吗?
针对运行实例,有许多配置选项可以通过CONFIG SET 命令进行修改,而无需执行任何
形式的重启。从Redis 2.2 开始,可以从AOF 切换到RDB 的快照持久性或其他方式
而不需要重启Redis。检索‘CONFIG GET *’ 命令获取更多信息。
但偶尔重新启动是必须的,如为升级Redis 程序到新的版本,或者当你需要修改某些目前
CONFIG 命令还不支持的配置参数的时候。
针对运行实例,有许多配置选项可以通过CONFIG SET 命令进行修改,而无需执行任何
形式的重启。从Redis 2.2 开始,可以从AOF 切换到RDB 的快照持久性或其他方式
而不需要重启Redis。检索‘CONFIG GET *’ 命令获取更多信息。
但偶尔重新启动是必须的,如为升级Redis 程序到新的版本,或者当你需要修改某些目前
CONFIG 命令还不支持的配置参数的时候。
查看Redis 使用情况及状态信息用什么命令?
info
缓存穿透、缓存击穿、缓存雪崩解决方案?缓存异常
缓存穿透
指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到DB去查询,可能导致DB挂掉。
解决方案:
1.查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短;
2.布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询。
1.查询返回的数据为空,仍把这个空结果进行缓存,但过期时间会比较短;
2.布隆过滤器:将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对DB的查询。
缓存穿透是指缓存和数据库中都没有的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
解决方案
1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将keyvalue对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
附加
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,
恐怕只能靠牺牲更多的空间、时间来完成了。
布隆过滤器(推荐)
就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能
相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值
我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash
函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是
Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
解决方案
1. 接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2. 从缓存取不到的数据,在数据库中也没有取到,这时也可以将keyvalue对写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击
3. 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmap中,一个一定不存在的数据会被这个 bitmap 拦截掉,从而避免了对底层存储系统的查询压力
附加
对于空间的利用到达了一种极致,那就是Bitmap和布隆过滤器(Bloom Filter)。
Bitmap: 典型的就是哈希表缺点是,Bitmap对于每个元素只能记录1bit信息,如果还想完成额外的功能,
恐怕只能靠牺牲更多的空间、时间来完成了。
布隆过滤器(推荐)
就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。
它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
Bloom-Filter算法的核心思想就是利用多个不同的Hash函数来解决“冲突”。
Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能
相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值
我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash
函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是
Bloom-Filter的基本思想。
Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
描述:访问一个缓存和数据库都不存在的 key,此时会直接打到数据库上,并且查不到数据,没法写缓存,所以下
一次同样会打到数据库上。此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一
样,起不到任何作用。
一次同样会打到数据库上。此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一
样,起不到任何作用。
解决方案:
1)接口校验。在正常业务流程中可能会存在少量访问不存在 key 的情况,但是一般不会出现大量的情况,所以这
种场景最大的可能性是遭受了非法攻击。可以在最外层先做一层校验:用户鉴权、数据合法性校验等,例如商品查
询中,商品的 ID 是正整数,则可以直接对非正整数直接过滤等等。
2)缓存空值。当访问缓存和 DB 都没有查询到值时,可以将空值写进缓存,但是设置较短的过期时间,该时间需要
根据产品业务特性来设置。
3)布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查
询缓存和数据库。
布隆过滤器
布隆过滤器的特点是判断不存在的,则一定不存在;判断存在的,大概率存在,但也有小概率不存在。
并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。
布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。
在初始化时,bitSet 的每一位被初始化为 0,同时会定义 Hash 函数,例如有 3 组 Hash 函数:hash1、hash2、hash3。
并且这个概率是可控的,我们可以让这个概率变小或者变高,取决于用户本身的需求。
布隆过滤器由一个 bitSet 和 一组 Hash 函数(算法)组成,是一种空间效率极高的概率型算法和数据结构,主要用来判断一个元素是否在集合中存在。
在初始化时,bitSet 的每一位被初始化为 0,同时会定义 Hash 函数,例如有 3 组 Hash 函数:hash1、hash2、hash3。
写入流程
当我们要写入一个值时,过程如下,以“jionghui”为例:
1)首先将“jionghui”跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、7、10。
2)将 bitSet 的这 3 个下标标记为 1。
假设我们还有另外两个值:java 和 diaosi,按上面的流程跟 3 组 Hash 函数分别计算,结果如下:
java:Hash 函数计算 bitSet 下标为:1、7、11
diaosi:Hash 函数计算 bitSet 下标为:4、10、11
当我们要写入一个值时,过程如下,以“jionghui”为例:
1)首先将“jionghui”跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、7、10。
2)将 bitSet 的这 3 个下标标记为 1。
假设我们还有另外两个值:java 和 diaosi,按上面的流程跟 3 组 Hash 函数分别计算,结果如下:
java:Hash 函数计算 bitSet 下标为:1、7、11
diaosi:Hash 函数计算 bitSet 下标为:4、10、11
子主题
查询流程
当我们要查询一个值时,过程如下,同样以“jionghui”为例:
1)首先将“jionghui”跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、7、10。
2)查看 bitSet 的这 3 个下标是否都为 1,
如果这 3 个下标不都为 1,则说明该值必然不存在,
如果这 3 个下标都为 1,则只能说明可能存在,并不能说明一定存在。
其实上图的例子已经说明了这个问题了,当我们只有值“jionghui”和“diaosi”时,bitSet 下标为 1 的有:1、4、7、10、11。
当我们又加入值“java”时,bitSet 下标为 1 的还是这 5 个,所以当 bitSet 下标为 1 的为:1、4、7、10、11 时,我们无法判断值“java”存不存在。
其根本原因是,不同的值在跟 Hash 函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了。
这也是为啥布隆过滤器只能判断某个值可能存在,无法判断必然存在的原因。
但是反过来,如果该值根据 Hash 函数计算的标记位没有全部都为 1,那么则说明必然不存在,这个是肯定的。
降低这种误判率的思路也比较简单:
x一个是加大 bitSet 的长度,这样不同的值出现“冲突”的概率就降低了,从而误判率也降低。
x提升 Hash 函数的个数,Hash 函数越多,每个值对应的 bit 越多,从而误判率也降低。
布隆过滤器的误判率还有专门的推导公式,有兴趣的可以去搜相关的文章和论文查看。
1)首先将“jionghui”跟 3 组 Hash 函数分别计算,得到 bitSet 的下标为:1、7、10。
2)查看 bitSet 的这 3 个下标是否都为 1,
如果这 3 个下标不都为 1,则说明该值必然不存在,
如果这 3 个下标都为 1,则只能说明可能存在,并不能说明一定存在。
其实上图的例子已经说明了这个问题了,当我们只有值“jionghui”和“diaosi”时,bitSet 下标为 1 的有:1、4、7、10、11。
当我们又加入值“java”时,bitSet 下标为 1 的还是这 5 个,所以当 bitSet 下标为 1 的为:1、4、7、10、11 时,我们无法判断值“java”存不存在。
其根本原因是,不同的值在跟 Hash 函数计算后,可能会得到相同的下标,所以某个值的标记位,可能会被其他值给标上了。
这也是为啥布隆过滤器只能判断某个值可能存在,无法判断必然存在的原因。
但是反过来,如果该值根据 Hash 函数计算的标记位没有全部都为 1,那么则说明必然不存在,这个是肯定的。
降低这种误判率的思路也比较简单:
x一个是加大 bitSet 的长度,这样不同的值出现“冲突”的概率就降低了,从而误判率也降低。
x提升 Hash 函数的个数,Hash 函数越多,每个值对应的 bit 越多,从而误判率也降低。
布隆过滤器的误判率还有专门的推导公式,有兴趣的可以去搜相关的文章和论文查看。
缓存击穿
对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把DB压垮。
解决方案:
1.使用互斥锁:当缓存失效时,不立即去load db,先使用如Redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
2.永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
1.使用互斥锁:当缓存失效时,不立即去load db,先使用如Redis的setnx去设置一个互斥锁,当操作成功返回时再进行load db的操作并回设缓存,否则重试get缓存的方法。
2.永远不过期:物理不过期,但逻辑过期(后台异步线程去刷新)。
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由
于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数
据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同
一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
1. 设置热点数据永远不过期。
2. 加互斥锁,互斥锁
于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数
据库压力瞬间增大,造成过大压力。和缓存雪崩不同的是,缓存击穿指并发查同
一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案
1. 设置热点数据永远不过期。
2. 加互斥锁,互斥锁
缓存击穿
描述:某一个热点 key,在缓存过期的一瞬间,同时有大量的请求打进来,由于此时缓存过期了,所以请求最终都
会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
解决方案:
1)加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁
就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
关于互斥锁的选择,网上看到的大部分文章都是选择 Redis 分布式锁(可以参考我之前的文章:面试必问的分布式
锁,你懂了吗?),因为这个可以保证只有一个请求会走到数据库,这是一种思路。
但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低
即可,所以还有另一个思路是 JVM 锁。
JVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性
能上比分布式锁更好。
需要注意的是,无论是使用“分布式锁”,还是“JVM 锁”,加锁时要按 key 维度去加锁。
我看网上很多文章都是使用一个“固定的 key”加锁,这样会导致不同的 key 之间也会互相阻塞,造成性能严重损耗。
使用 redis 分布式锁的伪代码,仅供参考:
会走到数据库,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。
解决方案:
1)加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁
就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。
关于互斥锁的选择,网上看到的大部分文章都是选择 Redis 分布式锁(可以参考我之前的文章:面试必问的分布式
锁,你懂了吗?),因为这个可以保证只有一个请求会走到数据库,这是一种思路。
但是其实仔细想想的话,这边其实没有必要保证只有一个请求走到数据库,只要保证走到数据库的请求能大大降低
即可,所以还有另一个思路是 JVM 锁。
JVM 锁保证了在单台服务器上只有一个请求走到数据库,通常来说已经足够保证数据库的压力大大降低,同时在性
能上比分布式锁更好。
需要注意的是,无论是使用“分布式锁”,还是“JVM 锁”,加锁时要按 key 维度去加锁。
我看网上很多文章都是使用一个“固定的 key”加锁,这样会导致不同的 key 之间也会互相阻塞,造成性能严重损耗。
使用 redis 分布式锁的伪代码,仅供参考:
2)热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。
这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还
有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。
这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还
有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。
缓存雪崩
设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。与缓存击穿的区别:雪崩是很多key,击穿是某一个key缓存。
解决方案:
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
将缓存失效时间分散开,比如可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
缓存雪崩是指缓存同一时间大面积的失效,所以,后面的请求都会落到数据库
上,造成数据库短时间内承受大量请求而崩掉。
解决方案
1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓
存标记失效,则更新数据缓存。
上,造成数据库短时间内承受大量请求而崩掉。
解决方案
1. 缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2. 一般并发量不是特别多的时候,使用最多的解决方案是加锁排队。
3. 给每一个缓存数据增加相应的缓存标记,记录缓存的是否失效,如果缓
存标记失效,则更新数据缓存。
描述:大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力
骤增,引起雪崩,甚至导致数据库被打挂。
缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。
解决方案:
1)过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加
上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
2)热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
3)加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原
地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
骤增,引起雪崩,甚至导致数据库被打挂。
缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。
解决方案:
1)过期时间打散。既然是大量缓存集中失效,那最容易想到就是让他们不集中生效。可以给缓存的过期时间时加
上一个随机值时间,使得每个 key 的过期时间分布开来,不会集中在同一时刻失效。
2)热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
3)加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原
地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
缓存预热
缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!
用户直接查询事先被预热的缓存数据!
解决方案
1. 直接写个缓存刷新页面,上线时手工操作一下;
2. 数据量不大,可以在项目启动的时候自动进行加载;
3. 定时刷新缓存;
这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!
用户直接查询事先被预热的缓存数据!
解决方案
1. 直接写个缓存刷新页面,上线时手工操作一下;
2. 数据量不大,可以在项目启动的时候自动进行加载;
3. 定时刷新缓存;
缓存降级
当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
缓存降级的最终目的是保证核心服务可用,即使是有损的。而且有些服务是无法降级的(如加入购物车、结算)。
在进行降级之前要对系统进行梳理,看看系统是不是可以丢卒保帅;从而梳理出哪些必须誓死保护,哪些可降级;比如可以参考日志级别设置预案:
1. 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
2. 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
3. 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
4. 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。
热点数据和冷数据
热点数据,缓存才有价值
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
对于冷数据而言,大部分数据可能还没有再次访问到就已经被挤出内存,不仅占用内存,而且价值不大。频繁修改的数据,看情况考虑使用缓存
对于热点数据,比如我们的某IM产品,生日祝福模块,当天的寿星列表,缓存以后可能读取数十万次。再举个例子,某导航产品,我们将导航信息,缓存以后可能读取数百万次。数据更新前至少读取两次,缓存才有意义。这个是最基本的策略,如果缓存还没有起作用就失效了,那就没有太大价值了。
那存不存在,修改频率很高,但是又不得不考虑缓存的场景呢?有!比如,这个读取接口对数据库的压力很大,但是又是热点数据,这个时候就需要考虑通过缓存手段,减少数据库的压力,比如我们的某助手产品的,点赞数,收藏数,分享数等是非常典型的热点数据,但是又不断变化,此时就需要将数据同步保存到Redis缓存,减少数据库压力。
缓存热点key
缓存中的一个Key(比如一个促销商品),在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
解决方案
对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询
解决方案
对缓存查询加锁,如果KEY不存在,就加锁,然后查DB入缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回数据或者进入DB查询
如何保证数据库和缓存的数据一致性
在上文的案例中,无论是先操作数据库,还是先操作缓存,都会存在脏数据的情况,有办法避免吗?
答案是有的,由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决
常见的有:2PC、TCC、MQ 事务消息等。
但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。
所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性。
如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。
保证数据库和缓存数据最终一致性的常用方案如下:
1)更新数据库,数据库产生 binlog。
2)监听和消费 binlog,执行失效缓存操作。
3)如果步骤 2 失效缓存失败,则引入重试机制,将失败的数据通过 MQ 方式进行重试,同时考虑是否需要引入幂等机制。
答案是有的,由于数据库和缓存是两个不同的数据源,要保证其数据一致性,其实就是典型的分布式事务场景,可以引入分布式事务来解决
常见的有:2PC、TCC、MQ 事务消息等。
但是引入分布式事务必然会带来性能上的影响,这与我们当初引入缓存来提升性能的目的是相违背的。
所以在实际使用中,通常不会去保证缓存和数据库的强一致性,而是做出一定的牺牲,保证两者数据的最终一致性。
如果是实在无法接受脏数据的场景,则比较合理的方式是放弃使用缓存,直接走数据库。
保证数据库和缓存数据最终一致性的常用方案如下:
1)更新数据库,数据库产生 binlog。
2)监听和消费 binlog,执行失效缓存操作。
3)如果步骤 2 失效缓存失败,则引入重试机制,将失败的数据通过 MQ 方式进行重试,同时考虑是否需要引入幂等机制。
子主题
兜底:当出现未知的问题时,及时告警通知,人为介入处理。
人为介入是终极大法,那些外表看着光鲜艳丽的应用,其背后大多有一群苦逼的程序员,在不断的修复各种脏数据和 bug。
人为介入是终极大法,那些外表看着光鲜艳丽的应用,其背后大多有一群苦逼的程序员,在不断的修复各种脏数据和 bug。
使用缓存时,先操作数据库 or 先操作缓存
1)先操作数据库
案例如下,有两个并发的请求,一个写请求,一个读请求,流程如下:
子主题
可能存在的脏数据时间范围:更新数据库后,失效缓存前。
这个时间范围很小,通常不会超过几毫秒。
2)先操作缓存
案例如下,有两个并发的请求,一个写请求,一个读请求,流程如下:
子主题
可能存在的脏数据时间范围:更新数据库后,下一次对该数据的更新前。这个时间范围不确定性很大,情况如下:
但是相比之下,先操作数据库,再操作缓存是更优的方式,即使在并发极端情况下,也只会出现很小量的脏数据。
- 如果下一次对该数据的更新马上就到来,那么会失效缓存,脏数据的时间就很短。
- 如果下一次对该数据的更新要很久才到来,那这期间缓存保存的一直是脏数据,时间范围很长。
但是相比之下,先操作数据库,再操作缓存是更优的方式,即使在并发极端情况下,也只会出现很小量的脏数据。
为什么是让缓存失效,而不是更新缓存?
1)更新缓存
案例如下,有两个并发的写请求,流程如下:
子主题
分析:数据库中的数据是请求 B 的,缓存中的数据是请求 A 的,数据库和缓存存在数据不一致。
2)失效(删除)缓存
案例如下,有两个并发的写请求,流程如下:
子主题
分析:由于是删除缓存,所以不存在数据不一致的情况。
结论:通过上述案例,可以很明显的看出,失效缓存是更优的方式。
缓存与数据库不一致怎么办
假设采用的主存分离,读写分离的数据库,
如果一个线程A先删除缓存数据,然后将数据写入到主库当中,这个时候,主库和从库同步没有完成,线程B从缓存当中读取数据失败,从从库当中读取到旧数据,然后更新至缓存,这个时候,缓存当中的就是旧的数据。
发生上述不一致的原因在于,主从库数据不一致问题,加入了缓存之后,主从不一致的时间被拉长了
处理思路:在从库有数据更新之后,将缓存当中的数据也同时进行更新,即当从库发生了数据更新之后,向缓存发出删除,淘汰这段时间写入的旧数据。
主从数据库不一致如何解决
场景描述,对于主从库,读写分离,如果主从库更新同步有时差,就会导致主从库数据的不一致
1、忽略这个数据不一致,在数据一致性要求不高的业务下,未必需要时时一致性
2、强制读主库,使用一个高可用的主库,数据库读写都在主库,添加一个缓存,提升数据读取的性能。
3、选择性读主库,添加一个缓存,用来记录必须读主库的数据,将哪个库,哪个表,哪个主键,作为缓存的key,设置缓存失效的时间为主从库同步的时间,如果缓存当中有这个数据,直接读取主库,如果缓存当中没有这个主键,就到对应的从库中读取。
假设采用的主存分离,读写分离的数据库,
如果一个线程A先删除缓存数据,然后将数据写入到主库当中,这个时候,主库和从库同步没有完成,线程B从缓存当中读取数据失败,从从库当中读取到旧数据,然后更新至缓存,这个时候,缓存当中的就是旧的数据。
发生上述不一致的原因在于,主从库数据不一致问题,加入了缓存之后,主从不一致的时间被拉长了
处理思路:在从库有数据更新之后,将缓存当中的数据也同时进行更新,即当从库发生了数据更新之后,向缓存发出删除,淘汰这段时间写入的旧数据。
主从数据库不一致如何解决
场景描述,对于主从库,读写分离,如果主从库更新同步有时差,就会导致主从库数据的不一致
1、忽略这个数据不一致,在数据一致性要求不高的业务下,未必需要时时一致性
2、强制读主库,使用一个高可用的主库,数据库读写都在主库,添加一个缓存,提升数据读取的性能。
3、选择性读主库,添加一个缓存,用来记录必须读主库的数据,将哪个库,哪个表,哪个主键,作为缓存的key,设置缓存失效的时间为主从库同步的时间,如果缓存当中有这个数据,直接读取主库,如果缓存当中没有这个主键,就到对应的从库中读取。
如何保证缓存与数据库双写时的数据一致性?
你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。还有一种方式就是可能会暂时产生不一致的情况,但是发生的几率特别小,就是先更新数据库,然后再删除缓存。
问题场景描述解决
先写缓存,再写数据库,缓存写成功,数据库写失败
缓存写成功,但写数据库失败或者响应延迟,则下次读取(并发读)缓存时,就出现脏读
这个写缓存的方式,本身就是错误的,需要改为先写数据库,把旧缓存置为失效;
读取数据的时候,如果缓存不存在,则读取数据库再写缓存
读取数据的时候,如果缓存不存在,则读取数据库再写缓存
先写数据库,再写缓存,数据库写成功,缓存写失败
写数据库成功,但写缓存失败,则下次读取(并发读)缓存时,则读不到数据
缓存使用时,假如读缓存失败,先读数据库,再回写缓存的方式实现
需要缓存异步刷新
指数据库操作和写缓存不在一个操作步骤中,
比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候
比如在分布式场景下,无法做到同时写缓存或需要异步刷新(补救措施)时候
确定哪些数据适合此类场景,根据经验值确定合理的数据不一致时间,用户数据刷新的时间间隔
其他问题
Redis 中的管道有什么用?
一次请求/响应服务器能实现处理新的请求即使旧的请求还未被响应。
这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。
例如许多POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
这样就可以将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复。
这就是管道(pipelining),是一种几十年来广泛使用的技术。
例如许多POP3 协议已经实现支持这个功能,大大加快了从服务器下载新邮件的过程。
Redis 如何做大量数据插入?
Redis2.6 开始Redis-cli 支持一种新的被称之为pipe mode 的新模式用于执行大量数据插入工作。
0 条评论
下一页