0%

Redis

本章是整理知识内容,为强化知识长期更新。

Redis 简介

Redis 是完全开源免费的,遵守 BSD 协议,英文全称是Remote Dictionary Server(远程字典服务),是一个开源的使用ANSI C语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value数据库,并提供多种语言的API。

  • Redis 与 其他 key - value 缓存产品有以下三个特点:
    • Redis 支持数据持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用。
    • Redis 不仅仅支持简单的 key - value 类型的数据,同时还提供 list,set,zset,hash 等数据结构的存储
    • Redis 支持数据的备份,即 master - slave 模式的数据备份。
  • Redis是内存数据库,所有操作都在内存上完成,内存的访问速度本身就很快,读的速度是 110000 次 /s, 写的速度是 81000 次 /s。另一方面是因为它的数据结构。键值对是按一定的数据结构来组织的,操作键值对最终就是对数据结构进行增删改查操作,所以高效的数据结构是 Redis 快速处理数据的基础。
  • Redis使用基于哈西槽(slot)的数据划分方式。

Redis数据类型与数据结构

数据结构时间复杂度

名称 时间复杂度
哈希表 O(1)
跳表 O(logN)
双向链表 O(N)
压缩列表 O(N)
整数数组 O(N)

Redis事务

严格上来说redis是伪事物,

  • Redis事务功能是通过MULTI、EXEC、DISCARD和WATCH 四个原语实现的。Redis会将一个事务中的所有命令序列化,然后按顺序执行。
    1. redis 不支持回滚“Redis 在事务失败时不进行回滚,而是继续执行余下的命令”, 所以 Redis 的内部可以保持简单且快速。
    2. 如果在一个事务中的命令出现错误,那么所有的命令都不会执行;
    3. 如果在一个事务中出现运行错误,那么正确的命令会被执行。
  • MULTI命令用于开启一个事务,它总是返回OK。 MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
  • EXEC:执行所有事务块内的命令。返回事务块内所有命令的返回值,按命令执行的先后顺序排列。当操作被打断时,返回空值 nil 。
  • 通过调用DISCARD,客户端可以清空事务队列,并放弃执行事务, 并且客户端会从事务状态中退出。
  • WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。 可以监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行,监控一直持续到EXEC命令。

Redis单线程

  • 我们通常说,Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。Redis 的单线程设计机制以及多路复用机制
  • Linux 中的 IO 多路复用机制
    • Linux 中的 IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。简单来说,在 Redis 只运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。
  • Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程

Redis单线程处理IO请求性能瓶颈

  • 任意一个请求在server中一旦发生耗时,都会影响整个server的性能,也就是说后面的请求都要等前面这个耗时请求处理完成,自己才能被处理到。耗时的操作包括以下几种:
    • 操作bigkey:写入一个bigkey在分配内存时需要消耗更多的时间,同样,删除bigkey释放内存同样会产生耗时;
    • 使用复杂度过高的命令:例如SORT/SUNION/ZUNIONSTORE,或者O(N)命令,但是N很大,例如lrange key 0 -1一次查询全量数据;
    • 大量key集中过期:Redis的过期机制也是在主线程中执行的,大量key集中过期会导致处理一个请求时,耗时都在删除过期key,耗时变长;
    • 淘汰策略:淘汰策略也是在主线程执行的,当内存超过Redis内存上限后,每次写入都需要淘汰一些key,也会造成耗时变长;
    • AOF刷盘开启always机制:每次写入都需要把这个操作刷到磁盘,写磁盘的速度远比写内存慢,会拖慢Redis的性能;
    • 主从全量同步生成RDB:虽然采用fork子进程生成数据快照,但fork这一瞬间也是会阻塞整个线程的,实例越大,阻塞时间越久;
  • 并发量非常大时,单线程读写客户端IO数据存在性能瓶颈,虽然采用IO多路复用机制,但是读写客户端数据依旧是同步IO,只能单线程依次读取客户端的数据,无法利用到CPU多核。
  • Redis在4.0推出了lazy-free机制,把bigkey释放内存的耗时操作放在了异步线程中执行,降低对主线程的影响。
  • Redis在6.0推出了多线程,可以在高并发场景下利用CPU多核多线程读写客户端数据,进一步提升server性能,当然,只是针对客户端的读写是并行的,每个命令的真正操作依旧是单线程的。

Redis网络框架

一般服务器都是采用Linxu系统,在Linxu上Redis采用epoll模型进行网络通信

Redis线程模型

  • redis 内部使⽤⽂件事件处理器file event handler,这个⽂件事件处理器是单线程的,所以redis 才叫做单线程的模型。它采⽤ IO 多路复⽤机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进⾏处理。

Socket通信过程

Redis采用Socket通信传输数据。Socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口,供应用层调用实现进程在网络中的通信。Socket起源于UNIX,在Unix一切皆文件的思想下,进程间通信就被冠名为文件描述符(file desciptor),Socket是一种“打开—读/写—关闭”模式的实现,服务器和客户端各自维护一个“文件”,在建立连接打开后,可以向文件写入内容供对方读取或者读取对方内容,通讯结束时关闭文件。

img

  • Socket保证了不同计算机之间的通信,也就是网络通信。通信模型是服务器与客户端之间的通信。两端都建立了一个Socket对象,然后通过Socket对象对数据进行传输。通常服务器处于一个无限循环,等待客户端的连接。

img

  • 服务端先初始化Socket,建立流式套接字,与本机地址及端口进行绑定,然后通知TCP,准备好接收连接,调用accept()阻塞,等待来自客户端的连接。如果这时客户端与服务器建立了连接,客户端发送数据请求,服务器接收请求并处理请求,然后把响应数据发送给客户端,客户端读取数据,直到数据交换完毕。最后关闭连接,交互结束。
  • TCP三次握手的Socket过程

img

  • 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待;
  • 客户端Socket对象调用connect()向服务器发送了一个SYN并阻塞;
  • 服务器完成了第一次握手,即发送SYN和ACK应答;
  • 客户端收到服务端发送的应答之后,从connect()返回,再发送一个ACK给服务器;
  • 服务器Socket对象接收客户端第三次握手ACK确认,此时服务端从accept()返回,建立连接。

Redis网络IO处理过程

Redis的网络模式是单reactor模式。non-blocking io + ( epoll 、select 、Kequeue

  • Redis基于Reactor模式开发了自己的文件事件派发器,文件事件处理器使用I/O多路复用技术,同时监听多个套接字,并为套接字关联不同事件处理函数。当套接字的可读或者可写事件触发时,就会调用相应的事件处理函数。

img

  • 在同步网络IO操作中,有潜在的阻塞点,分别是accept()、recv()。当Redis监听到一个客户端有连接请求,但未成功建立时,会柱塞在accept()函数里面,导致其他客户端无法和Redis建立连接,同理当Redis通过recv()从一个客户端读取数据但时候,如果数据一直没有达到。Redis也会一直柱塞在recv()。
  • Redis非柱塞模式。在非柱塞模式中主要提现在三个函数方法上。

img

  • 设置acceot()非柱塞,当Redis调用函数的时候,如果没有连接Redis并不会柱塞,这样就可以处理其它操作,同理在recv()和send()函数调用时候,也可以非柱塞处理。这样保证了Redis线程不会和同步的IO一样一直柱塞等待,也不会导致Redis无法处理实际达到的请求。

基于多路复用IO模型

Linxu中IO多路复用机制是指一个线程处理多个IO流,也就是select、epool机制。在Redis运行单线程的情况下,该机制允许内核中,同时存在多个监听套接字和已连接套接字。内核会一直监听这些套接字上的连接请求或数据请求。一旦有请求到达,就会交给 Redis 线程处理,这就实现了一个 Redis 线程处理多个 IO 流的效果。

img

  • 在RedisIO模型中,Redis的网络框架调用epool机制,让内核监听这些套接字。这样Redis不会柱塞在某一个特定的或已连接的套接字上,这样Redis就可以同时和多个客户端连接并处理请求,从而提高并发性。、
  • select、epoll提供基础事件的回调机制,针对不同的事件发送,并用相应的处理函数。比如Accept 事件和 Read 事件,Redis 分别对这两个事件注册 accept 和 get 回调函数。当 Linux 内核监听到有连接请求或读数据请求时,就会触发 Accept 事件和 Read 事件,此时,内核就会回调 Redis 相应的 accept 和 get 函数进行处理。

Redis持久化机制

Redis是一个支持持久化的内存数据库,通过持久化机制把内存中的数据同步到硬盘文件来保证数据持久化。当Redis重启后通过把硬盘文件重新加载到内存,就能达到恢复数据的目的。

  • Redis 4.0 之前数据持久化方式有两种:AOF 方式和 RDB 方式。

    • RDB(Redis DataBase,快照方式)是将某一个时刻的内存数据,以二进制的方式写入磁盘。RDB 默认的保存文件为 dump.rdb,优点是以二进制存储的,因此占用的空间更小、数据存储更紧凑,并且与 AOF 相比,RDB 具备更快的重启恢复能力。
    • AOF(Append Only File,文件追加方式)是指将所有的操作命令,以文本的形式追加到文件中。AOF 默认的保存文件为 appendonly.aof,它的优点是存储频率更高,因此丢失数据的风险就越低,并且 AOF 并不是以二进制存储的,所以它的存储信息更易懂。缺点是占用空间大,重启之后的数据恢复速度比较慢。
  • 混合模式,Redis4.0之后增加的方式,混合模式集合了RDB和AOF的优点,在写入的时候先把当前数据以RDB的形式写入文件的开头,然后以AOF的格式存入文件,这样既能保证Redis重启时候的速度,又能减少数据丢失的风险。

AOF(Append Only File)

Redis会将每一个收到的写命令都通过Write函数追加到文件最后,类似于MySQL的binlog。当Redis重启是会通过重新执行文件中保存的写命令来在内存中重建整个数据库的内容。当两种方式同时开启时,数据恢复Redis会优先选择AOF恢复。AOF持久化机制是以日志的形式记录Redis中的每一次的增删改操作,不会记录查询操作,以文本的形式记录,打开记录的日志文件就可以查看操作记录。

  • AOF更好保证数据不会被丢失,最多只丢失一秒内的数据,通过fock一个子进程处理持久化操作,保证了主进程不会进程io操作,能高效的处理客户端的请求。
  • AOF的日志文件的记录可读性非常的高,即使某一时刻有人执行flushall清空了所有数据,只需要拿到aof的日志文件,然后把最后一条的flushall给删除掉,就可以恢复数据。

1
2
redis> RPUSH list 1 2 3 4
(integer) 4
  • AOF实际保存结果
1
2
3
4
5
6
7
8
9
10
11
12
13
*6 #表示当前命令有6个部分,每个部分由$开头
$5
RPUSH
$4
list
$1
1
$1
2
$1
3
$1
4
  • 与Mysql写前日志不同WAL(Write Ahead Log),Redis采用写后日志,先执行命令,把数据写入内存中。然后在记录日志。这样可以避免记录错误命令、也不回柱塞当前的写操作。

持久化触发机制

AOF 自动触发策略
  • Always,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
    • 同步写回可以做到基本不丢数据,但是它在每一个写命令后都有一个慢速的落盘操作,不可避免地会影响主线程性能。
  • Everysec,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
    • 每秒写回采用一秒写回一次的频率,避免了“同步写回”的性能开销,虽然减少了对系统性能的影响,但是如果发生宕机,上一秒内未落盘的命令操作仍然会丢失。所以,这只能算是,在避免影响主线程性能和避免数据丢失两者间取了个折中。
  • No,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
    • 操作系统控制的写回在写完缓冲区后,就可以继续执行后续的命令,但是落盘的时机已经不在 Redis 手中了,只要 AOF 记录没有写回磁盘,一旦宕机对应的数据就丢失了。
AOF手动触发
1
在客户端执行 bgrewriteaof 命令就可以手动触发 AOF 持久化
AOF重写触发条件

只有同时满足 auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 设置的条件,才会触发 AOF 文件重写。

  • auto-aof-rewrite-min-size:允许 AOF 重写的最小文件容量,默认是 64mb 。
  • auto-aof-rewrite-percentage:AOF 文件重写的大小比例,默认值是 100,表示 100%,也就是只有当前 AOF 文件,比最后一次(上次)的 AOF 文件大一倍时,才会启动 AOF 文件重写。
  • 使用 bgrewriteaof 命令,可以自动触发 AOF 文件重写。
AOF 重写机制

在写入所有的操作到日志文件中时,就会出现日志文件很多重复的操作,比如一个计数器操作了99次,如果不重写就会有99条记录。导致日志文件越来越大。就浪费了资源空间,所以在Redis中出现了rewrite机制。redis提供了bgrewriteaof命令。将内存中的数据以命令的方式保存到临时文件中,同时会fork出一条新进程来将文件重写。

  • 重写AOF的日志文件不是读取旧的日志文件瘦身,而是将内存中的数据用命令的方式重写一个AOF文件,重新保存替换原来旧的日志文件,因此内存中的数据才是最新的。
  • 重写操作也会fork一个子进程来处理重写操作,重写以内存中的数据作为重写的源,避免了操作的冗余性,保证了数据的最新。
AOF重写流程

  • 和 AOF 日志由主线程写回不同,重写过程是由后台子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致数据库性能下降。
    1. 处理命令请求。
    2. 将写入命令追加到现有的AOF文件中。
    3. 将写入命令追加到AOF重写缓存中。
    4. 当子进程完成AOF重写之后,它会向父进程发送一个完成信号,父进程接到完成信号后,会将AOF重写缓存中的内容全部写入到AOF文件中。并对新的AOF文件进行改名,覆盖原有的AOF文件。
  • AOF文件重写是生成一个全新的文件,并把当前数据最少的操作命令保存到新文件上。当把所有的数据都保存至新文件后,Redis会交换两个文件,并把最新的持久化操作命令追加到新文件上。
    • 子进程进行AOF重写期间,主进程可以继续处理命令请求。
    • 子进程带有主进程的数据副本,使用子进程而不是线程。可以避免锁的情况下,保证数据的安全性。

AOF配置

redis.conf文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 是否开启 AOF,yes 为开启,默认是关闭
appendonly no

# AOF 默认文件名
appendfilename "appendonly.aof"

# AOF 持久化策略配置
# appendfsync always
appendfsync everysec
# appendfsync no

# AOF 文件重写的大小比例,默认值是 100,表示 100%,也就是只有当前 AOF 文件,比最后一次的 AOF 文件大一倍时,才会启动 AOF 文件重写。
auto-aof-rewrite-percentage 100

# 允许 AOF 重写的最小文件容量
auto-aof-rewrite-min-size 64mb

# 是否开启启动时加载 AOF 文件效验,默认值是 yes,表示尽可能的加载 AOF 文件,忽略错误部分信息,并启动 Redis 服务。
# 如果值为 no,则表示,停止启动 Redis,用户必须手动修复 AOF 文件才能正常启动 Redis 服务。
aof-load-truncated yes

AOF小结

  • AOF文件通过保存所修改数据的命令来记录数据库的状态。

  • AOF文件中保存所有的命令都以Redis通讯协议格式来保存。

  • 如果刚执行完一个命令,还没有来得及记日志就宕机了,那么这个命令和相应的数据就有丢失的风险。如果此时 Redis 是用作缓存,还可以从后端数据库重新读入数据进行恢复,但是,如果 Redis 是直接用作数据库的话,此时,因为命令没有记入日志,所以就无法用日志进行恢复了。

  • AOF 虽然避免了对当前命令的阻塞,但可能会给下一个操作带来阻塞风险。这是因为,AOF 日志也是在主线程中执行的,如果在把日志文件写入磁盘时,磁盘写压力大,就会导致写盘很慢,进而导致后续的操作也无法执行了。

  • Redis三种写回策略提现了一种设计原则trade-off取舍,在性能和可靠性保证之前做取舍。

  • 由于Redis重度使用fock,Huge page在实际使用Redis时是建议关掉的。

    • Huge page对提升TLB命中率比较友好,因为在相同的内存容量下,使用huge page可以减少页表项,TLB就可以缓存更多的页表项,能减少TLB miss的开销。
    • fork出的子进程指向与父进程相同的内存地址空间,此时子进程就可以执行AOF重写,把内存中的所有数据写入到AOF文件中。但是此时父进程依旧是会有流量写入的,如果父进程操作的是一个已经存在的key,那么这个时候父进程就会真正拷贝这个key对应的内存数据,申请新的内存空间,内存分配是以页为单位进行分配的,默认4k,如果父进程此时操作的是一个bigkey,重新申请大块内存耗时会变长,可能会产阻塞风险。另外,如果操作系统开启了内存大页机制(Huge Page,页面大小2M),那么父进程申请内存时阻塞的概率将会大大提高,所以在Redis机器上需要关闭Huge Page机制。Redis每次fork生成RDB或AOF重写完成后,都可以在Redis log中看到父进程重新申请了多大的内存空间。

RDB(Snapshot)

RDB持久化就是将当前进程的数据以生成快照的形式持久化到磁盘中。对于快照的理解,我们可以理解为将当前线程的数据以拍照的形式保存下来。

  • 将RDB做镜像全量持久化,AOF做增量持久化。RDB会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要AOF来配合使用。在redis实例重启时,会使用RDB持久化文件重新构建内存,再使用AOF重放近期的操作指令来实现完整恢复重启之前的状态。
  • RDB是Redis默认的持久化方式。按照一定的时间周期策略把内存的数据以快照的形式保存到硬盘的二进制文件。即Snapshot快照存储,对应产生的数据文件为dump.rdb,通过配置文件中的save参数来定义快照的周期。( 快照可以是其所表示的数据的一个副本,也可以是数据的一个复制品。)
  • RDB持久化后的文件是紧凑的二进制文件,适合于备份、全量复制、大规模数据恢复的场景,对数据完整性和一致性要求不高,RDB会丢失最后一次快照的数据。
  • Redis 的数据都在内存中,为了提供所有数据的可靠性保证,它执行的是全量快照,也就是说,把内存中的所有数据都记录到磁盘中。全量数据越多,RDB 文件就越大,往磁盘上写数据的时间开销就越大。
持久化触发机制

RDB 的持久化触发方式有两类:一类是手动触发,另一类是自动触发。

  • 手动触发 savebgsave

    • save:在主线程中执行,会导致阻塞;尽量避免在生产环境中使用。
    • bgsave fork()一个子进程,专门用于写入 RDB 文件,避免了主线程的阻塞,这也是 Redis RDB 文件生成的默认配置。整个过程中只有在 fork() 子进程时有短暂的阻塞,当子进程被创建之后,Redis 的主进程就可以响应其他客户端的请求了。
  • 条件出发save m n ,该命令表示m秒内n个健发生改变,则自动触发。会执行一次bgsave

  • flushall清空Redis数据库,自动触发持久化并清空RDB文件。

  • 主从同步触发,在Redis主从复制中,主节点会执行bgsave命令,并将RDB文件发送给从节点。过程会自动触发Redis持久化

fork和cow
  • fork和cow。fork是指redis通过创建子进程来进行RDB操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
RDB配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# RDB 保存的条件
save 900 1 # 表示 900 秒内如果至少有 1 个 key 值变化,则把数据持久化到硬盘;
save 300 10 # 表示 300 秒内如果至少有 10 个 key 值变化,则把数据持久化到硬盘;

# bgsave 失败之后,是否停止持久化数据到磁盘,yes 表示停止持久化,no 表示忽略错误继续写文件。
stop-writes-on-bgsave-error yes

# RDB 文件压缩
rdbcompression yes # 它的默认值是 yes 表示开启 RDB 文件压缩,Redis 会采用 LZF 算法进行压缩。如果不想消耗 CPU 性能来进行文件压缩的话,可以设置为关闭此功能,这样的缺点是需要更多的磁盘空间来保存文件。

# 写入文件和读取文件时是否开启 RDB 文件检查,检查是否有无损坏,如果在启动是检查发现损坏,则停止启动。
rdbchecksum yes

# RDB 文件名
dbfilename dump.rdb

# RDB 文件目录
dir ./

RDB小结
  • 因为RDB只能保存某个时间间隔的数据,如果Redis突然宕机了,则会丢失最近一段时间的数据。
  • RDB需要经常fock()才能使用子进程持久化在磁盘上。如果数据量很大fock()时间可能会很长,期间还会导致CPU不稳定。严重可能导致RedisServer阻塞较长时间。

Redis启动RDB&AOF持久化文件加载机制

Redis是基于内存的非关系型K-V数据库,它是基于内存的,如果Redis服务器挂了,数据就会丢失。为了避免数据丢失了,Redis提供了持久化,即把数据保存到磁盘。

混合持久化

RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时使用 RDB 和 AOF 各种的优点,Redis 4.0 之后新增了混合持久化的方式。

  • 简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。
Redis启动混合持久化文件加载机制

Redis的过期策略以及内存淘汰机制

  • redis采用的是定期删除+惰性删除策略。

    • 定期删除,redis默认每个100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查(如果每隔100ms,全部key进行检查,redis岂不是卡死)。因此,如果只采用定期删除策略,会导致很多key到时间没有删除。惰性删除派上用场。也就是说在你获取某个key的时候,redis会检查一下,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除。
  • 定时删除

    • 每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即对key进行清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的CPU资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
  • 内存淘汰策略

    • 当Redis内存快耗尽时,Redis会启动内存淘汰机制,将部分key清掉以腾出内存。
  • 主要有5种处理机制

    • LRU 最近最少使用
    • LFU 最近使用频率低
    • Random 随机淘汰
    • TTL 过期时间
    • No-Enviction 驱逐(什么都不做直接返回错误)
    • volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰。
  • volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰。

    • volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰。
  • volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。

    • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰。
  • allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰。

    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰。
  • no-enviction(驱逐):禁止驱逐数据,新写入操作会报错。

    • 如果没有设置 expire 的key, 不满足先决条件(prerequisites); 那么 volatile-lru, volatile-random 和volatile-ttl 策略的行为, 和 noeviction(不删除) 基本上一致。
  • 内存淘汰算法

    • LRU算法,全称是 Least Recently Used 译为最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。

      • LRU 算法需要基于链表结构,链表中的元素按照操作顺序从前往后排列,最新操作的键会被移动到表头,当需要内存淘汰时,只需要删除链表尾部的元素即可。

      • Redis LRU ,Redis 使用的是一种近似 LRU 算法,目的是为了更好的节约内存,它的实现方式是给现有的数据结构添加一个额外的字段,用于记录此键值的最后一次访问时间,Redis 内存淘汰时,会使用随机采样的方式来淘汰数据,它是随机取 5 个值(此值可配置),然后淘汰最久没有使用的那个。

      • 但是有一个缺陷,如果一键很少访问,但是最近访问了一次。就不会被淘汰。

    • LFU算法,LFU 全称是 Least Frequently Used 翻译为最不常用的,最不常用的算法是根据总访问次数来淘汰数据的,它的核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。

    • LFU 解决了偶尔被访问一次之后,数据就不会被淘汰的问题,相比于 LRU 算法也更合理一些。

Redis 的过期策略和内存淘汰机制的区别

  • 新增 Redis 缓存时可以设置缓存的过期时间,该时间保证了数据在规定的时间内失效。通过这种方式可以完成某些场景的业务开发。对对于已经过期的数据,Redis 将使用两种策略来删除这些过期键,它们分别是惰性删除和定期删除。

  • 惰性删除是指 Redis 服务器不主动删除过期的键值,而是当访问键值时,再检查当前的键值是否过期,如果过期则执行删除并返回 null 给客户端;如果没过期则正常返回值信息给客户端。

  • 定期删除是指 Redis 服务器每隔一段时间会检查一下数据库,看看是否有过期键可以被清除。默认情况下 Redis 定期检查的频率是每秒扫描 10 次,用于定期清除过期键。当然此值还可以通过配置文件进行设置,在 redis.conf 中修改配置“hz”即可,默认的值为“hz 10”。Redis 服务器为了保证过期删除策略不会导致线程卡死,会给过期扫描增加了最大执行时间为 25ms。

  • 当 Redis 的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,这和过期策略是完全不同的两个概念,经常有人把二者搞混,这两者一个是在正常情况下清除过期键,一个是在非正常情况下为了保证 Redis 顺利运行的保护策略。

Redis 实现高可用

  • 单节点数据持久化,保证了系统在发生宕机或者重启之后数据不会丢失,增加了系统的可靠性和减少了系统不可用的时间(省去了手动恢复数据的过程)
  • 主从数据同步(主从复制),可以将数据存储至多台服务器,这样当遇到一台服务器宕机之后,可以很快地切换至另一台服务器以继续提供服务
  • Redis 哨兵模式(Sentinel),用于发生故障之后自动切换服务器
  • Redis 集群(Cluster),多主多从的 Redis 分布式集群环境,用于提供性能更好的 Redis 服务,并且它自身拥有故障自动切换的能力。

单机版

单机版的Redis也有很多优点,比如实现实现简单、维护简单、部署简单、维护成本非常低,不需要其它额外的开支。但是,因为是单机版的Redis所以也存在很多的问题,比如最明显的单点故障问题,一个Redis挂了,所有的请求就会直接打在了DB上。并且一个Redis抗并发数量也是有限的,同时要兼顾读写两种请求,只要访问量一上来,Redis就受不了了,另一方面单机版的Redis数据量存储也是有限的,数据量一大,再重启Redis的时候,就会非常的慢,所以局限性也是比较大的。

主从版本

主从的原理还算是比较简单的,一主多从,主数据库(master)可以读也可以写(read/write),从数据库仅读(only read)。主从模式一般实现读写分离主数据库仅写(only write),减轻主数据库的压力。

  • 但是数据的一致性问题,假如主数据库写操作完成,那么他的数据会被复制到从数据库,若是还没有即使复制到从数据库,读请求又来了,此时读取的数据就不是最新的数据。从主同步的过程网络出故障了,导致主从同步失败,也会出现问题数据一致性的问题。主从模式不具备自动容错和恢复的功能,一旦主数据库,从节点晋升未主数据库的过程需要人为操作,维护的成本就会升高,并且主节点的写能力、存储能力都会受到限制。
数据同步方式
  1. 当slave启动后会向master发送PSYNC命令,master节后到从数据库的命令后通过bgsave保存快照(RDB持久化),并且期间的执行的些命令会被缓存起来。
  2. 然后master会将保存的快照发送给slave,并且继续缓存期间的写命令。
  3. slave收到主数据库发送过来的快照就会加载到自己的数据库中。
  4. 最后master讲缓存的命令同步给slave,slave收到命令后执行一遍,这样master与slave数据就保持一致了。

哨兵

哨兵模式是 Redis 官方推荐的高可用模式。哨兵模式是主从的升级版,因为主从的出现故障后,不会自动恢复,需要人为干预。在主从的基础上,实现哨兵模式就是为了监控主从的运行状况,对主从的健壮进行监控,就好像哨兵一样,只要有异常就发出警告,对异常状况进行处理。

监控

监控master和slave是否正常运行,以及哨兵之间也会相互监控

  • 监控是指哨兵进程在运行时,周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。如果从库没有在规定时间内响应哨兵的 PING 命令,哨兵就会把它标记为“下线状态”;同样,如果主库也没有在规定时间内响应哨兵的 PING 命令,哨兵就会判定主库下线,然后开始自动切换主库的流程。
自动故障恢复

当master出现故障的时候,会自动选举一个slave作为master顶上去。

节点通信
  • 哨兵与master建立连接后,定期会向(10秒一次)master和slave发送INFO命令,若是master被标记为主观下线,频率就会变为1秒一次。
  • 发送的内容包含哨兵的ip和端口、运行id、配置版本、master名字、master的ip端口还有master的配置版本等信息。
  • 定期的向master、slave和其它哨兵发送PING命令(每秒一次),以便检测对象是否存活,若是对方接收到了PING命令,无故障情况下,会回复PONG命令。
  1. INFO:该命令可以获取主从数据库的最新信息,可以实现新结点的发现
  2. PING:该命令被使用最频繁,该命令封装了自身节点和其它节点的状态数据。
  3. PONG:当节点收到MEET和PING,会回复PONG命令,也把自己的状态发送给对方。
  4. MEET:该命令在新结点加入集群的时候,会向老节点发送该命令,表示自己是个新人
  5. FAIL:当节点下线,会向集群中广播该消息。
上线和下线

当哨兵与master相同之后就会定期一直保持联系,若是某一时刻哨兵发送的PING在指定时间内没有收到回复(sentinel down-after-milliseconds master-name milliseconds 配置),那么发送PING命令的哨兵就会认为该master主观下线Subjectively Down)。

因为有可能是哨兵与该master之间的网络问题造成的,而不是master本身的原因,所以哨兵同时会询问其它的哨兵是否也认为该master下线,若是认为该节点下线的哨兵达到一定的数量(前面的quorum字段配置),就会认为该节点客观下线Objectively Down)。

若是没有足够数量的sentinel同意该master下线,则该master客观下线的标识会被移除;若是master重新向哨兵的PING命令回复了客观下线的标识也会被移除。

选举算法

当master被认为客观下线后,又是怎么进行故障恢复的呢?原来哨兵中首先选举出一个老大哨兵来进行故障恢复,

选举master哨兵的算法叫做Raft算法
  1. 发现master下线的哨兵(sentinelA)会向其它的哨兵发送命令进行拉票,要求选择自己为哨兵大佬。
  2. 若是目标哨兵没有选择其它的哨兵,就会选择该哨兵(sentinelA)为大佬。
  3. 若是选择sentinelA的哨兵超过半数(半数原则),该大佬非sentinelA莫属。
  4. 如果有多个哨兵同时竞选,并且可能存在票数一致的情况,就会等待下次的一个随机时间再次发起竞选请求,进行新的一轮投票,直到大佬被选出来。
  • 选出master哨兵后,master哨兵就会对故障进行自动回复,从slave中选出一名slave作为主数据库
  1. 所有的slave中slave-priority优先级最高的会被选中。
  2. 若是优先级相同,会选择偏移量最大的,因为偏移量记录着数据的复制的增量,越大表示数据越完整。
  3. 若是以上两者都相同,选择ID最小的。

集群Cluster模式

集群模式实现了Redis数据的分布式存储,实现数据的分片,每个redis节点存储不同的内容,并且解决了在线的节点收缩(下线)和扩容(上线)问题。

虚拟槽分区算法

在Redis集群中采用的使虚拟槽分区算法,会把redis集群分成16384 个槽(0 -16383)。

  • 当客户端请求过来,会首先通过对key进行CRC16 校验并对 16384 取模(CRC16(key)%16383)计算出key所在的槽,然后再到对应的槽上进行取数据或者存数据,这样就实现了数据的访问更新。
节点通信

节点之间实现了将数据进行分片存储,那么节点之间又是怎么通信的呢?这个和前面哨兵模式讲的命令基本一样。

  • 新上线的节点,会通过 Gossip 协议向老成员发送Meet消息,表示自己是新加入的成员。
  • 老成员收到Meet消息后,在没有故障的情况下会恢复PONG消息,表示欢迎新结点的加入,除了第一次发送Meet消息后,之后都会发送定期PING消息,实现节点之间的通信。
数据请求

在Redis的底层维护了unsigned char myslots[CLUSTER_SLOTS/8] 一个数组存放每个节点的槽信息。这样数组只表示自己是否存储对应的槽数据,若是1表示存在该数据,0表示不存在该数据,这样查询的效率就会非常的高,类似于布隆过滤器,二进制存储。并且,每个redis底层还维护了一个clusterNode数组,大小也是16384,用于储存负责对应槽的节点的ip、端口等信息,这样每一个节点就维护了其它节点的元数据信息,便于及时的找到对应的节点。

扩容和收缩

扩容和收缩也就是节点的上线和下线,可能节点发生故障了,故障自动回复的过程(节点收缩)。

  • 节点的收缩和扩容时,会重新计算每一个节点负责的槽范围,并发根据虚拟槽算法,将对应的数据更新到对应的节点。
  • 发生故障后,哨兵老大节点的选举,master节点的重新选举,slave怎样晋升为master节点,可以查看前面哨兵模式选举过程。
虚拟哈希槽分区

Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,每个 key 通过 CRC16 校验后对 16384 取模来决定放置哪个槽(slot),每一个节点负责维护一部分槽以及槽所映射的键值数据。

一致性哈希算法原理

一致性 Hash 算法使用取模的方法,对 2 的 32 方取模。即,一致性 Hash 算法将整个 Hash 空间组织成一个虚拟的圆环,Hash 函数的值空间为 0~2^32-1(一个 32 位无符号整型)

slot 原理

Redis Cluster 中有一个 16384 长度的槽的概念,它们的编号为 0~16383。这个槽是一个虚拟的槽,并不是真正存在的。正常工作的时候,Redis Cluster 中的每个 master 节点都会负责一部分的槽,当有某个 key 被映射到某个 master 负责的槽,那么这个 master 负责为这个 key 提供服务,至于哪个 master 节点负责哪个槽,这是可以由用户指定的,也可以在初始化的时候自动生成。每个槽映射一个数据子集,一般比节点数大。

  • Redis Cluster 是服务端管理节点、槽、数据,值得一提的是只有 master 节点才能分配槽。通过哈希算法再加上取模运算可以将一个值固定地映射到某个区间,在这里,这个区间叫做 slots,区间由连续的 slot 组成。在 Redis Cluster 中,我们拥有 16384 个 slot,这个数是固定的,我们存储在 Redis Cluster 中的所有的键都会被映射到这些 slot 中。
1
HASH_SLOT = CRC16(key) mod 16384

Redis6.0

  • 网络处理多IO线程
    • 随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 IO 的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度。采用多个 IO 线程来处理网络请求,提高网络请求处理的并行度。Redis 6.0 就是采用的这种方法。对于读写命令,Redis 仍然使用单线程来处理。

  • 客户端缓存、细粒度的权限设计
    • Redis 6.0 新增了一个重要的特性,就是实现了服务端协助的客户端缓存功能,也称为跟踪(Tracking)功能。有了这个功能,业务应用中的 Redis 客户端就可以把读取的数据缓存在业务应用本地了。
    • 6.0 实现的 Tracking 功能实现了两种模式
      • 第一种模式是普通模式。在这个模式下,实例会在服务端记录客户端读取过的 key,并监测 key 是否有修改。一旦 key 的值发生变化,服务端会给客户端发送 invalidate 消息,通知客户端缓存失效了。
      • 第二种模式是广播模式。在这个模式下,服务端会给客户端广播所有 key 的失效情况,不过,这样做了之后,如果 key 被频繁修改,服务端会发送大量的失效广播消息,这就会消耗大量的网络带宽资源。和普通模式不同,在广播模式下,即使客户端还没有读取过 key,但只要它注册了要跟踪的 key,服务端都会把 key 失效消息通知给这个客户端。
    • 在 Redis 6.0 版本之前,要想实现实例的安全访问,只能通过设置密码来控制,例如,客户端连接实例前需要输入密码。
      • 对于一些高风险的命令(例如 KEYS、FLUSHDB、FLUSHALL 等),在 Redis 6.0 之前,我们也只能通过 rename-command 来重新命名这些命令,避免客户端直接调用。
  • RESP 3 协议
    • edis 6.0 实现了 RESP 3 通信协议,而之前都是使用的 RESP 2。在 RESP 2 中,客户端和服务器端的通信内容都是以字节数组形式进行编码的,客户端需要根据操作的命令或是数据类型自行对传输的数据进行解码,增加了客户端开发复杂度。

扩展

缓存热key

什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。

  • 如果某一热点key的请求到服务器主机时,由于请求量特别大,可能会导致主机资源不足,甚至宕机,从而影响正常的服务。
    • Redis集群扩容:增加分片副本,均衡读流量;
    • 使用二级缓存,即JVM本地缓存,减少Redis的读请求。

缓存带来的问题

缓存雪崩
  • 我们可以简单的理解为:由于原有缓存失效,新缓存未到期间(例如:我们设置缓存时采用了相同的过期时间,在同一时刻出现大面积的缓存过期),所有原本应该访问缓存的请求都去查询数据库了,而对数据库CPU和内存造成巨大压力,严重的会造成数据库宕机。从而形成一系列连锁反应,造成整个系统崩溃。

  • 解决办法:

    1. 大多数系统设计者考虑用加锁( 最多的解决方案)或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。

    2. 将缓存失效时间分散开。

    3. 服务层增加熔断、降级、限流等操作,防止系统被流量攻溃。

    4. 增加redis的监控措施,并采用高可用架构处理。

缓存穿透

  • 缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
  • 解决办法
  1. 缓存空对象:代码维护较简单,但是效果不好。
  2. 布隆过滤器:代码维护复杂,空间效率和查询时间都远远超过一般的算法,效果很好。有一定的误识别率,删除困难。
  3. 请求参数检验:防止恶意请求进入业务系统,将部分恶意请求拦截掉。
  4. 过载保护:对Redis的流量进行限制,放置过大流量进入Redis导致崩溃。
  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。

    • 就是引入了k(k>1)k(k>1)个相互独立的哈希函数,保证在给定的空间、误判率下,完成元素判重的过程。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。Hash存在一个冲突(碰撞)的问题,用同一个Hash得到的两个URL的值有可能相同。为了减少冲突,我们可以多引入几个Hash,如果通过其中的一个Hash值我们得出某元素不在集合中,那么该元素肯定不在集合中。只有在所有的Hash函数告诉我们该元素在集合中时,才能确定该元素存在于集合中。这便是Bloom-Filter的基本思想。Bloom-Filter一般用于在大数据量的集合中判定某元素是否存在。
  • 缓存空对象,一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴。

布隆过滤器

布隆过滤器是一种基于概率数据结构,主要用来判断某个元素是否在集合内,它具有运行速度快(时间效率),占用内存小的优点(空间效率),但是有一定的误识别率删除困难的问题。它只能告诉你某个元素一定不在集合内或可能在集合内。

  1. 一个非常大的二进制位数组 (数组里只有0和1)
  2. 若干个哈希函数
  3. 空间效率查询效率高
  4. 不存在漏报(False Negative):某个元素在某个集合中,肯定能报出来。
  5. 可能存在误报(False Positive):某个元素不在某个集合中,可能也被爆出来。
  6. 不提供删除方法,代码维护困难。
  7. 位数组初始化都为0,它不存元素的具体值,当元素经过哈希函数哈希后的值(也就是数组下标)对应的数组位置值改为1。
布隆过滤器案例

当查询一件商品的缓存信息,布隆过滤器只能精确判断数据不存在情况,对于存在我们只能说是可能,因为存在Hash冲突情况,当然这个概率非常低。

  1. 当查询一件商品的缓存信息
  2. 然后,在布隆数组中查找访问对应的位值,0或1
  3. 判断,三个值中,只要有一个不是1,那么我们认为数据是不存在的。
布隆过滤器删除问题

直接删除肯定不行,存在hash冲突,存在误删。

  • 定时任务,定期创建一个新的布隆过滤器。
如何减少布隆过滤器的误判
  • 增加二进制位数组的长度。这样经过hash后数据会更加的离散化,出现冲突的概率会大大降低。
  • 增加Hash的次数,变相的增加数据特征。特征数据越多,冲突的概率越小。
缓存击穿

缓存击穿是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,瞬间对数据库的访问压力增大。

  • 当用户出现大并发访问的时候,在查询缓存的时候和查询数据库的过程加锁,只能第一个进来的请求进行执行,当第一个请求把该数据放进缓存中,接下来的访问就会直接集中缓存,防止了缓存击穿。即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁单机的话用普通的锁(synchronizedLock)就够了。
    • 使用互斥锁方案缓存失效时,不是立即去加载db数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载db数据库数据和设置缓存。否则就去重试获取缓存。
    • 持续不过期,通过定时任务的方式同步数据到缓存中。防止热key数据过期。
缓存预热
  • 缓存预热这个应该是一个比较常见的概念,缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据!
缓存更新
  • 除了缓存服务器自带的缓存失效策略之外(Redis默认的有6中策略可供选择),我们还可以根据具体的业务需求进行自定义的缓存淘汰,常见的策略有两种:
    • 定时去清理过期的缓存;
    • 当有用户请求过来时,再判断这个请求所用到的缓存是否过期,过期的话就去底层系统得到新数据并更新缓存。
  • 两者各有优劣,第一种的缺点是维护大量缓存的key是比较麻烦的,第二种的缺点就是每次用户请求过来都要判断缓存失效,逻辑相对比较复杂!具体用哪种方案,大家可以根据自己的应用场景来权衡。
缓存降级
  • 当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,仍然需要保证服务还是可用的,即使是有损服务。系统可以根据一些关键数据进行自动降级,也可以配置开关实现人工降级。
    • 以参考日志级别设置预案:
      • 一般:比如有些服务偶尔因为网络抖动或者服务正在上线而超时,可以自动降级;
      • 警告:有些服务在一段时间内成功率有波动(如在95~100%之间),可以自动降级或人工降级,并发送告警;
      • 错误:比如可用率低于90%,或者数据库连接池被打爆了,或者访问量突然猛增到系统能承受的最大阀值,此时可以根据情况自动降级或者人工降级;
      • 严重错误:比如因为特殊原因数据错误了,此时需要紧急人工降级。
  • 服务降级的目的,是为了防止Redis服务故障,导致数据库跟着一起发生雪崩问题。因此,对于不重要的缓存数据,可以采取服务降级策略,例如一个比较常见的做法就是,Redis出现问题,不去数据库查询,而是直接返回默认值给用户。

数据一致性

缓存一致性问题作为日常开发长期存在的问题。

  • 集中式远程缓存(单节点)
  • 集群式远程缓存
  • 集中式远程缓存+本地缓存(更新频率高采用远程缓存,更新频率低才用本地缓存)
    • 广播更新策略
    • 定时更新策略
  • 集群式远程缓存+本地缓存(更新频率高采用远程缓存,更新频率低才用本地缓存)
    • 广播更新策略
    • 定时更新策略

缓存常见的操作
  • 写入:缓存和数据库是两个不同的组件,只要涉及双写,就存在只有一个写成功的可能性,造成数据不一致。

  • 更新:更新的情况类似,需要更新两个不同的组件。

  • 读取:读取要保证从缓存中读到的信息是最新的,是和数据库中的是一致的。

  • 删除:当删除数据库记录的时候,如何把缓存中的数据也删掉?

同步直写
  • 写数据库的时候也写入缓存,通过代码检查机制使数据库和缓存数据统一。通常会采用事务、重试机制来保证数据。
延时双删
  • 先更新再删除,但是执行删除动作,在不久之后再执行一次,比如 1-5 秒之后。
集中更新
  • 弱化数据库,将数据优先存放到redis总,在通过定时器或扫描逻辑代码将数据同步到mysql中。
触发式加载

使用懒加载的方式,可以让缓存的同步变得非常简单

  • 当读取缓存的时候,如果缓存里没有相关数据,则执行相关的业务逻辑,构造缓存数据存入到缓存系统;
  • 当与缓存项相关的资源有变动,则先删除相应的缓存项,然后再对资源进行更新,这个时候,即使是资源更新失败,也是没有问题的。
可执行方案
  1. 通过Redis缓存策略过期时间更新缓存,对于访问量非常小的数据可以采用该方式。

  2. 更新数据库的后更新缓存(设置过期时间)

  3. 更新数据库后通过消息列队去处理Redis更新。(需要保证消息的顺序)

  4. 如果是Mysql可以定位binlog(需要单独单肩服务)

Redis命令是如何执行的

一条命令的执行过程有很多细节,但大体可分为:客户端先将用户输入的命令,转化为 Redis 相关的通讯协议,再用 socket 连接的方式将内容发送给服务器端,服务器端在接收到相关内容之后,先将内容转化为具体的执行命令,再判断用户授权信息和其他相关信息,当验证通过之后会执行最终命令,命令执行完之后,会进行相关的信息记录和数据统计,然后再把执行结果发送给客户端,这样一条命令的执行流程就结束了。

  • 用户输入一条命令
  • Redis客户端将命令转换成Redis的协议(RESP,REdis Serialization Protocol),然后在通过socket连接发送给服务器端。
    • Redis使用空格符号来确定完成的命令长度。
  • Redis Server接受到命令,会到输入缓冲中读取数据,判断数据是否超过最大限制(默认是是1G),如果超过了则返回异常信息。
  • Redis Server 在执行命令之前会做各种校验,命令是否异常、权限、集群相关、最大内存、持久化检查、只读检查、客户端订阅检查、从节点状态检查、是否初始化检查、lua脚本阻塞、事物、监视器。最后才会执行命令。调用 redisCommand 中的 proc 函数执行命令
  • 执行完成之后统计,检查慢查询是否开启、检查统计信息是否开启、检查持久化是否开启(开启则记录信息)、检查是否有slave复制当前server(存在则同步命令到其他服务器)
  • 最后在通过soket返回客户端结果。

单线程的Redis为什么这么快

  • 存内存操作。
  • 单线程操作,避免平凡的上下文切换。
  • 采用了非阻塞I/O多路复用操作。(这里的I/O是指网络I/O)
    • 这里的多路是指多个网络连接。复用复用同一个线程,采用多路I/O复用技术可以让单个线程高效处理多个连接请求。这样避免了线程创建销毁带来的资源消耗。

为什么Redis的操作是原子性的,怎么保证原子性的?

  • 对于Redis而言,命令的原子性指的是:一个操作的不可以再分,操作要么执行,要么不执行。Redis的操作之所以是原子性的,是因为Redis是单线程的。
  • Redis本身提供的所有API都是原子操作,Redis中的事务其实是要保证批量操作的原子性。
  • 使用Redis的事务,或者使用Redis+Lua的方式实现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 客户端限流操作
//获取ip对应的访问次数
current = GET(ip)
//如果超过访问次数超过20次,则报错
IF current != NULL AND current > 20 THEN
ERROR "exceed 20 accesses per second"
ELSE
//如果访问次数不足20次,增加一次访问计数
value = INCR(ip)
//如果是第一次访问,将键值对的过期时间设置为60s后
IF value == 1 THEN
EXPIRE(ip,60)
END
//执行其他操作
DO THINGS
END

大量key短时间内同时过期

  • 如果大量的key集中在某一个时间过期,会导致redis卡顿现象,所以在设置过期时间的时候最好加上一个随机数,让的key过期时间分散一点。

Keys命令

  • keys命令可以扫出指定的key列表,但是keys命令会导致线程柱塞,线上服务会停顿,直到指令执行完毕,服务才能恢复。建议使用scan命令来代替使用。scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。但是对于 SCAN 这类增量式迭代命令来说, 因为在对键进行增量式迭代的过程中, 键可能会被修改, 所以增量式迭代命令只能对被返回的元素提供有限的保证 。

Redis为何是 16384 个槽

Redis Cluster 为什么是 16384 个槽,16384 是 2^14 的结果

  • 作者怎么说
1
2
3
4
5
The reason is:

Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.