Redis学习(三)Redis数据库

目录

服务器/客户端

数据库键空间

过期键

总结

服务器/客户端

Redis主要包括两种工作状态,服务器 redisServer 对象结构和客户端 RedisClient 对象结构,服务器将所有的数据库都保存在其成员的一个db数组中,db数组的每一个元素都是一个 redisDb 的redis数据库结构,代表一个数据库。

struct redisServer {
    //...

    //保存服务器所有数据库的数组
    redisDb *db;
    
    //服务器的数据库数量
    int dbnum;                      

    //...
};

在初始化服务器时,程序会根据服务器的dbnum属性来决定创建多少个数据库,dbnum属性由服务器配置的database选项决定,默认情况下为16,所以Redis服务器默认会创建16个数据库。

每个Redis客户端都有自己的目标数据库,每当客户端执行数据库的读/写命令的时候,目标数据库就会成为这些命令的操作对象。默认情况下,Redis客户端的目标数据库是0号数据库(db[0]),客户端可通过执行SELECT 1命令切换到1号目标数据库。

typedef struct redisServer {
    //...

    //记录客户端正在使用的目标数据库(指向服务器db数组的某个元素)
    redisDb *db;
                     
    //...
} redisClient;

客户端的db属性记录了客户端当前的目标数据库,它是一个指向服务器中redisDb结构的指针,RedisClient.db 指针指向RedisServer.db数组中的其中一个元素,这个元素就是此客户端的目标数据库。而切换客户端的目标数据库其实就是修改它指向服务器db数组中元素的指针。

数据库键空间

Redis 是一个键值对(key-value pair)数据库服务器, 服务器中的每个数据库都由一个 redisDb 结构表示, 其中redisDb 结构的 dict 字典保存了数据库中的所有键值对, 我们将这个字典称为键空间(key space)。

typedef struct redisDb {
    // ...

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;

    // ...
} redisDb;

键空间和用户所见的数据库是直接对应的:

  • 键空间的键也就是数据库的键, 每个键都是一个字符串对象。
  • 键空间的值也就是数据库的值, 每个值可以是字符串对象、列表对象、哈希表对象、集合对象和有序集合对象在内的任意一种 Redis 对象。

digraph {      label = "\nå¾ IMAGE_DB_EXAMPLE    æ°æ®åºé®ç©ºé´ä¾å­";      rankdir = LR;      node [shape = record];      //      redisDb [label = "redisDb | ... | <dict> dict | ..."];      dict [label = "<dict> dict | <alphabet> StringObject \n \"alphabet\" | <book> StringObject \n \"book\" | <message> StringObject \n \"message\""];      subgraph cluster_alphabet {          a [label = " StringObject \n \"a\" "];         b [label = " StringObject \n \"b\" "];         c [label = " StringObject \n \"c\" "];          a -> b -> c;          label = "ListObject";      }      //alphabet [label = "<head> ListObject | { StringObject \n \"a\" | \"b\" | \"c\" }"];      book [label = "<head> HashObject | <name> StringObject \n \"name\" | <author> StringObject \n \"author\" | <publisher> StringObject \n \"publisher\""];      //name [label = " StringObject \n \"Redis in Action\""];     name [label = " StringObject \n \"Redis in Action\""];      author [label = " StringObject \n \"Josiah L. Carlson\""];      publisher [label = " StringObject \n \"Manning\""];      message [label = " StringObject \n \"hello world\""];      //      redisDb:dict -> dict:dict;      dict:alphabet -> a;     dict:book -> book:head;     dict:message -> message;      book:name -> name;     book:publisher -> publisher;     book:author -> author;  }

上图所示就是一个数据库键空间即一个dict字典,其中:

  • alphabet是一个列表键,键的名字是一个包含字符串 "alphabet" 的字符串对象,键的值则是一个包含三个元素的列表对象。
  • book 是一个哈希表键,键的名字是一个包含字符串 "book" 的字符串对象,键的值则是一个包含三个键值对的哈希表对象。
  • message 是一个字符串键, 键的名字是一个包含字符串 "message" 的字符串对象, 键的值则是一个包含字符串 "hello world" 的字符串对象。

数据库的键空间是一个字典, 所以所有针对数据库的操作 —— 比如添加一个键值对到数据库, 或者从数据库中删除一个键值对, 又或者在数据库中获取某个键值对, 等等, 实际上都是通过对键空间字典进行操作来实现的。

添加键

添加一个新键值对到数据库, 实际上就是将一个新键值对添加到键空间字典里面, 其中键为字符串对象, 而值则为任意一种类型的 Redis 对象。

如在键空间添加一个新的键值对,这个新键值对的键是一个包含字符串 "date" 的字符串对象, 而键值对的值则是一个包含字符串 "2013.12.1"的字符串对象,添加完成后数据库的结构如下:

删除键

删除数据库中的一个键, 实际上就是在键空间里面删除键所对应的键值对对象。

如DEL book表示删除上述数据库键空间中的book键所对应的键值对,结果如下:

digraph {      label = "\nå¾ IMAGE_DB_AFTER_DEL    å é¤ book é®ä¹åçé®ç©ºé´";      rankdir = LR;      node [shape = record];      //      redisDb [label = "redisDb | ... | <dict> dict | ..."];      dict [label = "<dict> dict | <alphabet> StringObject \n \"alphabet\" |  <message> StringObject \n \"message\""];      subgraph cluster_alphabet {          a [label = " StringObject \n \"a\" "];         b [label = " StringObject \n \"b\" "];         c [label = " StringObject \n \"c\" "];          a -> b -> c;          label = "ListObject";      }      message [label = " StringObject \n \"hello world\""];      //      redisDb:dict -> dict:dict;      dict:alphabet -> a;     dict:message -> message;  }

更新键

对一个数据库键进行更新, 实际上就是对键空间里面键所对应的值对象进行更新, 根据值对象的类型不同, 更新的具体方法也会有所不同。

对于字符串键: SET 键 新的值 ,对于哈希键 :HSET 键 新的值 等等。

对键取值

对一个数据库键进行取值, 实际上就是在键空间中取出键所对应的值对象, 根据值对象的类型不同, 具体的取值方法也会有所不同。

对于字符串键: GET 键,对于列表键的范围取值:LRANGE 键 0 -1,表示找到键之后接着取得该键所对应的列表对象值。

其他键操作

除了添加、删除、更新、取值操作之外,还有很多针对数据库本身的 Redis 命令 也是通过对键空间进行处理来完成的。

  • 用于清空整个数据库的 FLUSHDB 命令, 就是通过删除键空间中的所有键值对来实现的。
  • 用于随机返回数据库中某个键的 RANDOMKEY 命令, 就是通过在键空间中随机返回一个键来实现的。
  • 用于返回数据库键数量的 DBSIZE 命令, 就是通过返回键空间中包含键值对的数量来实现的。
  • 类似的命令还有 EXISTS 、 RENAME 、 KEYS , 等等, 这些命令都是通过对键空间进行操作来实现的。

读写键空间时的维护操作

当使用 Redis 命令对数据库进行读写时,服务器不仅会对键空间执行指定的读写操作,还会执行一些额外的维护操作,包括:

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

过期键

Redis作为一个缓存数据库,对内存的限制有着很高的要求。其具体实现为键的设置时间过期功能,即对存储在 redis 数据库中的值可以设置一个过期时间,到时自动删除。这对Redis的性能提升是非常有意义的。

设置过期时间

通过EXPIRE命令或者PEXPIRE命令,客户端可以以秒或者毫秒级的精度为数据库的某个键设置生存时间(Time To Live,TTL)在经过指定的秒数或者毫秒数之后,服务器就会自动删除生存时间为0的键。

与EXPIRE命令和PEXPIRE命令相似,客户端可以通过EXPIREAT命令或者PEXPIREAT命令,以秒或者毫秒精度为数据库中的某个键设置过期时间(expire time)。过期时间是一个UNIX时间戳,当过期时间来临时,服务器就会自动删除这个键。

通过EXPIRE命令或者PEXPIRE命令实现秒级或者毫秒级的生存时间的设置功能:

  • EXPIRE <KEY> <TTL> : 将键的生存时间设为 ttl 秒
  • PEXPIRE <KEY> <TTL> :将键的生存时间设为 ttl 毫秒
  • EXPIREAT <KEY> <timestamp> :将键的过期时间设为 timestamp 所指定的秒数时间戳
  • PEXPIREAT <KEY> <timestamp>: 将键的过期时间设为 timestamp 所指定的毫秒数时间戳.

虽然有多种不同单位和不同形式的设置命令,但实际上EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT四个命令内部都是调用同一个函数实现,无论客户端执行的上述四个命令的哪一个,最终的执行效果都和执行PEXPIREAT命令是一样的。事实上,生存时间和过期时间是一个意思,过期时间即为当前时间+生存时间

保存过期时间

数据库redisDb结构中的 expires 字典保存了数据库中所有键的过期时间,被称为过期字典

typedef struct redisDb {
    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;
    // 键的过期时间
    dict *expires;
    //...
} redisDb;
  • 过期字典的键是一个指针,指向键空间dict中的某个键对象(某个数据库键,5大类中一种)。
  • 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间(一个毫秒级的UNIX时间戳)。
  • 如果给定的键不存在于键空间,那么不能设置过期时间。

             

移除过期时间

  persist命令就是pexpireat命令的反操作:persist命令在过期字典中查找给定的键,并解除键值在过期字典中的关联。如果对上述键空间执行:persist message、persist book,其键空间变更为如下状态:

                 

过期键的判定

TTL或者PTTL命令可以返回秒级或者毫秒级别的键的剩余生存时间。

通过过期字典,程序可以通过以下步骤检查一个键是否过期:

  1. 检查给定键是否存在于过期字典中,如果存在则取出其过期时间;
  2. 检查当前UNIX时间戳是否大于键的过期时间,如果是则键过期执行过期删除;

这两种方法都可以进行过期判定,TTL命令返回值大于等于0,说明未过期,但实际中一般都采用后者,因为直接访问字典比执行一个命令要快。

过期键的删除策略

对于过期键的删除,有三种策略:定时删除、惰性删除定期删除。

定时删除:在设置键的过期时间时,创建一个定时事件,当过期时间到达时,由事件处理器自动执行键的删除操作。

定时删除策略对内存是最友好的: 因为它保证过期键会在第一时间被删除, 过期键所消耗的内存会立即被释放。

这种策略的缺点是, 它对 CPU 时间是最不友好的: 因为删除操作可能会占用大量的 CPU 时间 —— 在内存不紧张、但是 CPU 时间非常紧张的时候 (比如说,进行交集计算或排序的时候), 将 CPU 时间花在删除那些和当前任务无关的过期键上, 这种做法毫无疑问会是低效的。

惰性删除 :放任键过期不管,但是在每次从 dict 字典中取出键值时,要检查键是否过期,如果过期的话,就删除它,并返回空;如果没过期,就返回键值。

惰性删除对 CPU 时间来说是最友好的: 它只会在取出键时进行检查, 这可以保证删除操作只会在非做不可的情况下进行 —— 并且删除的目标仅限于当前处理的键, 这个策略不会在删除其他无关的过期键上花费任何 CPU 时间。

惰性删除的缺点是, 它对内存是最不友好的: 如果一个键已经过期, 而这个键又仍然保留在数据库中, 那么 dict 字典和 expires 字典都需要继续保存这个键的信息, 只要这个过期键不被删除, 它占用的内存就不会被释放。

定期删除:每隔一段时间,对 expires 字典进行检查,删除里面的过期键。

定期删除是这两种策略的一种折中

  • 它每隔一段时间执行一次删除操作,并通过限制删除操作执行的时长和频率,来减少删除操作对CPU时间的影响。
  • 另一方面,通过定期删除过期键,它有效地减少了因惰性删除而带来的内存浪费。

Redis 使用惰性删除定期删除两种策略来删除过期的键: 惰性删除策略只在碰到过期键时才进行删除操作, 定期删除策略则每隔一段时间, 主动查找并删除过期键。

Redis的这三种删除方式虽然可以在一定程度上避免内存的浪费和对CPU的影响,但仍然没有彻底的解决突发性的高并发问题;试想一下,如果很短的时间内,Redis的缓存中有巨大量数据新增(比如微博热搜、淘宝双11等),而且这些数据并没有超过限制的时间,超时命令机制无法删除它们,最终就会导致Redis内存块耗尽。解决这个问题,Redis采用的是内存淘汰机制。

Redis内存淘汰 (内存回收) 机制

我们可以通过配置redis.conf中的maxmemory这个值来开启内存淘汰功能,我们可以通过这个值来设置内存淘汰算法

  1. 客户端发起了需要申请更多内存的命令(如set)。
  2. Redis检查内存使用情况,如果已使用的内存大于maxmemory则开始根据用户配置的不同淘汰策略(就是配置的maxmemory这个值)来淘汰内存(key),从而换取一定的内存。
  3. 如果上面都没问题,则这个命令执行成功。

maxmemory为0的时候表示我们对Redis的内存使用没有限制。

Redis提供了下面几种淘汰策略供用户选择,其中默认的策略为noeviction策略:

  1. volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰(仅仅是超时的)
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰(剩余存活时间最短)
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(不仅仅是超时的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. noeviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,读正常进行,新写入操作会报错。

这些策略适用的场景:

  • allkeys-lru:如果我们的应用对缓存的访问符合幂律分布(也就是存在相对热点数据),或者我们不太清楚我们应用的缓存访问分布状况,我们可以选择allkeys-lru策略。
  • allkeys-random:如果我们的应用对于缓存key的访问概率相等,则可以使用这个策略。
  • volatile-ttl:这种策略使得我们可以向Redis提示哪些key更适合被eviction。

另外,对于过期键在持久化RBD、AOF和复制功能中的处理,后续Redis持久化之后的学习中会详细说明。

总结

  • Redis 服务器的所有数据库都保存在 redisServer.db 数组中, 而数据库的数量则由 redisServer.dbnum 属性保存。
  • 客户端通过修改目标数据库指针, 让它指向 redisServer.db 数组中的不同元素来切换不同的数据库。
  • 数据库主要由 dict 和 expires 两个字典构成, 其中 dict 字典负责保存键值对, 而 expires 字典则负责保存键的过期时间。
  • 因为数据库由字典构成, 所以对数据库的操作都是建立在字典操作之上的。
  • 数据库的键总是一个字符串对象, 而值则可以是任意一种 Redis 对象类型, 包括字符串对象、哈希表对象、集合对象、列表对象和有序集合对象, 分别对应字符串键、哈希表键、集合键、列表键和有序集合键。
  • expires 字典的键指向数据库中的某个键, 而值则记录了数据库键的过期时间, 过期时间是一个以毫秒为单位的 UNIX 时间戳。
  • Redis 使用惰性删除和定期删除两种策略来删除过期的键: 惰性删除策略只在碰到过期键时才进行删除操作, 定期删除策略则每隔一段时间, 主动查找并删除过期键。
  • 执行 SAVE 命令或者 BGSAVE 命令所产生的新 RDB 文件不会包含已经过期的键。
  • 执行 BGREWRITEAOF 命令所产生的重写 AOF 文件不会包含已经过期的键。
  • 当一个过期键被删除之后, 服务器会追加一条 DEL 命令到现有 AOF 文件的末尾, 显式地删除过期键。
  • 当主服务器删除一个过期键之后, 它会向所有从服务器发送一条 DEL 命令, 显式地删除过期键。
  • 从服务器即使发现过期键, 也不会自作主张地删除它, 而是等待主节点发来 DEL 命令, 这种统一、中心化的过期键删除策略可以保证主从服务器数据的一致性。
  • 当 Redis 命令对数据库进行修改之后, 服务器会根据配置, 向客户端发送数据库通知。

 

 

参考文章:

《Redis设计与实现》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值