数据库

数据库键空间

  • Redis是一个key-value形式的数据服务器,服务器中的每个数据库都是一个redis.h/redisDb结构表示。redisDb中的dict字典属性记录了数据库中的所有键值对,这个字典称为键空间。
    typedef struct redisDb {
    
        // ...
    
        // 数据库键空间,保存着数据库中的所有键值对
        dict *dict;
    
        // ...
    
    } redisDb;
    
  • 键空间中的键就是数据库的键,每个键都是一个字符串对象
  • 键空间中的值就是数据库的值,每个值都可以是Redis五大基本对象中的一种。
  • 举例
    • 在空白数据库中执行命令
      redis> SET message "hello world"
      OK
      
      redis> RPUSH alphabet "a" "b" "c"
      (integer) 3
      
      redis> HSET book name "Redis in Action"
      (integer) 1
      
      redis> HSET book author "Josiah L. Carlson"
      (integer) 1
      
      redis> HSET book publisher "Manning"
      (integer) 1
      
    • 执行完成后数据库的键空间结构如下
      数据库键空间
  • 因数据库的键空间是一个字典结构,所有针对数据库增删改查操作都是对其中的字典进行操作实现。

添加新键

  • 添加新的键值对到数据库中,本质就是对数据库的字典进行新增操作,其中键为字符串对象,值则为Redis五大对象之一。
  • 示例
    • 执行命令
      redis> SET date "2013.12.1"
      OK
      
    • 执行完成后数据库键空间结构图
      数据库新增

删除键

  • 本质就是删除数据库键空间里的键值对对象
  • 示例
    • 执行命令
      redis> DEL book
      (integer) 1
      
    • 执行完成后数据库键空间结构图
      数据库删除

更新键

  • 本质就是对数据库键空间的键值对对象进行更新,值对象不同,更新的具体方法也不同
  • 示例
    • 执行命令(值为字符串对象)
      redis> SET message "blah blah"
      OK
      
    • 执行完成后数据库键空间结构图
      数据库更新
    • 执行命令(值为哈希对象)
      redis> HSET book page 320
      (integer) 1
      
    • 执行完成后数据库键空间结构图
      数据库更新

对键查值

  • 根据数据库键空间的键取出所对应的值对象,根据值不同,具体取值方法也不同。
  • 示例
    • 执行命令(字符串对象)
      redis> GET message
      "hello world"
      
    • 执行完成后数据库键空间结构图
      数据库查找
    • 执行命令(列表对象)
      redis> LRANGE alphabet 0 -1
      1) "a"
      2) "b"
      3) "c"
      
    • 执行完成后数据库键空间结构图
      数据库查找

其他键空间操作

  • 除了以上的增删改查操作,还有一些基于Redis数据库本身的操作。
  • FLUSHDB :清空数据库,通过删除键空间中所有键值对实现。
  • RANDOMKEY :随机返回数据库中某个键,通过在键空间中随机返回一个键实现。
  • DBSIZE :返回数据库键数,通过返回键空间中包含键值对的数量来实现。
  • EXISTSRENAMEKEYS等。

读写键空间时的维护操作

当使用Redis命令对数据库进行读写操作的时候,服务器还会执行一些额外的维护操作:

  • 在读取一个键之后(读操作和写操作都要对键进行读取),服务器会根据键是否存在,以此来更新服务器的键空间命中(hit)次数或键空间不命中(miss)次数,这两个值可以在 INFO stats 命令的 keyspace_hits 属性和 keyspace_misses 属性中查看。
  • 在读取一个键之后,服务器会更新键的 LRU (最后一次使用)时间,这个值可以用于计算键的闲置时间,使用命令 OBJECT idletime 命令可以查看键 key 的闲置时间。
  • 如果服务器在读取一个键时,发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作,本章稍后对过期键的讨论会详细说明这一点。
  • 如果有客户端使用 WATCH 命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过,《事务》一章会详细说明这一点。
  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增一,这个计数器会触发服务器的持久化以及复制操作执行,《RDB 持久化》、《AOF 持久化》和《复制》这三章都会说到这一点。
  • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送相应的数据库通知,本章稍后讨论数据库通知功能的实现时会详细说明这一点。

重点回顾

  • Redis服务器的所有数据库都保存在redisServer.db数组中,数据库的数量由redisServer.dbnum保存。
  • 客户端通过修改目标数据库指针,让他指向redisServer.db数组中不同的元素实现切换数据库。
  • 数据库主要由dictexpires两个字典构成,dict用于保存键空间里的键值对,expires保存键的过期时间。
  • 数据库由字典构成,所有对数据库的操作,实际都是在对字典进行操作。
  • 数据库的键总是一个字符串对象,值可以是Redis五大对象之一。
  • expires字典键指向数据库中的键,值则为键的过期时间,以毫秒为单位的时间戳。
  • Redis使用惰性删除和定期删除策略来删除过期键:惰性删除碰到过期键才删除,定期删除则每隔一定时间,主动查找过期键删除。
  • 执行SAVEBGSAVE命令,产生的新的RDB文件不会存在过期键。
  • 执行BGREWRITEAOF命令,产生的重新AOF文件不存在过期键。
  • 当一个过期键被删除之后,服务器会追加一条DEL命令到现有的AOF文件末尾,显示的删除过期键。
  • 从服务器即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当 Redis 命令对数据库进行修改之后,服务器会根据配置,向客户端发送数据库通知。

持久化机制

  • Redis持久化有两种策略
    • RDB 快照
    • AOF 日志

RDB快照

  • RDB持久化就是将Redis在内存中的数据库状态保存到磁盘里,以RDB持久化生成的RDB二进制文件。通过该文件可以还原生成RDB文件时候的数据库状态。
    RDB快照

创建RDB文件

  • 执行SAVEBGSAVE 命令:
    • SAVE会堵塞Redis服务器进程,一直到RDB文件生成完毕,期间服务器无法进行任何操作
    • BGSAVE:创建了一个子进程负责生成RDB文件,主进程继续处理命令

加载RDB文件

  • RDB文件在服务器启动的时候自动加载,在加载的时候Redis服务器会被堵塞。如果服务器开启了AOF持久化,那么服务器在启动的时候优先使用AOF还原数据库状态。

AOF日志

  • AOF通过保存Redis服务器执行的写命令来记录服务器状态。
  • AOF持久化功能的实现可以分为命令追加、文件写入、文件同步三个步骤

创建AOF日志

  • 当AOF持久化功能打开的时候,服务器在执行完写命令之后,会以协议格式将被执行的写命令追加到服务器状态的aof_buf缓冲区末尾。
    struct redisServer {
    
        // ...
    
        // AOF 缓冲区
        sds aof_buf;
    
        // ...
    };
    
  • 示例
    • 执行写命令
      redis> SET KEY VALUE
      OK
      
    • 服务器在执行命令之后,将以下协议内容追加到 aof_buf 缓冲区的末尾:
      *3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
      

AOF文件的写入与同步

  • 文件写入:为了提高文件的写入效率,在实际操作中,当用户调用write函数,将数据写入文件的时候,系统会暂时保存在内存缓存区里,
  • 文件同步:等缓存区被填满或者由系统主动操作的时候才会真的将缓存区里的数据写入磁盘。
    注意:这种做法虽然高效,但是对写入的数据存在一定安全隐患,因为如果计算机出现停机,那么在缓存区中的数据将会丢失。
  • 服务器每次结束一个事件循环之前,都会调用flushAppendOnlyFile函数,考虑是否要把aof_buf缓存区里的内容写入到AOF日志里。
def eventLoop():
    
    while True:
    
        # 处理文件事件,接收命令请求以及发送命令回复
        # 处理命令请求时可能会有新内容被追加到 aof_buf 缓冲区中
        processFileEvents()
    
        # 处理时间事件
        processTimeEvents()
    
        # 考虑是否要将 aof_buf 中的内容写入和保存到 AOF 文件里面
        flushAppendOnlyFile()
  • flushAppendOnlyFile值由服务器配置的appendfsync选项的值来决定,不同的值产生不同的行为

    appendfsync选项值 flushAppendOnlyFile函数行为
    always aof_buf缓冲区中的所有内容写入并同步到AOF文件。
    everysec aof_buf缓冲区中的所有内容写入到AOF文件,如果上次同步AOF文件的时间距离现在超过一秒钟,再次同步AOF文件,由专门的线程执行。
    no aof_buf缓冲区中的所有内容写入到AOF文件,但不对AOF文件进行同步,同步时机由操作系统决定。
  • appendfsync默认为everysec

  • 系统提供fsyncfdatasync 两个同步函数,可以强制将缓存区中的数据写入到磁盘,确保数据安全。

  • 示例:

    • 执行命令
      SADD databases "Redis" "MongoDB" "MariaDB"
      SET date "2013-9-5"
      INCR click_counter 10086
      
    • 命令转换成协议追加到aof_buf缓存区
      *5\r\n$4\r\nSADD\r\n$9\r\ndatabases\r\n$5\r\nRedis\r\n$7\r\nMongoDB\r\n$7\r\nMariaDB\r\n
      *3\r\n$3\r\nSET\r\n$4\r\ndate\r\n$8\r\n2013-9-5\r\n
      *3\r\n$4\r\nINCR\r\n$13\r\nclick_counter\r\n$5\r\n10086\r\n
      
    • 执行flushAppendOnlyFile函数,查看appendfsync属性,若为everysec,则判断距离上次同步AOF文件是否超过1秒。
    • 如超过则把aof_buf中的内容写如到AOF文件中,再对AOF文件进行同步。
  • 安全性分析:

    1. appendfsync值为always:服务器每次事件循环都要将aof_buf缓存区的数据写入AOF文件,同步AOF文件。效率最低,但是安全性最高,即使服务器停机,也只损失循环中的一点点命令数据。
    2. appendfsync值为everysec:服务器也是每次事件循环都要将aof_buf缓存区中的数据写入AOF文件,每隔一秒事件就要在子线程中对AOF文件进行一次同步。效率相对还行,安全性也还行,即使服务器停机,也只损失一秒的命令数据。
    3. appendfsync值为no:服务器也是每次事件循环都要将aof_buf缓存区中的数据写入AOF文件,但是同步交由系统控制。效率最高,但是安全性很低,如果长时间没有执行AOF同步,服务器停机,损失的数据量就会很大。

AOF重写与后台重写

什么是AOF重写

因为AOF持久化是通过保存被执行的写命令来记录数据库状态,随着服务器的运行,AOF文件内容会越来越多,文件体积会越来越大。

  • 体积过大的AOF文件会对Redis服务器造成影响
  • 使用AOF进行还原数据所用的事件越多。

Redis服务器会选择一个事件节点 创建一个新的AOF文件来代替旧的AOF文件,AOF文件的所保存的数据库状态相同,但是不会有任何冗余的命令,所以新AOF文件要比旧AOF文件小很多。

如何实现AOF重写(aof_rewrite 函数)

仅需从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前的多条命令记录键值对。无需读取现有的AOF文件。

什么是AOF后台重写

AOF重写会进行大量写入操作,调用该函数的线程很可能会出现堵塞,为了避免堵塞,Redis决定分配一个子进程给AOF重写。这样可以达到两个目的:

  1. 主服务器还可以继续处理命令请求
  2. 子进程拥有服务器进程的数据副本,使用进程而不是线程,很好的避免了在使用锁的情况下,保证数据的安全性,因为线程会共享父进程的资源。
  • 使用子进程带来的问题:
    • 子进程在进行AOF重写期间,服务器还在处理命令请求,新的命令可能会对现有的数据库进行修改,那么子进程所重写的AOF文件就有可能和数据库状态不一样。
    • 解决方案:
      1. 设置一个AOF重写缓存区,在服务器进行命令操作后,同时把命令发送给AOF缓存区和AOF重写缓存区。
        AOF重写缓存区
      2. 接着再重写完成的时候,再把AOF重写缓存区中的内容写入到新的AOF文件中。
      3. 最后原子性的对AOF文件进行重命名,覆盖掉现有的AOF文件。

RDB和AOF的使用场景

RDB优劣势

  • 优势:Redis只包含一个快照文件,方便备份,比如一天归档一次
  • 劣势:在服务器故障时,会丢失大量数据

AOF优劣势

  • 优势:Redis持久性会比较好,写操作由协议组成,非常容易解析。
  • 劣势:AOF文件体积较大。

fork函数耗时问题

在主进程创建子进程的时候会调用fork函数。

fork执行时,主进程要拷贝自己的页表给子进程,如果数据量很大,那就要占用大量的资源,在完成拷贝之前,整个Redis会被堵塞,无法进行任何操作。

优化方式:

  • 控制Redis整体内存
  • 合理的进行持久化操作
  • 不要部署在虚拟机上

事件

Redis是一个事件驱动程序有一下两种事件:

  1. 文件事件
  2. 时间事件

Redis对事件的处理都是同步、有序、原子的,不会出现中断事件、事件抢占

文件事件

什么是文件事件:服务器与客户端产生的通信,服务器通过监听和处理这些事件实现网络通信。

Redis基于Reactor模式开发了自己的文件事件处理器。是单线程的运行的。

单线程如何监听大量客户端

文件处理器采用I/O多路复用程序来监听客户端的大量连接

Redis 6.0 以后为什么又引入了多线程

需要提高网络IO的读写性能,但是在执行命令的时候依然是单线程顺序执行。

时间事件

什么是时间事件:Redis的一些操作需要在指定的时间点执行。

分为两类:

  • 定时事件
  • 周期性事件