Nivelle 开拓视野冲破艰险看见世界 身临其境贴近彼此感受生活

redis基础学习之数据库

2017-09-09

  • conten

数据库

服务器中的数据库

redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数组中,db数组的每个项都是一个redis.h/redisDb结构,每个redisDb结构代表一个数据库:


struct redisServer{
  //...
  //一个数组,保存着服务器中的所有数据库
  redisDb *db;
  //dbnum属性的值由服务器配置的database选项决定,默认情况下,该选项的值为多少,redis服务器默认会创建几个数据库;
  int dbnum;
};


typedef struct redisDb {

    // 保存着数据库以整数表示的号码
    int id;

    // 保存着数据库中的所有键值对数据
    // 这个属性也被称为键空间(key space)
    dict *dict;

    // 保存着键的过期信息
    dict *expires;

    // 实现列表阻塞原语,如 BLPOP
    // 在列表类型一章有详细的讨论
    dict *blocking_keys;
    dict *ready_keys;

    // 用于实现 WATCH 命令
    // 在事务章节有详细的讨论
    dict *watched_keys;

} redisDb;

切换数据库

每个redis客户端都有自己的目标数据库,每当客户端执行数据库写命令或数据库读命令的时候,目标数据库就会成为这些命令的操作对象.

默认情况下,redis客户端的目标数据库为0号数据库,但客户端可以通过执行select命令来切换数据库.


redis > SET msg "hello world"
OK

redis > GET msg
"hello world"

redis > select 2
OK

redis[2] > GET msg
(nil)

redis[2]> SET msg "another world"
ok

redis[2]>GET msg
"another world"


在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,

typedef struct redisClient{
  //...
  //记录客户端当前正在使用的数据库

  redisDb *db;

}redisClient;


数据库键空间

redis是一个键值对数据库服务器,服务器中的每个数据库都由一个redis.h/redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间:

typedef struct redisDb{
  //...
  //数据库键空间,保存着数据库中所有键值对
  dict *dict;
}redisDb;


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

  • 键空间的键也就是数据库的键,每个键都是一个字符串对象

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

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

添加新建

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

删除键

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

更新键

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

对键取值

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

读写键空间时的维护操作

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

  • 在读取一个键后(读操作和写操作都要对键进行读取),服务器会根据键是否存在来更新服务器的键空间命中(hit)次数货键空间不命中(misss)次数,这两个值可以在INFO stats命令的keyspace_hits属性和keyspace_misses属性中查看

  • 在读取一个键之后,服务器会更新键的LRU(最后一次使用)时间,这个值可以用于计算键的闲置时间,使用OBJECT idletime命令可以查看键key的闲置时间

  • 如果服务器在读取一个键时发现该键已经过期,那么服务器会先删除这个过期键,然后才执行余下的其他操作

  • 如果客户端使用WATCH命令监视了某个键,那么服务器在对被监视的键进行修改之后,会将这个键标记为脏(dirty),从而让事务程序注意到这个键已经被修改过

  • 服务器每次修改一个键之后,都会对脏(dirty)键计数器的值增1,这个计数器会触发服务器的持久化以及复制操作.

  • 如果服务器开启了数据库通知功能,那么在对键进行修改之后,服务器将按配置发送响应的数据库通知.

设置键的生存时间或过期时间

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


redis > set key value
ok

redis > EXPIRE key 5
(integer)1

redis > GET key//5秒之内
"value"

redis > GET key //5秒之后
(nil)


TTL命令和PTTL命令接受一个带有生存时间或者过期时间的键,返回这个键的剩余生存时间,也就是,返回距离这个键被服务器自动删除还有多长时间

设置过期时间

redis有四个不同的命令用于设置生存时间(键可存在多久)或过期时间(键什么时候被删除):

  • EXPIRE命令用于将键key的生存时间设置为ttl秒

  • PEXPIRE 命令用于将key的生存时间设置为ttl毫秒

  • EXPIREAT命令用于将键key的过期时间设置为timestamp所指定的秒数时间戳

  • PEXPIREAT 命令用于将键key的过期时间设置为timestamp所指定的毫秒数时间戳

无论上述四种名执行的是哪一个,经过转换,最终执行的效果都转换成PEXPIREAT命令一样.

首先,EXPIRE命令可以转换成PEXPIRE命令:

def EXPIRE (key,ttl_in_sec):

 // 将TTL从秒转换成毫秒
 ttl_in_ms = sec_to_ms(ttl_in_sec)

 PEXPIRE(key,ttl_in_ms)

 接着,PEXPIRE 命令又可以转换成PEXPIRAT 命令:

  def PEXPIRE(key,ttl_in_ms):
  
  //获取以毫秒计算的当前unix时间戳
  now_ms = get_current_unix_timestamp_in_ms()
  //当前时间加上TTL,得出毫秒格式的键过期时间
  PEXPIREAT(key,now_ms+ttl_in_ms)


并且,EXPIREAT命令也可以转换成PEXPIREAT命令:

def EXPIREAT(key,expire_time_in_sec):

//将过期时间从秒转换为毫秒
expire_time_in_ms = sec_to_ms(expire_time_in_sec)

PEXPIREAT(key,expire_time_in_ms)

保存过期时间

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

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即使某个数据库键)

  • 过期字典的值是一个long long 类型的整数,这个整数保存了键所指向的数据库键的过期时间:一个毫秒精度的UNIX时间戳

typedef struct redisDb{
  
  //...

  //过期字典,保存着键的过期时间

  dict *expires;

  //...
}redisDb


移除过期时间

PERSIST命令可以移除一个键的过期时间:

redis > PEXPIREAT message 1391234400000
(integer) 1

redis > TTL message
(integer) 13893281

redis > PERSIST message
(integer) 1

redis TTL message
(interger) -1

PERSIST命令就PEXPIREAT命令的反操作:在过期字典里查找给定的键,并解除键和和值在过期字典中的关联

计算并返回剩余生存时间

TTL命令以秒为单位返回键的剩余生存时间,而PTTL命令则以毫秒为单位返回键的剩余生存时间:

redis > PEXPIREAT alphabet 13385877600000
(integer)1

redis> TTL alphabet
(integer) 8549007

redis > PTTL alphabet
(integer) 8549001011


过期键的判断

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

  • 检查给定键是否存在于过期字典:如果存在,那么取得键的过期时间

  • 检查当前UNIX时间戳是否大于键的过期时间:如果是,那么键已经过期;否则,键未过期.

过期键的删除策略

  • 定时删除:在设置键的过期时间同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作.通过定时器,定时删除策略可以保证过期键会尽肯能地被删除,并释放过期键所占用的内存.对CPU不够友好,

  • 惰性删除:放任键过期不管,但每次从键空间获取键时,都检查取得的键是否过期,如果过期就删除该键;如果没有过期就返回该键.惰性删除策略的缺点是,它对内存时最不友好的:如果一个键已经过期,而这个键又在数据库中,那么只要这个过期键不被删除,它所占用的内存就不会释放.

  • 定期删除:每隔一段时间,程序就对数据库执行一次检查,删除里面的过期键.至于要删除多少过期键,以及要检查多少个数据,由算法决定.

    1.定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响

    2.除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键而来的内存浪费

redis过期键删除策略

redis服务器实际使用的是惰性删除和定期删除两种策略:通过配合使用这两种删除策略,服务器可以很好地合理使用CPU时间和避免浪费内存空间之间取得平衡.

  • 惰性删除策略实现

过期键的惰性删除策略由db.c/expireIfNeeded函数实现,所有读写数据库的redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查:

  1. 如果输入键已经过期,那么expireIfNeeded函数对输从数据库中删除

  2. 如果输入键未过期,那么expireIfNeeded函数不做动作

  • 定期删除策略的实现

过期键的删除由redis.c/activeExpireCycle函数实现,每当服务器调用redis.c/serverCron函数执行时,activeExpirCycle函数就会被调用,它在规定时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键.

过期键对 AOF 、RDB 和复制的影响

  • 更新后的 RDB 文件:在创建新的 RDB 文件时,程序会对键进行检查,过期的键不会被写入到更新后的 RDB 文件中。因此,过期键对更新后的 RDB 文件没有影响。
  • AOF 文件: 在键已经过期,但是还没有被惰性删除或者定期删除之前,这个键不会产生任何影响,AOF 文件也不会因为这个键而被修改。当过期键被惰性删除、或者定期删除之后,程序会向 AOF 文件追加一条 DEL 命令,来显式地记录该键已被删除。
  • AOF 重写: 和 RDB 文件类似, 当进行 AOF 重写时, 程序会对键进行检查, 过期的键不会被保存到重写后的 AOF 文件。因此,过期键对重写后的 AOF 文件没有影响。

  • 复制: 当服务器带有附属节点时, 过期键的删除由主节点统一控制:

(1):如果服务器是主节点,那么它在删除一个过期键之后,会显式地向所有附属节点发送一个 DEL 命令。

(2):如果服务器是附属节点,那么当它碰到一个过期键的时候,它会向程序返回键已过期的回复,但并不真正的删除过期键。因为程序只根据键是否已经过期、而不是键是否已经被删除来决定执行流程,所以这种处理并不影响命令的正确执行结果。当接到从主节点发来的 DEL 命令之后,附属节点才会真正的将过期键删除掉。

总结

  • 数据库主要由 dict 和 expires 两个字典构成,其中 dict 保存键值对,而 expires 则保存键的过期时间。
  • 数据库的键总是一个字符串对象,而值可以是任意一种 Redis 数据类型,包括字符串、哈希、集合、列表和有序集。
  • expires 的某个键和 dict 的某个键共同指向同一个字符串对象,而 expires 键的值则是该键以毫秒计算的 UNIX 过期时间戳。
  • Redis 使用惰性删除和定期删除两种策略来删除过期的键。
  • 更新后的 RDB 文件和重写后的 AOF 文件都不会保留已经过期的键。
  • 当一个过期键被删除之后,程序会追加一条新的 DEL 命令到现有 AOF 文件末尾。
  • 当主节点删除一个过期键之后,它会显式地发送一条 DEL 命令到所有附属节点。
  • 附属节点即使发现过期键,也不会自作主张地删除它,而是等待主节点发来 DEL 命令,这样可以保证主节点和附属节点的数据总是一致的。
  • 数据库的 dict 字典和 expires 字典的扩展策略和普通字典一样。它们的收缩策略是:当节点的填充百分比不足 10% 时,将可用节点数量减少至大于等于当前已用节点数量。

评论