Redis 是一个由 C 语言开发并且基于内存的键值型数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。
有以下几个特征:
因为 Redis 内部做了非常多的性能优化,比如:
主要有三种。
因为 Tendis 出现的时间较短,相对于其他成熟的分布式 KV 存储系统,Tendis 的用户基数和社区规模相对较小,因此目前比较少公司使用。此外,Tendis 的文档和资料相对较少,不太方便开发人员学习和使用。
先讲下两者的共同点:
再分析一下两者的区别:
主要是因为 Redis 具备 【高性能】 和 【高并发】 两种特性。
1、Redis 具备高性能
例如用户第一次访问 MySQL 中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。而将该用户访问的数据缓存在 Redis 中,下一次再访问这些数据的时候就可以直接从缓存中获取了(缓存命中),操作 Redis 缓存就是直接操作内存,速度会非常快。
如果 MySQL 中的对应数据改变的之后,同步改变 Redis 缓存中相应的数据即可,不过这里会有 Redis 和 MySQL 双写一致性的问题。
2、Redis 具备高并发
单台设备的 Redis 的 QPS(Query Per Second,每秒钟处理完请求的次数) 是 MySQL 的 10 倍,Redis 单机的 QPS 能轻松破 10w,而 MySQL 单机的 QPS 很难破 1w。
所以,直接访问 Redis 能够承受的请求是远远大于直接访问 MySQL 的,可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
常见的有五种数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
后续随着版本的更新,又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。
分别对应的应用场景是:
String 对应:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 对应:消息队列
但是有两个问题:生产者需要自行实现全局唯一 ID,不能以消费组形式消费数据。
Hash 对应:缓存对象、购物车等。
Set 对应:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 对应:排序场景,比如排行榜、电话和姓名排序等。
BitMap(位图)对应:二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
HyperLogLog(基数统计)对应:海量数据基数统计的场景,比如百万级网页 UV 计数等;
GEO(地理信息)对应:存储地理位置信息的场景,比如滴滴打车;
Stream(流)对应:消息队列,
相比于基于 List 类型实现的消息队列,
有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
String 类型的底层的数据结构实现主要是 SDS(简单动态字符串)。之所以没有使用 C 语言的字符串表示,是因为 SDS 相比于 C 的原生字符串:
SDS 不仅可以保存文本数据,还可以保存二进制数据。
这是因为 SDS 使用 len 属性的值而不是空字符来判断字符串是否结束,
并且 SDS 的所有 API 都会以处理二进制的方式来处理 SDS 存放在 buf[] 数组里的数据。
所以 SDS 不光能存放文本数据,而且能保存图片、音频、视频、压缩文件这样的二进制数据。
SDS 获取字符串长度的时间复杂度是 O(1)。
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出。
3.2 之前,List 类型的底层数据结构是由双向链表或压缩列表实现的:
如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节,Redis 会使用压缩列表作为 List 类型的底层数据结构;
上面的 512,64 都是默认值,可由 list-max-ziplist-entries 配置
其他情况的话,Redis 会使用双向链表作为 List 类型的底层数据结构;
注意:但是在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。
Hash 类型的底层数据结构是由压缩列表或哈希表实现的:
注意:在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
Set 类型的底层数据结构是由整数集合或哈希表实现的:
在 7.0 之前,Zset 类型的底层数据结构是由压缩列表或跳表实现的:
注意:在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
是。但是,Redis 程序不是单线程的,因为 Redis 在启动的时候。会启动后台线程(BIO):
Redis 在 2.6 版本,会启动 2 个后台线程,分别处理关闭文件、AOF 刷盘这两个任务;
在 4.0 版本之后,新增了一个新的后台线程,用来异步释放 Redis 内存,也就是 lazyfree 线程。
例如执行 unlink key、 flushdb async、 flushall async 等命令,会把这些删除操作交给后台线程来执行,好处是不会导致 Redis 主线程卡顿。因此,当我们要删除一个大 key 的时候,不要使用 del 命令删除,因为 del 是在主线程处理的,这样会导致 Redis 主线程卡顿,因此我们应该使用 unlink 命令来异步删除大 key。
为什么 Redis 要为「关闭文件、AOF 刷盘、释放内存」这些任务启动后台线程?
是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。
Redis 单线程指的是:
【 接收客户端请求 -> 解析请求 -> 进行数据读写等操作 -> 发送数据给客户端 】
这个过程是由一个线程(主线程)来完成的,所以我们常说 Redis 是单线程的。
Redis 单线程模式是指:
Redis 服务器在运行时只使用一个线程来处理客户端的请求和所有的数据操作,这个线程同时也负责了网络 I/O、内存管理、磁盘同步等操作,因此 Redis 的性能非常高。在单线程模式下,Redis 使用了多路复用技术来实现非阻塞 I/O,从而避免了线程切换和线程同步所带来的性能消耗。此外,Redis 通过对数据的操作进行批量化和异步化来进一步提高性能。需要注意的是,虽然 Redis 是单线程模式,但是它可以通过多个进程或者多个实例的方式来进行横向扩展,从而提高整个系统的性能和可伸缩性。
主要有以下三个原因:
什么是客户端 Socket 请求?
客户端 Socket 请求指的是客户端通过建立 Socket 连接向服务器发送请求的过程。在网络编程中,Socket 是一种通信机制,它提供了一种通过网络进行进程间通信的方式。 在客户端Socket 请求中,客户端首先需要创建一个 Socket 对象,并指定要连接的服务器的IP地址和端口号。然后,客户端可以向服务器发送请求,请求可以是任何形式的数据,例如HTTP请求、FTP请求等。 客户端发送请求后,服务器收到请求后会进行处理,并返回响应给客户端。客户端接收到服务器返回的响应后,可以对响应进行处理,例如解析HTML页面、保存文件等。最后,客户端关闭Socket连接,释放资源。 客户端Socket请求是网络编程中非常常见的一种操作,它可以实现各种网络应用程序,例如网页浏览器、聊天工具、文件下载器等。
众所周知,Redis 的主要工作(网络 I/O 和执行命令)一直是单线程模型,而单线程的程序是无法利用服务器的多核 CPU 的,但是 CPU 并不是制约 Redis 性能表现的瓶颈所在,所以 Redis 核心网络模型使用单线程并没有什么问题,如果想要使用服务的多核 CPU,可以在一台服务器上启动多个节点或者采用分片集群的方式。
还有一点就是,使用单线程后,可维护性高,多线程模型虽然在某些方面表现优异,但是它却引入了程序执行顺序的不确定性,带来了并发读写的一系列问题,比如:增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。
这是因为在 Redis 6.0 版本之后,也采用了多个 I/O 线程来处理网络请求,这是因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上。
所以为了提高网络 I/O 的并行度,Redis 6.0 对于网络 I/O 采用多线程来处理。但是对于命令的执行,Redis 仍然使用单线程来处理,所以大家不要误解 Redis 有多线程同时执行命令。
简单来说就是为了提升性能,采用多线程来处理 网络 I/O
Redis 通过实现数据持久化的机制来保证数据不丢失,这个机制会把数据存储到磁盘,这样在 Redis 重启后就能够从磁盘中恢复原有的数据。
Redis 共有三种数据持久化的方式:
Redis 在执行完一条写操作命令后,就会把该命令以追加的方式写入到一个文件里,然后 Redis 重启时,会读取该文件记录的命令,然后逐一执行命令的方式来进行数据恢复。
简单来说就是:客服端 发送请求到 Redis 中,然后会记录命令到 AOF 文件中
流程如下图:
文件内容解释如下:
文件先是用 [*3] 表示当前命令有三个部分,每部分都是以 [$+数字] 开头,后面紧跟着具体的命令、键或值。然后,这里的 [数字] 表示这部分中的命令、键或值一共有多少字节。
例如,[$3 set] 表示这部分有 3 个字节,也就是 [set] 命令这个字符串的长度。
因为这么做有两个好处:
当然,这样做也会带来风险:
具体来说就是:
如图:
Redis 提供了 3 种写回硬盘的策略,控制的就是具体内核缓冲区的数据什么时候写入到硬盘的过程。
在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:
这 3 个写回策略的优缺点如下:
会触发 AOF 重写机制。
因为 AOF 日志是一个文件,随着执行的写操作命令越来越多,文件的大小也会越来越大,相应的也就会带来性能问题。比如重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。
所以为了避免 AOF 文件越写越大,Redis 提供了 AOF 重写机制,当 AOF 文件的大小超过所设定的阈值后,Redis 就会启用 AOF 重写机制,来压缩 AOF 文件。
AOF 重写机制是怎么实现的?
AOF 重写机制:是在重写时,读取当前数据库的所有键值对(即数据),然后将每一个键值对转换成一条命令记录到一个新的 AOF 文件,等到全部记录完后,就会将新的 AOF 文件替换掉现有的 AOF 文件。相当于去掉了历史命令,压缩 AOF 文件。
举个例子,在没有使用重写机制前,假设前后执行了 set name xiaolin
和 set name xiaolincoding
这两个命令的话,就会将这两个命令记录到 AOF 文件。但是在使用重写机制后,就会读取 name 最新的 value(键值对) ,然后用一条 set name xiaolincoding
命令记录到新的 AOF 文件,之前的第一个命令就没有必要记录了,因为它属于「历史」命令,没有作用了。这样一来,一个键值对在重写日志中只用一条命令就行了。
Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的,由子进程来完成有两个好处:
子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
子进程带有主进程的数据副本,这里使用子进程而不是线程,不会降低性能。
因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。
而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。
触发重写机制后,主进程就会创建重写 AOF 的子进程,此时父子进程共享物理内存,重写子进程只会对这个内存进行只读,重写 AOF 子进程会读取数据库里的所有数据,并逐一把内存数据的键值对转换成一条命令,再将命令记录到重写日志(新的 AOF 文件)。
如果主进程修改了已经存在键值对,会产生什么问题?
在重写过程中,主进程依然可以正常处理命令,那问题来了,重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,那么会发生写时复制,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?
为了解决这种数据不一致问题,Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。
在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」。
当子进程完成 AOF 重写工作后,会向主进程发送一条信号,主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:
信号函数执行完后,主进程就可以继续像往常一样处理命令了。
具体过程如下图:
RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
因为 AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢。
为了解决这个问题,Redis 增加了 RDB 快照。
在 Redis 中,RDB 和 AOF 都是持久化的方式,用于将内存中的数据保存到磁盘中,以便在 Redis 重启时能够重新加载数据。
综上所述,尽管 RDB 可以提供快速备份和恢复的功能,但 AOF 持久化方式可以提供更高级别的数据保护,因此在一些关键业务场景下(金融、电商),我们需要同时使用 RDB 和 AOF 两种持久化方式,以确保数据的完整性和可靠性。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsave 命令,默认会提供以下配置:
// 只要满足下面条件的任意一个,就会执行 bgsave
save 900 1 // 900 秒之内,对数据库进行了至少 1 次修改
save 300 10 // 300 秒之内,对数据库进行了至少 10 次修改
save 60 10000 // 60 秒之内,对数据库进行了至少 1万 次修改。
这里的选项名虽然叫 save,但实际上执行的是 bgsave 命令,也就是会创建子进程来生成 RDB 快照文件。
这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。所以执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
可以,在执行 bgsave 过程中,Redis 依然可以继续处理操作命令,也就是数据是能被修改的,关键的技术就在于写时复制技术。
什么是写时复制技术?
写时复制(Copy-On-Write,简称为COW)是一种常见的内存管理技术,也被广泛应用于数据库系统中,包括 Redis。
在 Redis 中,写时复制是指当父进程复制自己创建的子进程时,子进程与父进程共享相同的内存空间,只有在子进程需要修改某个内存页面时,才会将该页面复制一份到子进程的独立内存空间中,从而实现了父子进程之间的内存隔离,避免了频繁的内存复制,提高了内存的使用效率。Redis 使用写时复制来实现主从复制和 Sentinel 高可用性,以及 AOF 持久化中的 fork 操作。
因为在执行 bgsave 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个,此时如果主线程执行读操作,则主线程和 bgsave 子进程互相不影响。
如果主线程执行写操作,则被修改的数据会复制一份副本,然后 bgsave 子进程会把该副本数据写入 RDB 文件,在这个过程中,主线程仍然可以直接修改原来的数据。
是为了集成 AOF 和 RDB 的优点,Redis 4.0 提出了混合使用 AOF 日志和内存快照,也叫混合持久化,既保证了 Redis 重启速度,又降低数据丢失风险。
先分析下 AOF 和 RDB 的优缺点:
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,安全性高,但是数据恢复不快。
混合持久化工作在 AOF 日志重写过程。
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数。
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
简单来说:就是既可以加快加载速度,又可以减少数据的丢失。
优点:
缺点:
要想设计一个高可用的 Redis 服务,一定要从 Redis 的多服务节点来考虑,比如 Redis 的主从复制、哨兵模式、切片集群。
主从复制是 Redis 高可用服务的最基础的保证。
就是将从前的一台 Redis 服务器,同步数据到多台 Redis 从服务器上,即一主多从的模式,且主从服务器之间采用的是**「读写分离」**的方式。
【主服务器】可以进行读写操作,当发生写操作时自动将写操作同步给【从服务器】,而【从服务器】一般是只读,并接受【主服务器】同步过来写操作命令,然后执行这条命令。
也就是说,所有的数据修改只在主服务器上进行,然后将最新的数据同步给从服务器,这样就使得主从服务器的数据是一致的(但不是强一致性)。
注意,主从服务器之间的命令复制是异步进行的。
在主从服务器命令传播阶段,主服务器收到新的写命令后,会发送给从服务器。但是,主服务器并不会等到从服务器实际执行完命令后,再把结果返回给客户端,而是主服务器自己在本地执行完命令后,就会向客户端返回结果了。
如果从服务器还没有执行主服务器同步过来的命令,主从服务器间的数据就不一致了。
所以,无法实现强一致性保证(主从数据时时刻刻保持一致),数据不一致是难以避免的。
在使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复。
为了解决这个问题,Redis 增加了哨兵模式(Redis Sentinel),因为哨兵模式做到了可以监控主从服务器,并且提供主从节点故障转移的功能。
哨兵(Sentinel)机制是 Redis 在 2.8 版本以后提供的。
它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
哨兵一般是以集群的方式部署,至少需要 3 个哨兵节点,哨兵集群主要负责三件事情:监控、选主、通知。
哨兵节点通过 Redis 的发布者/订阅者机制,哨兵之间可以相互感知,相互连接,然后组成哨兵集群,同时哨兵又通过 INFO 命令,在主节点里获得了所有从节点连接信息,于是就能和从节点建立连接,并进行监控了。
1、第一轮投票:判断主节点下线
当哨兵集群中的某个哨兵判定主节点下线(主观下线)后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。
2、第二轮投票:选出哨兵leader
某个哨兵判定主节点客观下线后,该哨兵就会发起投票,告诉其他哨兵,它想成为 leader,想成为 leader 的哨兵节点,要满足两个条件:
3、由哨兵 leader 进行主从故障转移
选举出了哨兵 leader 后,就可以进行主从故障转移的过程了。该操作包含以下四个步骤:
思考:哨兵是怎么实现的?
当 Redis 缓存数据量大到一台服务器无法缓存时,就需要使用 Redis 切片集群(Redis Cluster)方案。
实现方式:
它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。
Redis Cluster 方案采用哈希槽(Hash Slot),来处理数据和节点之间的映射关系。
在 Redis Cluster 方案中,一个切片集群共有 16384 个哈希槽,这些哈希槽类似于数据分区,每个键值对都会根据它的 key,被映射到一个哈希槽中。
具体执行过程分为两大步:
有两种方式:
注意:在手动分配哈希槽时,需要把 16384 个槽都分配完,否则 Redis 集群无法正常工作。
通过命令手动分配哈希槽,比如节点 1 保存哈希槽 0 和 1,节点 2 保存哈希槽 2 和 3,如下所示:
redis-cli -h 192.168.1.10 –p 6379 cluster addslots 0,1
redis-cli -h 192.168.1.11 –p 6379 cluster addslots 2,3
在集群运行的过程中,key1 和 key2 计算完 CRC16 值后,对哈希槽总个数 4 进行取模,再根据各自的模数结果,就可以被映射到哈希槽 1(对应节点1) 和 哈希槽 2(对应节点2)。
就好比一个人有两个大脑,不知道受谁控制。
比如出现了两个主节点的情况。
那么在 Redis 中,集群脑裂产生数据丢失的现象是怎样的呢?
在 Redis 主从架构中,部署方式一般是「一主多从」,主节点提供写操作,从节点提供读操作。 如果主节点的网络突然发生了问题,它与所有的从节点都失联了,但是此时的主节点和客户端的网络是正常的,这个客户端并不知道 Redis 内部已经出现了问题,还在照样的向这个失联的主节点写数据(过程A),此时这些数据被旧主节点缓存到了缓冲区里,因为主从节点之间的网络问题,这些数据都是无法同步给从节点的。
这时,哨兵也发现主节点失联了,它就认为主节点挂了(但实际上主节点正常运行,只是网络出问题了),于是哨兵就会在「从节点」中选举出一个 leader 作为主节点,这时集群就有两个主节点了 —— 也就是脑裂出现了。
然后,网络突然好了,哨兵因为之前已经选举出一个新主节点了,它就会把旧主节点降级为从节点(A),然后从节点(A)会向新主节点请求数据同步,因为第一次同步是全量同步的方式,此时的从节点(A)会清空掉自己本地的数据,然后再做全量同步。所以,之前客户端在过程 A 写入的数据就会丢失了,也就是集群产生脑裂数据丢失的问题。
总结一句话就是:由于网络问题,集群节点之间失去联系。主从数据不同步;哨兵重新平衡选举,产生两个主服务。等网络恢复,旧主节点会降级为从节点,再与新主节点进行同步复制的时候,由于从节点会清空自己的缓冲区,所以导致之前客户端写入的数据丢失了。
当主节点发现从节点下线或者通信超时的总数量小于阈值时,就禁止主节点进行写数据,直接把错误返回给客户端。
在 Redis 的配置文件中有两个参数我们可以设置:
min-slaves-to-write x,主节点必须要有至少 x 个从节点连接,如果小于这个数,主节点会禁止写数据。
min-slaves-max-lag x,主从数据复制和同步的延迟不能超过 x 秒,如果超过,主节点会禁止写数据。
我们可以把 min-slaves-to-write 和 min-slaves-max-lag 这两个配置项搭配起来使用,分别给它们设置一定的阈值,假设为 N 和 T。
这两个配置项组合后的要求是:主库连接的从库中至少有 N 个从库,和主库进行数据复制时的 ACK (确认字符)消息延迟不能超过 T 秒,否则,主库就不会再接收客户端的写请求了。
即使原主库是假故障,它在假故障期间也无法响应哨兵心跳,也不能和从库进行同步,自然也就无法和从库进行 ACK 确认了。这样一来,min-slaves-to-write 和 min-slaves-max-lag 的组合要求就无法得到满足,原主库就会被限制接收客户端写请求,客户端也就不能在原主库中写入新数据了。
等到新主库上线时,就只有新主库能接收和处理客户端请求,此时,新写的数据会被直接写到新主库中。而原主库会被哨兵降为从库,即使它的数据被清空了,也不会有新数据丢失。
总结来说就是:
解决方案是通过某种限制条件来禁止主节点进行写数据。
在 Redis 的配置文件中设置两个参数,指明主节点至少要连接的从节点个数 N 以及主从数据复制和同步的延迟不能超过 T 秒,一旦达不到这个组合要求就限制主节点接收客户端的写请求,不再写入新数据,等哨兵主从切换完成后,新写的数据就会被直接写到新主节点上,从而避免避免了闹裂现象的发生,也就不会发生数据丢失了。
主要有三个作用:
如果过期时间设置得太短,可能会导致缓存命中率降低,请求直接访问后端数据库的次数增多;
如果过期时间设置得太长,可能会导致缓存中存储的数据不是最新的,从而影响系统的性能。
Redis 使用的过期删除策略是「惰性删除+定期删除」这两种策略配和使用。负责删除已过期的键值对。
每当我们对一个 key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典中。
当我们查询一个 key 时,Redis 首先检查该 key 是否存在于过期字典中:
惰性删除策略的做法是:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key,返回 null 给客户端。
惰性删除策略的优点:
惰性删除策略的缺点:
总结来说就是:
占用的系统资源少,对 CPU 时间友好;但会浪费一定的内存空间,对内存不友好。以空间换时间。
定期删除策略的做法是:每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
具体流程是:
从过期字典中随机抽取 20 个 key;
检查这 20 个 key 是否过期,并删除已过期的 key;
如果本轮检查的已过期 key 的数量,超过 5 个(抽取的个数*25%),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;
如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。
定期抽取 => 检查并删除 => 判断过期 key 是否超过 25%
定期删除策略的优点:
定期删除策略的缺点:
定期删除对内存更加友好,惰性删除对 CPU 更加友好。两者各有千秋,所以 Redis 采用的是 定期删除+惰性/懒汉式删除 。
Redis 持久化文件有两种格式:RDB 和 AOF。
下面来说下过期键在这两种格式中的呈现状态。
先讲下 RDB,RDB 文件分为两个阶段,RDB 文件生成阶段和加载阶段。
再讲下 AOF 的情况,AOF 文件也分为两个阶段,AOF 文件写入阶段和 AOF 重写阶段。
当 Redis 运行在主从模式下时,从库不会进行过期扫描,从库对过期的处理是被动的(依赖于主库)。也就是说即使从库中的 key 过期了,如果有客户端访问从库,依然可以得到 key 对应的值,像未过期的键值对一样返回。
从库的过期键处理依靠主服务器控制,主库在 key 到期时,会在 AOF 文件里增加一条 del 指令,同步到所有的从库,从库通过执行这条 del 指令来删除过期的 key。
在 Redis 的运行内存达到了某个阀值,就会触发内存淘汰机制,这个阀值就是我们设置的最大运行内存,此值在 Redis 的配置文件中可以找到,配置项为 maxmemory。
Redis 内存淘汰策略一共有八种,而这八种策略大体可分为「不进行数据淘汰」和「进行数据淘汰」两类策略。
1 - 4 - 7
noeviction(Redis3.0之后,默认的内存淘汰策略):它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。
针对「进行数据淘汰」这一类策略,又可以细分为「在设置了过期时间的数据中进行淘汰」和「在所有数据范围内进行淘汰」这两类策略。
LRU 全称是 Least Recently Used(lru) 翻译为最近最少使用,会选择淘汰最近最少使用的数据。(最久未使用 – 时间)
传统 LRU 算法的实现是基于「链表」结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可,因为链表尾部的元素就代表最久未被使用的元素。
但是 Redis 并没有使用这样的方式实现 LRU 算法,因为传统的 LRU 算法存在两个问题:
Redis 实现的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是在 Redis 的对象结构体中添加一个额外的字段,用于记录此数据的最后一次访问时间。
当 Redis 进行内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。
Redis 实现的 LRU 算法的优点:
但是 LRU 算法有一个问题,无法解决缓存污染问题,比如应用一次读取了大量的数据,而这些数据只会被读取这一次,那么这些数据会留存在 Redis 缓存中很长一段时间,造成缓存污染。
因此,在 Redis 4.0 之后引入了 LFU 算法来解决这个问题。
缓存污染问题是指:缓存中存储了错误的数据,导致应用程序获取到的数据不正确。**通常是由于缓存中存储了过期的、损坏的或者恶意的数据造成的。**当应用程序从缓存中获取到错误的数据时,可能会导致程序异常或者返回不正确的结果。为了避免缓存污染问题,需要定期清理过期数据、设置合理的缓存过期时间、使用合法的缓存数据源等措施。
LFU 全称是 Least Frequently Used 翻译为最不经常使用,LFU 算法是根据数据访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。(最少使用 – 次数)
所以, LFU 算法会记录每个数据的访问次数。当一个数据被再次访问时,就会增加该数据的访问次数。这样就解决了偶尔被访问一次之后,数据留存在缓存中很长一段时间的问题,相比于 LRU 算法也更合理一些。
LFU 算法相比于 LRU 算法的实现,多记录了「数据的访问频次」的信息。
高 16bit – 记录 key 的访问时间戳;低 8bit – 记录 key 的访问频次
Redis 对象的结构如下:
typedef struct redisObject {
...
// 24 bits,用于记录对象的访问信息
unsigned lru:24;
...
} robj;
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 故障宕机时,同时又有大量的用户请求,都无法在 Redis 中处理,于是全部请求都直接访问数据库,从而导致数据库的压力骤增,严重的会造成数据库宕机,从而形成一系列连锁反应,造成整个系统崩溃。
对于缓存雪崩问题,我们可以采用两种方案解决。
我们的业务通常会有几个数据会被频繁地访问,比如秒杀活动,这类被频地访问的数据被称为热点数据。
如果缓存中的某个热点数据过期了,此时大量的请求访问了该热点数据,就无法从缓存中读取,直接访问数据库,数据库很容易就被高并发的请求冲垮,这就是缓存击穿的问题。
可以发现缓存击穿跟缓存雪崩很相似,你可以认为缓存击穿是缓存雪崩的一个子集。(缓存数据 包含 热点数据)
应对缓存击穿可以采取前面说到两种方案:
简单来说,缓存穿透就是:大量请求的 key 是不合理的,既不存在于缓存中,也不存在于数据库中。
导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,甚至可能直接就被这么多请求弄宕机了。
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
应对缓存穿透的方案,常见的方案有三种。
缓存异常会面临的三个问题:缓存雪崩、击穿和穿透。
其中,缓存雪崩和缓存击穿主要原因是数据不在缓存中,而导致大量请求访问了数据库,数据库压力骤增,容易引发一系列连锁反应,导致系统奔溃。不过,一旦数据被重新加载回缓存,应用又可以从缓存快速读取数据,不再继续访问数据库,数据库的压力也会瞬间降下来。因此,缓存雪崩和缓存击穿应对的方案比较类似。
而缓存穿透主要原因是数据既不在缓存也不在数据库中。因此,缓存穿透与缓存雪崩、击穿应对的方案不太一样。
由于数据存储受限,系统并不是将所有数据都需要存放到缓存中的,而只是将其中一部分热点数据缓存起来,所以我们要设计一个热点数据动态缓存的策略。
热点数据动态缓存的策略总体思路:通过数据最新访问时间来做排名,并过滤掉不常访问的数据,只留下经常访问的数据。
以电商平台场景中的例子,现在要求只缓存用户经常访问的 Top 1000 的商品。具体细节如下:
在 Redis 中可以用 zadd 方法和 zrange 方法来完成排序队列和获取 200 个商品的操作。
常见的缓存更新策略共有3种:
实际开发中,Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了。
Cache Aside(旁路缓存)策略是最常用的,应用程序直接与「数据库、缓存」交互,并负责对缓存的维护,该策略又可以细分为「读策略」和「写策略」。
写策略的步骤:
读策略的步骤:
注意:写策略的步骤的顺序不能倒过来,即不能先删除缓存再更新数据库,原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。
因为删除一个数据,相比更新一个数据更加轻量级,出问题的概率更小。在实际业务中,缓存的数据可能不是直接来自某一张数据库表,也许来自多张底层数据表的聚合。
系统设计中有一个思想叫 Lazy Loading,适用于那些加载代价大的操作,删除缓存而不是更新缓存,就是懒加载思想的一个应用。
其实先更新数据库,再删除缓存也会出现数据不一致性的问题,但是在实际中,这个问题出现的概率并不高。
例如:请求 A 更新完数据库,但还未删除缓存,此时请求 B 命中了缓存并返回,就导致了数据不一致。
出现的概率低是因为 缓存的写入非常快,中间的时间差非常短,通常只有几毫秒或者几十毫秒。
「先更新数据库再删除缓存」不会造成数据不一致的问题,是因为在更新数据库的同时,缓存并没有被删除,而是在接下来的读取操作中被重新写入。具体来说,当应用程序更新数据库时,会先更新数据库中的数据,然后再删除缓存中对应的数据。接着,当下一次需要访问该数据时,应用程序会重新从数据库中读取该数据,并将其写入缓存中。这样,缓存中存储的数据就是最新的,与数据库中的数据一致。
Cache Aside 策略适合读多写少的场景,不适合写多的场景,因为当写入比较频繁时,缓存中的数据会被频繁地清理,这样会对缓存的命中率有一些影响。
如果业务对缓存命中率有严格的要求,那么可以考虑两种解决方案:
Read/Write Through(读穿 / 写穿)策略原则是:应用程序只和缓存交互,不再和数据库交互,然后由缓存和数据库交互,相当于更新数据库的操作由缓存自己代理了。
1、Read Through 读穿策略
2、Write Through 写穿策略
当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:
Read Through/Write Through 策略的特点是:
由缓存节点而非应用程序来和数据库打交道,
在我们开发过程中相比 Cache Aside 策略要少见一些,原因是我们经常使用的分布式缓存组件,无论是 Memcached 还是 Redis 都不提供写入数据库和自动加载数据库中的数据的功能。
而我们在使用本地缓存的时候可以考虑使用这种策略。
Write Back(写回)策略:在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。
Write Back(写回)策略也不能应用到我们常用的数据库和缓存的场景中,因为 Redis 并没有异步更新数据库的功能。
Write Back 是计算机体系结构中的设计,比如 CPU 的缓存、操作系统中文件系统的缓存都采用了 Write Back(写回)策略。
Write Back 策略特别适合写多的场景,因为发生写操作的时候, 只需要更新缓存,就立马返回了。比如,写文件的时候,实际上是写入到文件系统的缓存就返回了,并不会写磁盘。
写回策略会带来什么问题?
带来的问题是,数据不是强一致性的,而且会有数据丢失的风险,因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。所以你会发现系统在掉电之后,之前写入的文件会有部分丢失,就是因为 Page Cache 还没有来得及刷盘造成的。
一共两个操作:
使用 Cache Aside 旁路缓存策略,并增加**「消息队列来重试缓存的删除」或「订阅 MySQL binlog 再操作缓存」**来保证两个操作都能执行成功。
1、只给缓存加上过期时间进行兜底会出现什么情况?
可能会出现删除缓存操作失败的问题,从而出现缓存中的数据是旧值,数据库的是最新值,需要过一段时间才会有更新生效的现象(因为过期时间到了,缓存数据重新写入)。
2、如何保证两个操作都能执行成功?
有两种方法:
这两种方法有一个共同的特点,都是采用异步操作缓存。
1. 重试机制
我们可以引入消息队列,将第二个操作(删除缓存)要操作的数据加入到消息队列,由消费者来操作数据。
2. 订阅 MySQL binlog,再操作缓存
「先更新数据库,再删缓存」的策略的第一步是更新数据库,那么更新数据库成功,就会产生一条变更日志,记录在 binlog 里。
于是我们就可以通过订阅 binlog 日志,拿到具体要操作的数据,然后再执行缓存删除,阿里巴巴开源的 Canal 中间件就是基于这个实现的。
Canal 的工作原理:
KEYS *
;save
命令生成 RDB 快照文件时;有两种常见的方法:
大 key 并不是指 key 的值很大,而是指 key 对应的 value 很大。
一般而言,下面这两种情况被称为大 key:
bigkey 除了会消耗更多的内存空间和带宽,还会对性能造成比较大的影响。
大 key 会带来以下四种影响:
有三种方法:
有两种方法:
UNLINK
命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 SCAN
命令结合 DEL
命令来分批次删除。简单来说,如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 hotkey。
hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。
处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。
此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。
因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性。
1、使用 Redis 自带的 --hotkeys 参数来查找。
2、使用 MONITOR 命令。
MONITOR 命令是 Redis 提供的一种实时查看 Redis 的所有操作的方式,可以用于临时监控 Redis 实例的操作情况,包括读写、删除等操作。
3、借助开源项目。
比如京东零售的 hotkeyopen in new window 这个项目,不光支持 hotkey 的发现,还支持 hotkey 的处理。
4、根据业务情况提前预估。
可以根据业务情况来预估一些 hotkey,比如参与秒杀活动的商品数据等。
不过,我们无法预估所有 hotkey 的出现,比如突发的热点新闻事件等。
5、业务代码中记录分析。
在业务代码中添加相应的逻辑对 key 的访问情况进行记录分析。不过,这种方式会让业务代码的复杂性增加,一般也不会采用。
6、借助公有云的 Redis 分析服务。
如果你用的是公有云的 Redis 服务的话,可以看看其是否提供了 key 分析功能(一般都有提供)。
hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用):
除了这些方法之外,如果你使用的公有云的 Redis 服务话,还可以留意其提供的开箱即用的解决方案。
慢查询命令是指:执行时间超过 Redis 配置的阈值的命令。
这个阈值可以通过 Redis 配置文件中的 slowlog-log-slower-than 参数进行设置,默认值是 10000,即 10 毫秒。
当 Redis 执行一个命令的时间超过了这个阈值,这个命令就会被记录在 Redis 的慢查询日志中。慢查询日志会记录命令的执行时间、执行命令的客户端、命令的参数等信息。通过查看慢查询日志,可以找到执行时间较长的命令,进而优化 Redis 的性能。
这是因为 Redis 是单线程的,如果某个命令执行时间过长,就会阻塞其他命令的执行。为了避免这种情况的发生,Redis 会记录所有执行时间超过一定阈值的命令,并将其作为慢查询命令进行记录和统计,以便开发人员进行优化和调整。
有三种常用的方法:
可以将内存碎片简单地理解为那些不可用的后续没办法再被分配存储其他数据的空闲内存。
比如:操作系统为你分配了 32 字节的连续内存空间,而你存储数据实际只需要使用 24 字节内存空间,那这多余出来的 8 字节内存空间如果后续没办法再被分配存储其他数据的话,就可以被称为内存碎片。
Redis 内存碎片虽然不会影响 Redis 性能,但是会增加内存消耗。
有 2 个比较常见的原因:
1、Redis 存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
因为 Redis 对内存的分配和回收采用的是 jemalloc 或 libc 等第三方库,这些库的内存分配策略可能会导致内存碎片。
2、频繁修改 Redis 中的数据也会产生内存碎片。
比如:当 Redis 中的某个数据删除时,Redis 通常不会轻易释放内存给操作系统。
使用 info memory
命令即可查看 Redis 内存相关的信息。
Redis4.0-RC3 版本以后自带了内存整理,可以避免内存碎片率过大的问题。
有两种方法:
1、直接通过 config set
命令将 activedefrag
配置项设置为 yes
即可,但可能会对 Redis 的性能产生影响。
通过 Redis 自动内存碎片清理机制可能会对 Redis 的性能产生影响。
可以设置相关参数来控制具体什么时候清理以及减少对 Redis 性能的影响。
2、重启 Redis 可以做到内存碎片重新整理,但是需要重启Redis服务,会导致一定的停机时间。
延迟队列是指:把当前要做的事情,往后推迟一段时间再做。
延迟队列的常见使用场景有以下几种:
可以使用有序集合(ZSet)的方式来实现延迟消息队列的,ZSet 有一个 Score 属性可以用来存储延迟执行的时间。
zadd score1 value1
命令就可以一直往内存中生产消息。zrangebysocre
查询符合条件的所有待处理的任务,通过循环执行队列任务即可。管道技术(Pipeline):是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
使用管道技术可以解决多个命令执行时的网络等待,它是把多个命令整合到一起发送给服务器端处理之后统一返回给客户端,这样就免去了每条命令执行后都要等待的情况,从而有效地提高了程序的执行效率。
不支持。
Redis 中并没有提供回滚机制,虽然 Redis 提供了 DISCARD 命令,但是这个命令只能用来主动放弃事务执行,把暂存的命令队列清空,起不到回滚的效果。
事务执行过程中,如果命令入队时没报错,而事务提交后,实际执行时报错了,正确的命令依然可以正常执行,所以这可以看出 Redis 并不一定保证原子性
MySQL 在执行事务时,会提供回滚机制,当事务执行发生错误时,事务中的所有操作都会撤销,已经修改的数据也会被恢复到事务执行前的状态。
Redis 不支持事务回滚的原因有两个:
这里不支持事务回滚,指的是不支持事务运行时错误的事务回滚
1、什么是分布式锁?
分布式锁是用于分布式环境下并发控制的一种机制,用于控制某个资源在同一时刻只能被一个应用所使用。
2、加锁
Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:
基于 Redis 节点实现分布式锁时,对于加锁操作,我们需要满足三个条件。
满足这三个条件的分布式命令如下:
SET lock_key unique_value NX PX 10000
简单来说,三个条件就是:需以原子操作完成、要有过期时间和一个用于标识客户端的标识。
3、解锁
用 Lua 脚本来进行解锁。
解锁的过程就是:将 lock_key 键删除(del lock_key),但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
1、优点
基于 Redis 实现分布式锁的优点:
2、缺点
基于 Redis 实现分布式锁的缺点:
为了保证集群环境下分布式锁的可靠性,Redis 官方设计了一个分布式锁算法 Redlock(红锁)。
它是基于多个 Redis 节点的分布式锁,即使有节点发生了故障,锁变量仍然是存在的,客户端还是可以完成锁操作。官方推荐是至少部署 5 个 Redis 节点,而且都是主节点,它们之间没有任何关系,都是一个个孤立的节点。
Redlock 算法的基本思路,是让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。
加锁失败会怎样?
加锁失败后,客户端会向所有 Redis 节点发起释放锁的操作,释放锁的操作和在单节点上释放锁的操作一样,只要执行释放锁的 Lua 脚本就可以了。