数据库

  • 介绍Redis服务器的数据库实现,服务器保存数据库的方法
  • 客户端切换数据库的方法,数据库保存键值对的方法,数据库的增,删,改,查实现方法
  • 服务器保存键的过期时间的方法,服务器自动删除过期键的方法
  • 数据库通知功能实现方法

服务器中的数据库

  • redis服务器将所有数据库都保存到服务器状态redis.h/redisServer结构中的db数组中,db数组中的每一个项都是redis.h/redisDb结构,数据库的数量由dbnum属性决定,dbnum是由服务器配置的database选项决定,默认是16所以Redis服务器默认会创建16个数据库
struct redisServer{
  //...
  //db数组,保存着服务器中所有数据库
  redisDb *db;
  //服务器的数据库数量
  int dbnum;
  //...
};
服务器数据库

切换数据库

  • 默认情况,Redis客户端的目标数据库是0号数据库,客户端可以通过执行select 2命令来切换数据库到2号数据库
  • 在服务器内部,客户端状态redisClient结构的db属性记录了客户端当前的目标数据库,这个属性是一个指向redisDb结构的指针:
typedef struct redisClient{
  //...
  //记录客户端当前正在使用的数据库
  redisDb *db;
  //...
}redisClient;

redisClient.db指针指向redisServer.db数组中的其中一个元素,而被指向的元素就是客户端的目标数据库

比如某客户端的目标数据库是1号,执行了select 2,客户端和服务器状态之间的关系将更新,具体如下图所示:


客户端目标为1号数据库

客户端目标数据库为2号

selects实现的原理,就是通过修改redisClient.db的指针,让他指向不同的数据库

数据库键空间

  • 服务器中的数据库由redis.h/redisDb结构表示,其中redisDb结构的dict字典保存了数据库中的所有键值对,我们将这个字典称为键空间
typedef struct redisDb{
  //...
  //数据库的键空间,保存着数据库中所有键值对
  dict *dict;
  //...
}redisDb;

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

执行下面命令:

  • SET message "hello world"
  • RPUSH alphabet "a" "b" "c"
  • HSET book name "redis in action"
  • HSET book author "josiah l. carlson"
  • HSET book publisher "manning"
  • 对应的数据库键空间


    数据库键空间
  • 数据库的增删改查都是基于键空间来实现的

添加新键

  • SET data "2013.12.1"
    添加data后的键空间

删除键

  • DEL book
    删除book后的键空间

更新键

  • SET message "blah blah"
    更新message键

对键取值
GET message,就是首先在键空间中找到message,找到之后取得该键所对应的字符串对象值,然后返回值对象包含的字符串

  • 其他键空间操作

FLUSHDB清空整个数据库,通过删除键空间中所有的键值对
RANDOMKEY随机返回数据库中某个键,就是在键空间中随机返回某个键的
DBSIZE返回数据库键数量,返回键空间中包含的键值对的数量
EXISTS
RENAME
KEYS

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

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

SETEX这个命令在设置一个字符串键的同时为键设置过期时间,但只对字符串键有用

  • EXPIRE <KEY> <TTL> 将key生存时间设置为TTL秒
  • PEXPIRE <KEY> <TTL> 将key生存时间设置为TTL毫秒
  • EXPIREAT <KEY> <TIMESTAMP> 将key的过期时间设置为timestamp所指定的秒数时间戳
  • PEXPIREAT <KEY> <TIMESTAMP> 将key的过期时间设置为timestamp所指定的毫秒时间戳

最终执行过期的的命令其实都是PEXPIREAT命令来实现的,上面四个命令在经过转换之后,都会执行PEXPIREAT命令的

def EXPIRE(key,ttl_in_sec):
  ttl_in_ms=sec_to_ms(ttl_in_sec);//将ttl从秒转为毫秒
  PEXPIRE(key,ttl_in_ms);

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

def EXPIREAT(key,expire_time_in_sec):
  expire_time_in_ms=sec_to_ms(expire_time_in_sec);//将过期时间转化为毫秒
  PEXPIREAT(key,expire_time_in_ms);

设置生存时间和过期时间的命令转化
  • 保存过期时间
typedef struct redisDb{
  //..
  //过期字典,保存着键的过期时间
  dict *expires;
}redisDb;

过期字典的,键是一个指针,执行键空间某个键对象,值是一个long long类型的整数,保存了键所指向的数据库键的过期时间,一个毫秒精度的unix时间戳,实际中键空间的键和过期字典的键都指向同一个键对象,所以不存在浪费空间


带有过期字典的数据库
def PEXPIREAT(key,expire_time_in_ms):
  if key not in redisDb.dict: //如果键不在键空间中,就不能设置过期时间
    return 0;
  redisDb.expires[key]=expire_time_in_ms; //在过期字典中关联键和过期时间
  return 1; //过期时间设置成功
  • 移除过期时间
    PERSIST key可以移除过期时间,在过期字典中查找键,并解除键和值在过期字典中的关联
def PERSIST(key):
  if key not in redisDb.expires: //如果键不存在,或者键没有设置过期时间,直接返回
    return 0;
  redisDb.expires.remove(key); //移除过期字典中给定键的键值对关联
  return 1; //键的过期时间移除成功
  • 计算并返回剩余生存时间
    TTL key或者PTTL key命令,分别是以秒 和毫秒返回键的剩余生存时间,通过计算过期时间和当前时间差来实现
def PTTL(key):
  if key not in redisDb.dict: //键不在数据库中
    return -2;
  expire_time_in_ms=reidsDb.expires.get(key)//获取键的过期时间,如果没有设置过期时间,expire_time_in_ms就是None
  if expire_time_in_ms is None: //没有设置过期时间
    return -1;
  now_ms=get_current_unix_timestamp_in_ms() //获取当前时间
  return (expire_time_in_ms-now_ms) //过期时间-当前时间,就是键的剩余生存时间

def TTL(key):
  ttl_int_ms=PTTL(key) //获取以毫秒为单位的剩余生存时间
  if ttl_in_ms<0: //处理返回-1,-2的情况
    return ttl_in_ms;
  else:
    return ms_to_sec(ttl_in_ms); //将毫秒转换为秒
  • 过期键的判定
  1. 通过使用TTL或者PTTL来实现,如果返回的值大于等于0,说明没有过期
  2. 执行is_expired函数,Redis中判断是否过期,和is_expired的过程一样
  • 检查当前键是否存在于过期字典中,如果存在,取得过期时间
  • 检查当前unix时间戳是否大于键的过期时间,是,过期,否,没有过期
def is_expired(key):
  expire_time_in_ms=redisDb.expires.get(key) //取得键的过期时间
  if expire_time_in_ms is None: //键没有设置过期时间
    return false;
  now_ms=get_current_unix_timstamp_in_ms() //取得当前时间的unix时间戳
  if now_ms > expire_time_in_ms:  //检查当前时间是否大于键的过期时间
    return true;
  else:
    return false;

过期键删除策略

这个过期键什么时候被删除了,有三种策略可以选择

  1. 定时删除,设置键的同时,创建一个定时器,在过期时间来的时候,立即执行删除
  2. 惰性删除,每次从键空间获取键,都先检查这个键是否过期,过期,删除,没有,返回
  3. 定期删除,每隔一段时间,就对数据库进行一次检查,删除里面的过期键

定时删除,占用太多的cpu时间,影响服务器的响应时间和吞吐量

惰性删除,浪费太多内存,有内存泄漏的危险

定期删除,是前两种的整合和折中,服务器必须根据实际情况,合理的设置删除操作执行的时长和指向频率

  • Redis的过期键执行策略:
    采用惰性删除和定期删除两种策略
  • 惰性删除策略实现:
  • 由db.c/expireIfNeeded函数实现,所有读写数据库的命令,在执行前都会调用这个函数,对键进行检查:如果键过期,就删除键,没有过期,继续执行实际命令
  • 因为每个命令执行前可能这个键会因为过期被删除,或不存在,所以需要判断键是否存在


    命令执行
  • 定期删除策略的实现:
  • 由redis.C/activeExpireCycle函数实现,每当redis服务器周期性的执行redis.c/serverCron函数,activeExpireCycle就会被调用,在规定时间内分多次遍历服务器中的各个数据库,从数据库中的expires字典中随机检查一部分键的过期时间,并删除其中的过期键
DEFAULT_DB_NUMBERS=16//默认每次检查的数据库数量
DEFAULT_KEY_NUMBERS=20 //默认每个数据库检查的键数量
current_db=0 //全局变量,记录检查进度
def activeExpireCycle():
   //初始化要检查的数据库量,如果服务器的数据库量比默认的小,就用服务器的
  if server.dbnum<DEFAULT_DB_NUMBERS:
    db_numbers=server.dbnum
  else:
    db_numbers=DEFAULT_DB_NUMBERS
  //遍历各个数据库
  for i in range(db_numbers):
    //如果当前current_db的值等于服务器的数据库量,说明遍历完数据库了,将current_db=0,开始新的一轮遍历
    if current_db == server.dbnum:
      current_db=0
    //获取当前要处理的数据库
    redisDb=server.db[current_db]
    //将数据库索引增1,指向下一个要处理的数据库
    current_db+=1
    //检查数据库键
    for j in range(DEFAULT_KEY_NUMBERS):
      //如果数据库中没有一个键有过期时间,就跳过这个数据库
      if redisDb.expires.size()==0:break
      //随机获取一个带有过期时间的键
      key_with_ttl=redisDb.expires.get_random_key()
      //检查键是否过期,如果过期就删除他
      if is_expired(key_with_ttl):
        delete_key(key_with_ttl)
      //已达时间上限,停止处理
      if reach_time_limit():return

AOF、RDB和复制功能对过期键的处理

  • 生成RDB文件,执行save或者bgsave命令创建一个新的RDB文件,程序会对数据库的键进行检查,已过期的键不会被保存到新的RDB中
  • 载入RDB文件,在启动redis服务器的时候,如果启动了RDB功能,服务器会对RDB文件进行载入
  • 如果是主服务器模式运行,在载入RDB的时候,会对文件的键进行检查,没有过期的键会被载入到数据库中,过期的会被忽略
  • 如果是从服务器模式运行,文件中所有键都会被载入到数据库中,在主从服务器同步的时候,从服务器的数据库会被清空
  • AOF文件写入,当服务器以AOF持久化模式运行的时候,如果数据库的键过期了,但没有被删除,AOF文件不会有任何影响,当过期键被删除了,程序会向AOF文件追加append命令,显式记录该键被删除了
    GET message,从数据库中删除message键,追加一条DEL message命令到AOF文件中,向执行GET命令的客户端返回空回复
  • AOF重写,程序会对数据库中的键进行检查,已过期的键不会被保存到重写后的AOF文件中,执行BGREWRITEAOF命令
  • 复制,当服务器运行在复制模式下,从服务器的过期键动作由主服务器控制
  • 主服务器在删除一个过期键后,会显式的向所有从服务器发送一个DEL命令,告知从服务器删除这个过期键
  • 从服务器执行客户端发送的命令,即使碰到过期键也不会删除,继续向处理未过期的键一样
  • 从服务器只有在接到主服务器发来的EDL命令,才会删除过期键


    主从服务器删除过期键

    这时客户端向从服务器发送get message,会正常得到value值
    这时客户端向主服务器发送get message,主服务器发现message过期了,会删除 message,向客户端返回空回复,并向从服务器发送DEL message,从服务器 收到del,会删除message,这样主从服务器都不保存message了


    主服务器删除过期键,并向从服务器发送del

    主从服务器中都没有过期键了

数据库通知

这个功能是Redis2.8新增的,可以让客户端通过订阅给定的频道或者模式,来获取数据库中键的变化,以及数据库中命令的执行情况

键空间通知,客户端获取0号数据库中针对message键的执行的所有命令,SUBSCRIBE _ _ keyspace@0_ _:message
键事件通知,某个命令被什么键执行了,SUBSCRIBE _ _ keyevent@0 _ _:del

服务器配置的notify-keyspace-events选项决定了服务器所发送通知的类型

  • 想让服务器发送的所有类型的键空间通知和键事件通知,设为AKE
  • 想让服务器发送的所有类型的键空间通知,设置AK
  • 想让服务器发送的所有类型的键事件通知,设置为AE
  • 想让服务器只发送和字符串键有关的键空间通知,设置为K$
  • 想让服务器只发送和列表键有关的键事件通知,设置为E1
  • 发送通知

发送数据库通知的功能是由notify.c/notifyKeyspaceEvent函数实现的
void notifyKeyspaceEvent(int type,char *event,robj *key,int dbid);

  • 发送通知的实现
def notifyKeyspaceEvent(type,event,key,dbid):
//如果给定的通知不是服务器允许发送的通知,那么直接返回
  if not(server.notify_keyspace_events & type):
    return;
//发送键空间通知
  if server.notify_keyspace_evetns & REDIS_NOTIFY_KEYSPACE:
    //构建频道名字
    chan = "__keysapce@{dbid}__:{key}".format(dbid=dbid,key=key)
    //发送通知
    pubsubPublishMessage(chan,event)
//发送键事件通知
  if server.notify_keyspace_events & REDIS_NOTIFY_KEYEVENT:
    chan="__keyevent@{dbid}__:{event}".format{dbid=dbid,event=event}
    pubsubPublishMessage(chan,key)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容

  • 服务器中的数据库 Redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构的db数...
    tracy_668阅读 355评论 1 3
  • 1 服务器中的数据库 Redis服务器所有数据库都保存在服务器状态的redisServer结构的db数组中,db数...
    HRADPX阅读 638评论 0 0
  • 本文主要介绍Redis服务器数据库的设计与实现,说明保存数据库的方法,保存键值对的方法,以及针对数据库添加、删除、...
    wenmingxing阅读 438评论 0 3
  • 前言 开始之前,我们可以设想一下,假设是我们自己要设计一款内存缓存系统,需要用什么结构来保存我们的数据呢?首选的当...
    家硕先生阅读 517评论 0 6
  • 1、服务器中的数据库 redis服务器将所有数据库都保存在服务器状态redis.h/redisServer结构...
    rookie_yuqi阅读 297评论 0 0