Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
Redis 提供了多种数据类型来支持不同的业务场景,比如
String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流),
并且对数据类型的操作都是原子性的,因为执行命令由单线程负责的,不存在并发竞争的问题。
Redis 和 Memcached 有什么区别?
Redis 与 Memcached 共同点:
都是基于内存的数据库,一般都用来当做缓存使用。
都有过期策略。
两者的性能都非常高。
·
Redis 与 Memcached 区别:
Redis 支持的数据类型更丰富(String、Hash、List、Set、ZSet),而 Memcached 只支持最简单的 key-value 数据类型;
Redis 支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而 Memcached 没有持久化功能,数据全部存在内存之中,Memcached 重启或者挂掉后,数据就没了;
Redis 原生支持集群模式,Memcached 没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;
Redis 支持发布订阅模型、Lua 脚本、事务等功能,而 Memcached 不支持
为什么用 Redis 作为 MySQL 的缓存?
Redis 具备「高性能」和「高并发」两种特性。
操作 Redis 缓存就是直接操作内存,速度相当快;直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的
常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
随着Redis更新,又支持了四种数据类型:BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。
Redis 数据类型以及使用场景分别是什么?
常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
五种常见数据类型的实现
1、String类型底层数据结构:int 和 SDS(简单动态字符串)
· SDS 不仅可以保存文本数据,还可以保存二进制数据。
SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。
· SDS 获取字符串长度的时间复杂度是 O(1)
SDS 结构里用 len 属性记录了字符串长度
· Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出
SDS 在拼接字符串之前会检查 SDS 空间是否满足要求,如果空间不够会自动扩容
2、List类型底层数据结构:双向链表或压缩列表
· 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),会使用压缩列表;否则使用双向链表
在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
3、Hash类型底层数据结构:压缩列表或哈希表
· 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,满足条件使用压缩列表;否则使用哈希表
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
4、Set类型底层数据结构:哈希表或整数集合
· 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合;否则使用哈希表
5、ZSet类型底层数据结构:压缩列表或跳表
· 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,使用压缩列表;否则使用跳表
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发送数据给客户端」这个过程是由一个线程(主线程)来完成的
但Redis 程序并不是单线程的,Redis 在启动的时候,是会启动后台线程(BIO)的:
· Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
· Redis 在 4.0 版本之后,新增了lazyfree 线程(一个新的后台线程),用来异步释放 Redis 内存。
Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,通过把任务丢入任务队列,后台线程(BIO)轮询队列,取出任务去执行
图中的蓝色部分是一个事件循环,是由主线程负责的,可以看到网络 I/O 和命令处理都是单线程。
Redis初始化:
· 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 创建一个服务端 socket
· 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
· 然后,将调用 epoll_ctl() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。
初始化完成后,主线程进入事件循环函数:
· 首先,先调用处理发送队列函数,看发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
· 接着,调用 epoll_wait 函数等待事件的到来:
· 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctl 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
· 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
· 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发送完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
Redis 采用单线程(网络 I/O 和执行命令)那么快,有如下几个原因:
· Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构
· Redis 采用单线程模型可以避免了多线程之间的竞争
· Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求
Redis6.0之后,采用了多个I/O线程来处理网络请求
原因:
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络I/O的处理上
通过将 Redis.conf 配置文件中的 io-threads-do-reads 配置项设为 yes。可开启多线程处理客户端读请求
Redis.conf 配置文件提供了 IO 多线程个数的配置项
三种持久化方式:
· AOF 日志:每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里;
· RDB 快照:将某一时刻的内存数据,以二进制的方式写入磁盘;
· 混合持久化方式:Redis 4.0 新增的方式,集成了 AOF 和 RBD 的优点;
「*3」表示当前命令有三个部分,每部分都是以「$+数字」开头,后面紧跟着具体的命令、键或值。然后,这里的「数字」表示这部分中的命令、键或值一共有多少字节。例如,「$3 set」表示这部分有 3 个字节,也就是「set」命令这个字符串的长度。
先执行写操作命令,再将该命令记录到AOF日志里
好处:
· 避免额外的检查开销
· 不会阻塞当前写操作命令的执行
坏处:
· 数据可能会丢失
· 可能阻塞其他操作
Redis写入AOF日志过程:
Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;由内核决定什么时候写入到硬盘。
当AOF文件内容过大(超过阈值)时,触发AOF重写机制
在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。
重写AOF日志过程:
由后台子进程 bgrewriteaof 来完成的。子进程进行AOF重写期间,主进程可继续执行命令,父子进程是共享内存数据(只读),任意一方修改共享内存,触发写时复制,父子进程具有独立的数据副本。
为了解决重写过程中,主进程修改了已存在的键值对而造成的数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
子进程在追加 执行后的写命令,除了AOF缓冲区外还有AOF重写缓冲区;在子进程完成重写工作后,会向主进程发送一个信号,主进程在收到信号后调用一个信号处理函数,主要工作:
· 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
· 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。
由于AOF日志记录的是操作命令,如果在日志非常多的情况下做故障恢复,会造成Redis恢复操作缓慢。
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据。恢复数据效率要高
Redis通过两个命令来生成RDB文件(save和bgsave),区别在于是否在主进程里执行
· 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
· 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB快照为全量快照,即每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。执行 bgsave 过程中,Redis 依然可以继续处理操作命令的,依然用到写时复制技术
执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的(复制父进程的页表,但指向的物理内存为同一个)
此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。(发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是没办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照)
在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程(父进程)的内存数据和子进程的内存数据已经分离了,子进程写入到 RDB 文件的内存数据只能是原本的内存数据。
Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
AOF日志重写过程:
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
即 AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据。
重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
优点:
· 开头为 RDB 的格式,使得 Redis 可以更快的启动,同时结合 AOF 的优点,有减低了大量数据丢失的风险。
缺点:
· AOF 文件中添加了 RDB 格式的内容,使得 AOF 文件的可读性变得很差;
· 兼容性差,如果开启混合持久化,那么此混合持久化 AOF 文件,就不能用在 Redis 4.0 之前版本了
主从复制是高可用的保证,实现一主多从模式,主从服务器之间采用读写分离
主服务器可进行读写操作,写操作会同步给从服务器,从服务器一般是只读
主从服务器之间的命令复制是异步的,无法实现强一致性保证(主从数据时时刻刻保持一致)。
为解决 Redis 的主从服务器出现故障宕机的问题需要手动恢复。
可以监控主从服务器,并且提供主从节点故障转移的功能。
哨兵会每隔 1 秒给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。这个「规定的时间」是配置项 down-after-milliseconds 参数设定的,单位是毫秒。
主观下线、客观下线
客观下线只适用于主节点
之所以针对「主节点」设计「主观下线」和「客观下线」两个状态,是因为有可能「主节点」其实并没有故障,可能只是因为主节点的系统压力比较大或者网络发送了拥塞,导致主节点没有在规定时间内响应哨兵的 PING 命令。
用多个节点部署成哨兵集群(最少需要三台机器来部署哨兵集群),通过多个哨兵节点一起判断,就可以就可以避免单个哨兵因为自身网络状况不好,而误判主节点下线的情况。
当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
当主节点被判定为客观下线后,需要进行主从故障转移,从哨兵集群中选举出一个leader,让leader执行主从切换
哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者,所谓的候选者就是想当 Leader 的哨兵。
每个哨兵只有一次投票机会,如果用完后就不能参与投票了,可以投给自己或投给别人,但是只有候选者才能把票投给自己。在投票过程中,任何一个「候选者」,要满足两个条件:
· 第一,拿到半数以上的赞成票;
· 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。
quorum 的值建议设置为哨兵个数的二分之一加1,例如 3 个哨兵就设置 2,5 个哨兵设置为 3,而且哨兵节点的数量应该是奇数。
· 第一步:在已下线主节点(旧主节点)属下的所有「从节点」里面,挑选出一个从节点,并将其转换为主节点。
· 第二步:让已下线主节点属下的所有「从节点」修改复制目标,修改为复制「新主节点」;
· 第三步:将新主节点的 IP 地址和信息,通过「发布者/订阅者机制」通知给客户端;
· 第四步:继续监视旧主节点,当这个旧主节点重新上线时,将它设置为新主节点的从节点;
第一步:
首先,通过 down-after-milliseconds * 10 配置项(down-after-milliseconds 是主从节点断连的最大连接超时时间)过滤从节点
在down-after-milliseconds 毫秒内,主从节点无法通过网络联系上,且发生断连次数超过10次,则证明该节点网络不好,不适合作为新的主节点
其次,对所有从节点进行三轮考察:优先级、复制进度、ID 号。在进行每一轮考察的时候,哪个从节点优先胜出,就选择其作为新主节点。
· 第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前,
· 第二轮考察:如果优先级相同,则查看复制的下标,哪个从「主节点」接收的复制数据多,哪个就靠前。
· 第三轮考察:如果优先级和下标都相同,就选择从节点 ID 较小的那个。
最后选出从节点后,哨兵 leader 向被选中的从节点发送 SLAVEOF no one 命令,让这个从节点解除从节点的身份,将其变为新主节点。
至此,新主节点选举出来
第二步:
哨兵 leader 下一步要做的就是,让已下线主节点属下的所有「从节点」指向「新主节点」,这一动作可以通过向「从节点」发送 SLAVEOF 命令来实现。命令包含有新主节点的ip及端口号
哨兵集群终于完成主从切换的工作
第三步:
通知给客户端,通过 Redis 的发布者/订阅者机制来实现,每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。
哨兵提供的消息订阅频道有很多,不同频道包含了主从节点切换过程中的不同关键事件,几个常见的事件如下:
主从切换完成后,哨兵就会向 +switch-master 频道发布新主节点的 IP 地址和端口的消息,这个时候客户端就可以收到这条信息,然后用这里面的新主节点的 IP 地址和端口进行通信了。
第四步:
继续监视旧主节点,当旧主节点重新上线时,哨兵集群就会向它发送 SLAVEOF 命令,让它成为新主节点的从节点
哨兵集群如何组成?
哨兵节点之间是通过 Redis 的发布者/订阅者机制来相互发现的。
在主从集群中,主节点上有一个名为__sentinel__:hello的频道,不同哨兵就是通过它来相互发现,实现互相通信的。
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster )方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。有两种方案将哈希槽映射到具体的 Redis 节点上:
· 平均分配: 在使用 cluster create 命令创建 Redis 集群时,Redis 会自动把所有哈希槽平均分布到集群节点上。比如集群中有 9 个节点,则每个节点上槽的个数为 16384/9 个。
· 手动分配: 可以使用 cluster meet 命令手动建立节点间的连接,组成集群,再使用 cluster addslots 命令,指定每个节点上的哈希槽个数。
脑裂现象
由于网络问题,集群节点之间失去联系。主从数据不同步;重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于会从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
解决方案:当主节点发现从节点下线或者通信超时的总数量小于阈值时,那么禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
每当对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中。即,「过期字典」保存了数据库中所有 key 的过期时间。
当查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
· 如果不在,则正常读取键值;
· 如果存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。
过期删除策略:「惰性删除+定期删除」
不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
优缺点:
优点:因为每次访问时,才会检查 key 是否过期,所以此策略只会使用很少的系统资源,因此,惰性删除策略对 CPU 时间最友好。
缺点:如果一个 key 已经过期,而这个 key 又仍然保留在数据库中,那么只要这个过期 key 一直没有被访问,它所占用的内存就不会释放,造成了一定的内存空间浪费。所以,惰性删除策略对内存不友好。
每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
Redis 的定期删除的流程:
· 从过期字典中随机抽取 20 个 key;
· 检查这 20 个 key 是否过期,并删除已过期的 key;
· 当「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%(超过 5 个(20/4)),则继续重复步骤 1;否则,则停止继续删除过期 key,然后等待下一轮再检查。
优缺点:
优点:通过限制删除操作执行的时长和频率,来减少删除操作对 CPU 的影响,同时也能删除一部分过期的数据减少了过期键对空间的无效占用。
缺点:难以确定删除操作执行的时长和频率。如果执行的太频繁,就会对 CPU 不友好;如果执行的太少,那又和惰性删除一样了,过期 key 占用的内存不会及时得到释放。
RDB 文件生成阶段和加载阶段:
RDB 文件生成阶段:从内存状态持久化成 RDB(文件)的时候,会对 key 进行过期检查,过期的键「不会」被保存到新的 RDB 文件中
RDB 加载阶段:RDB 加载阶段时,要看服务器是主服务器还是从服务器,分别对应以下两种情况:
「主服务器」运行模式:在载入 RDB 文件时,程序会对文件中保存的键进行检查,过期键「不会」被载入到数据库中
「从服务器」运行模式:在载入 RDB 文件时,不论键是否过期都会被载入到数据库中。但由于主从服务器在进行数据同步时,从服务器的数据会被清空。
AOF 文件写入阶段和 AOF 重写阶段:
AOF 文件写入阶段:如果数据库某个过期键还没被删除,那么 AOF 文件会保留此过期键,当此过期键被删除后,Redis 会向 AOF 文件追加一条 DEL 命令来显式地删除该键值。
AOF 重写阶段:执行 AOF 重写时,会对 Redis 中的键值对进行检查,已过期的键不会被保存到重写后的 AOF 文件中
从库不会进行过期扫描(从库对过期处理是被动的,主库在key到期时,会在AOF文件中追加一条del指令,同步给所有从库,从库再执行指令删除)
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制(阈值通过配置项为 maxmemory设置)
内存淘汰机制策略:
不进行数据淘汰的策略:
noeviction(Redis3.0之后,默认的内存淘汰策略) :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
进行数据淘汰的策略:
在设置了过期时间的数据中进行淘汰:
· volatile-random:随机淘汰设置了过期时间的任意键值;
· volatile-ttl:优先淘汰更早过期的键值。
· volatile-lru(Redis3.0 之前,默认的内存淘汰策略):淘汰所有设置了过期时间的键值中,最久未使用的键值;
· volatile-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰所有设置了过期时间的键值中,最少使用的键值;
在所有数据范围内进行淘汰:
· allkeys-random:随机淘汰任意键值;
· allkeys-lru:淘汰整个键值中最久未使用的键值;
· allkeys-lfu(Redis 4.0 后新增的内存淘汰策略):淘汰整个键值中最少使用的键值。
LRU 全称是 Least Recently Used 翻译为最近最少使用,会选择淘汰最近最少使用的数据。
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
Redis实现LRU算法:
实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
但无法解决缓存污染问题
比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
在 Redis 4.0 之后引入了 LFU 算法来解决这个问题
LFU 全称是 Least Frequently Used 翻译为最近最不常用的,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
Redis 对象的结构如下:
Redis 对象头中的 lru 字段,在 LRU 算法下和 LFU 算法下使用方式并不相同。
在 LRU 算法中,Redis 对象头的 24 bits 的 lru 字段是用来记录 key 的访问时间戳,因此在 LRU 模式下,Redis可以根据对象头中的 lru 字段记录的值,来比较最后一次 key 的访问时间长,从而淘汰最久未被使用的 key。
在 LFU 算法中,Redis对象头的 24 bits 的 lru 字段被分成两段来存储,高 16bit 存储 ldt(Last Decrement Time),用来记录 key 的访问时间戳;低 8bit 存储 logc(Logistic Counter),用来记录 key 的访问频次。
Redis实现LFU算法:据访问频率来淘汰数据的,而不只是访问次数。
Redis 在访问 key 时,对于 logc 是这样变化的:
先按照上次访问距离当前的时长,来对 logc 进行衰减;
然后,再按照一定概率增加 logc 的值
redis.conf 提供了两个配置项,用于调整 LFU 算法从而控制 logc 的增长和衰减:
当大量缓存数据在同一时间过期(失效)时,如果此时有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃
解决方案
业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮
解决方案
当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增
出现该问题一般有两种情况:
· 业务误操作,缓存中的数据和数据库中的数据都被误删除了,所以导致缓存和数据库中都没有数据;
· 黑客恶意攻击,故意大量访问某些读取不存在数据的业务;
解决方案
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
常见的缓存更新策略共有3种:
Cache Aside(旁路缓存)策略
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
读策略的步骤:
Cache Aside 策略适合读多写少的场景,不适合写多的场景,对缓存命中率有影响,可以考虑两种解决方案:
Read/Write Through(读穿 / 写穿)策略
应用程序只和缓存交互,不再和数据库交互,而是由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
Read Through 策略
Write Through 策略
当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:
注意
无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能。在使用本地缓存的时候可以考虑使用这种策略。
Write Back(写回)策略
在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。
Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。
Write Back 策略特别适合写多的场景,但数据不是强一致性的,而且会有数据丢失的风险。
无论是「先更新数据库,再更新缓存」,还是「先更新缓存,再更新数据库」,这两个方案都存在并发问题,当两个请求并发更新同一条数据的时候,可能会出现缓存和数据库中的数据不一致的现象。
「先更新数据库 + 再删除缓存」的方案,是可以保证数据一致性的。
但出现数据库、缓存不一致的原因在于,不管是先操作数据库,还是先操作缓存,只要第二个操作失败都会出现数据一致的问题。
解决方案
1、重试机制
引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
· 如果应用删除缓存失败,可以从消息队列中重新读取数据,然后再次删除缓存,这个就是重试机制。当然,如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。
· 如果删除缓存成功,就要把数据从消息队列中移除,避免重复操作,否则就继续重试。
2、订阅 MySQL binlog,再操作缓存。
更新数据库成功,就会产生一条变更日志,记录在 binlog 里。通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除
两种方式均采用异步操作缓存
使用**有序集合(ZSet)**的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
使用 zadd score1 value1 命令就可以一直往内存中生产消息。再利用 zrangebysocre 查询符合条件的所有待处理的任务, 通过循环执行队列任务即可。
大 key 并不是指 key 的值很大,而是 key 对应的 value 很大。一般而言,下面这两种情况被称为大 key:
影响持久化:
1、大key对AOF日志的影响
三种AOF日志写回硬盘的策略只是在控制 fsync() 函数的调用时机。
当使用 Always 策略的时候,如果写入是一个大 Key,主线程在执行 fsync() 函数的时候,阻塞的时间会比较久,
当使用 Everysec 策略的时候,由于是异步执行 fsync() 函数,所以大 Key 持久化的过程(数据同步磁盘)不会影响主线程。
当使用 No 策略的时候,由于永不执行 fsync() 函数,所以大 Key 持久化的过程不会影响主线程。
2、大 Key 对 AOF 重写和 RDB 的影响
AOF 重写机制和 RDB 快照(bgsave 命令)的过程,都会分别通过 fork() 函数创建一个子进程来处理任务。
有两个阶段会导致阻塞父进程:
创建子进程的途中,由于要复制父进程的页表等数据结构,阻塞的时间跟页表的大小有关,页表越大,阻塞的时间也越长;
创建完子进程后,如果子进程或者父进程修改了共享数据,就会发生写时复制,这期间会拷贝物理内存,如果内存越大,自然阻塞的时间也越长;
当页表过大时,执行fork函数会发生阻塞;可以执行 info 命令获取到 latest_fork_usec 指标,表示 Redis 最近一次 fork 操作耗时。
如果 fork 耗时很大,比如超过1秒,则需要做出优化调整:
单个实例的内存占用控制在 10 GB 以下,这样 fork 函数就能很快返回。
如果 Redis 只是当作纯缓存使用,不关心 Redis 数据安全性问题,可以考虑关闭 AOF 和 AOF 重写,这样就不会调用 fork 函数了。
在主从架构中,要适当调大 repl-backlog-size,避免因为 repl_backlog_buffer 不够大,导致主节点频繁地使用全量同步的方式,全量同步的时候,是会创建 RDB 文件的,也就是会调用 fork 函数。
其它影响:
如何删除大key?
管道技术(Pipeline)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
使用管道技术可以解决多个命令执行时的网络等待
管道技术本质上是客户端提供的功能,而非 Redis 服务器端的功能。
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。不支持事务回滚指的是不支持事务运行时错误的事务回滚。
DISCARD 命令用法:
事务执行过程中,如果命令入队时没报错,而事务提交后,实际执行时报错了,正确的命令依然可以正常执行,所以这可以看出 Redis 并不一定保证原子性
作者不支持事务回滚的原因有以下两个:
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
· 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
· 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
加锁操作
需要满足三个条件。
· lock_key 就是 key 键;
· unique_value 是客户端生成的唯一的标识,区分来自不同客户端的锁操作;
· NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
· PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
解锁操作
解锁的时候,先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
优缺点:
基于 Redis 实现分布式锁的优点:
1、性能高效(这是选择缓存实现分布式锁最核心的出发点)。
2、实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
3、避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
基于 Redis 实现分布式锁的缺点:
1、超时时间不好设置。
2、Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。
如何解决集群情况下分布式锁的可靠性?
Redis 官方已经设计了一个分布式锁算法 Redlock(红锁)。
它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。
Redlock 算法的基本思路:是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
Redlock 算法加锁三个过程:
第一步是,客户端获取当前时间(t1)。
第二步是,客户端按顺序依次向 N 个 Redis 节点执行加锁操作:
第三步是,一旦客户端从超过半数(大于等于 N/2+1)的 Redis 节点上成功获取到了锁,就再次获取当前时间(t2),然后计算计算整个加锁过程的总耗时(t2-t1)。如果 t2-t1 < 锁的过期时间,此时,认为客户端加锁成功,否则认为加锁失败。
加锁成功后,客户端需要重新计算这把锁的有效时间,计算的结果是「锁最初设置的过期时间」减去「客户端从大多数节点获取锁的总耗时(t2-t1)」。如果计算的结果已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。
加锁失败后,客户端向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。