Redis数据库

数据库中的服务器

Redis服务器将所有的数据库都保存在服务器状态redis.h/redisServer结构的db数组中、db数组的每个项都是一个redisDB结构

struct redisServer {
  // ...
  redisDb *db; // 一个数组、保存着服务器中的所有数据库
  // ...
} ;

初始化服务器时、程序会根据服务器状态的dbnum属性决定创建多少个数据库

struct redisServer {
  // ...
  int dbnum; // 服务器的数据库数量
  // ...
};

切换数据库

每个Redis客户端都有自己的目标数据库, 每当客户端执行数据库读写命令时, 目标数据库就会成为这些命令的操作对象, 默认使用0号数据库, 可以使用select 切换

客户端状态redisClient结构的db属性记录了客户端当前的目标数据库, 是一个指向redisDb结构的指针

typedef struct redisClient {
  // ...
  redisDb *db;
  // ...
}redisClent;

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

通过修改redisClient.db指针, 让它指向服务器中的不同数据库, 实现切换数据库的功能、select命令就是依靠db指针实现的

redis键空间

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

typedef struct redisDb {
  // ...
  dict *dict; // 数据库键空间
}

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

  1. 键空间的键即数据库的键, 每个键都是一个字符串对象
  2. 键空间的值也就是数据库的值, 每个值都可以是字符串对象、列表对象、hash表对象、集合对象和有序集合对象的任意一种redis对象
添加新键

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

删除键

删除数据库中的一个键、实际上是在键空间里删除键锁对应的键值对对象

更新键

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

对键取值

其实就是在键空间中取出键所对应的值对象, 根据值对象的类型不同、具体的取值方法也会有所不同

其它键空间操作

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

eg. flushdb, 就是通过删除键空间的所有键值对来实现的

​ randomkey是通过随机在键空间返回一个键实现的

​ 另外, 用于返回数据库键数量的dbsize命令, 是通过返回键空间中包含的键值对的数量来实现的, 类似的命令还有 exists, rename, keys 等

读写键空间时的维护工作
  1. 在读取一个键之后(w/r), 服务器会根据键是否存在更新键命中(hit)或不命中(miss)次数, 这两个值可以通过info stats命令的keyspace_hits属性和keyspace_misses属性中查看
  2. 读取一个键后、服务器会更新键的lru时间, 这个值会用于计算键的闲置时间, 使用object idletime可以查看键的闲置时间
  3. 若服务器在读取一个键时、发现键已过期, 会先删除这个过期key, 然后执行下边的操作
  4. 若客户端使用watch监视了某个key, 服务器在对被监视的key进行修改之后、会将这个键标记为 dirty, 从而让事务程序注意到键已被修改过
  5. 服务器每次修改键、会对脏dirty键计数器的值+1, 它会触发服务器的持久化及复制操作
  6. 若服务器开启了数据库通知功能, 在对键进行修改后、服务器将按配置发送相应的数据库通知

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

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

**注意: **

setex可以在设置一个字符串键的同事为键设置过期时间, 因为这个命令是一个类型限定的命令(只能用于字符串键), 它的原理和expire一致

expirepexpire命令类似, 客户端可以通过expireat 或者 pexpireat 以 s 或者 ms 为精度给数据库中的某个键设置过期时间, 过期时间是一个unix时间戳, 到达过期时间时, key会被删除

ttlpttl命令接受一个带有生存时间或者过期时间的键, 返回键的剩余生存时间, 即: 还有多久键会被删除

设置过期时间

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

  • expire 用于将键key的生存时间设置为 ttl s
  • pexpire 命令用于将键key的生存时间设置为 ttl ms
  • expireat 命令用于将key的过期时间设置为timestamp指定的秒数时间戳
  • pexpireat 命令用于将key的过期时间设置为timestamp指定的毫秒数时间戳

实际上, expire, pexpire, expireat 三个命令都是使用pexpireat命令来实现的

def expire(key, ttl) :
   # 将ttl从s转换为ms
   ttl_in_ms = sec_to_ms(ttl_in_seec)
   pexpire(key, ttl_in_ms)
     

def pexipre(key, ttl_in_ms) :
   # 获取以ms计算的当前unix时间戳
   now_ms = get_current_unnix_timeestamp_in_ms()
  # 当前时间——ttl, 得出ms格式的键过期时间
  pexpireat(key, now_ms + ttl_in_ms)
  

def expireat(key, expire_time_in_sec) :
  # 将过期时间从s转换为ms
  expire_time_in_ms = sec_to_ms(expire_time_in_seec)
  pexpreat(key, expire_time_in_ms)
保存过期时间

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

  1. 过期字典的键是一个指针、指向键空间的键对象
  2. 过期字典的值是一个long long类型的整数, 这个整数保存了键所指向的数据库键的过期时间(一个ms精度的unix时间戳)
typedef struct redisDb{
  // ..
  dict *expires; // 过期字典, 保存着键的过期时间
  // ...
} redisDb;

[图片上传失败...(image-d5c5dd-1588838165448)]

当客户端执行pexpireat命令为数据库的额键设置过期时间时, 服务器会在数据库的额过期字典中关联给定的数据库键和过期时间

def pexpireat(key, expire_time_in_ms) :
  # 若给的的key不存在于键空间, 不设置过期时间
  if key nnot in redisDb.dict:
    return 0;
  # 在过期字典中关联键和过期时间
  redisDb.expires[key] = expire_time_in_ms
  # 过期时间设置成功
  return 1 
移除过期时间

persist可以移除一个键的过期时间, 它是pexpireat命令的反操作, 是在过期字典中查找给定的键, 并解除键和值(过期时间)在过期字典中的关联

def perstst(key) :
  # 若键不存在、或者键未设置过期时间, 直接返回
  if key not in redisDb.expires:
    return 0;
  # 移除过期字典中给定键的键值对关联
  redisDb.expires.remove(key)
  # 键的过期时间移除成功
  return 1
计算并返回剩余生存时间

TTL以 s 为单位返回键的剩余生存时间, pttl以ms会单位返回

def pttl(key) :
  # 键不存在于数据库
  if key not in redisDb.dict:
    return -2;
  # 尝试获取键的过期时间
  # 若键未设置过期时间、expire_time_in_ms 为 none
  expire_time_in_ms = redisDb.expires.get(key)
  # 键未设置过期时间
  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) :
  # 获取以ms为单位的剩余生存时间
  ttl_in_ms = PTTL(key)
  if ttl_in_ms < 0:
    # 处理返回值为-1 和 -2 的情况
    return ttl_in_ms
  else:
    # 将ms转为s
    return ms_to_sec(ttl_in_ms)
  
过期键的判定

通过过期字典, 程序可以检测一个key是否过期:

  1. 检查给定key是否存在于过期字典, 若在, 取得key的过期时间
  2. 检查当前unix时间戳是否>key的过期时间, 是: key已过期, 否则: 未过期
def is_expired(key) :
  # 取得键的过期时间
  expire_time_in_ms = redisDb.expires.get(key)
  # 键未设置过期时间
  if expire_time_in_ms is None:
    return false
  # 取得当前unix时间戳
  now_ms = get_current_unix_time();
  # 检查当前时间是否大于key的过期时间
  if now_ms > expire_time_in_ms :
    # 是, key 过期
    return true
  else :
    return false
  

过期键删除策略

一个键过期了、什么时候会被删除呢 ?

  1. 定时删除: 在设置键过期的同时, 创建一个定时器, 让定时器在键的过期时间来临时、立即执行键删除操作
  2. 惰性删除: 放任键过期不管, 每次从键空间获取键时、都检查取得的键是否过期、过期即删除; 否则就返回
  3. 定期删除: 每隔一段时间、程序就对数据库进行一次检查、删除里边的过期key, 至于删除多少过期键、及要检查多少个数据库由算法决定

其中: 第一种和第三种策略为主动删除策略, 第二种是被动删除

定时删除

定时删除是内存友好的, 可以保证过期key尽快被删除、并释放过期键占用的内存

缺点: 对CPU时间不友好, 在过期key较多时、删除过期key这一行为会占用相当一部分CPU时间, 在内存不紧张但CPU紧张时、会对服务器的响应时间和吞吐量造成影响

eg. 正有大量的请求在等待服务器处理, 且当前服务器不缺少内存, 那么服务器应优先将CPU时间用在处理客户端请求上、而不是删除过期key上

此外. 创建一个定时器需要用到redis服务器中的时间事件, 当前时间事件的实现是: 无序链表, 查找一个事件的时间复杂度是 O(N), 不能高效的处理大量时间事件

所以, 想要服务器创建大量的定时器、实现定时删除策略, 不是特别现实

惰性删除

惰性删除是CPU友好的, 程序只会在取出键时才进行过期检查, 保证删除key只会在非做不可的情况下进行, 且仅处理当前key, 不会再删除其它无用key上花费CPU时间

缺点: 内存不友好, 若key已过期, 但一直无访问, 它会一直存在, 内存不会释放

在使用惰性删除策略时、若db中有大量过期key, 且恰好未被访问到, 它们会一直占用内存, 甚至可以视为内存泄露, eg. 日志、在某个时间点之后、使用大量减少、甚至不会访问, 假设以为服务器已经自动删除, 实际上这些键依然存在, 占用的内存也未释放, 会有大量的内存浪费

定期删除

从上边的情况来看, 这两种方式各自有自己的特点:

  1. 定时删除占用CPU时间过多, 影响服务器的响应时间和吞吐量
  2. 惰性删除浪费太多内存、有内存泄露的危险

定期删除是前两种策略的整合和折中:

  1. 定期删除是每隔一段时间执行一次, 并通过限制删除操作执行的时长和频率减少删除操作对CPU时间的影响
  2. 除此之外, 定期key过期删除、有效减少因过期key带来的内存浪费

定期删除的难点在于确定删除操作执行的时长和频率:

  1. 若删除执行的过于频繁, 或执行的时间太长, 定期删除策略就会退化为定时删除策略, 消耗过多CPU时间在key删除上
  2. 若删除操作执行的少, 或执行时间太短, 定期删除策略又和惰性删除策略一样、出现内心浪费

因此、必须根据服务器情况、合理设置删除操作的执行时长和频率

redis 的过期键删除策略

上节讨论了定时删除惰性删除定期删除三种过期key删除策略, 实际上 Redis服务器使用的是 惰性删除定期删除 两种策略.

惰性删除策略实现

db.c/expireIfNeeded函数实现, 所有的命令在执行之前都会调用它进行过期检查

  1. 若键已过期、expireIfNeeded会将key从db中删除
  2. 未过期、不操作
定期删除策略的实现

redis.c/activeExpireCycle函数实现, 每当redis的服务器周期性操作redis.c/serverCron函数执行时、activeExpireCycle就会被调用, 在规定时间内、分多次遍历服务器中的各个数据库, 从数据库的expires字典随机检查一部分键的过期时间, 并删除其中的过期键, 伪代码表示如下:

# 默认每次检查的数据库数量
default_db_numbers = 16
# 默认每个数据库检查的键数量
default_key_nnumbeers = 20
# 全局变量、记录检查进度
current_db = 0
def activeExpireCycle() :
  # 初始化要检查的服务器数量
  # 若服务器的数据库数量比 default 值小, 以实际服务器数量为准
  if server.dbnum < default_db_numbeers:
    db_numbers = server.dbnum
  else:
    db_numbers = default_db_numbers
  # 遍历各个数据库
  for i in range(db_numbers)
    # 若current_db的值等于服务器的数据库数量, 表示已遍历一遍, 将current置为0, 开始新一轮遍历
    if current_db == server.dbnum :
      current_db = 0
    # 获取当前要处理的数据库
    redisDb = server.db[current_db]
    # 将数据库索引+1, 指向下一个要处理的db
    current_db +=1
    # 检查数据库键
    for j in range(default_keey_numbers) :
      # 若数据库中没有一个key带有过期时间, 跳过即可
      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

activeExpireCyclee函数的工作模式总结如下:

  1. 函数每次运行时、从数据库抽取一定数据量的key随机检查、并删除过期key
  2. 全局变量current_db记录当前activeExpireCycle函数检查的进度, 并在下次被调用时接上次处理, eg. 当前处理到10号数据库, 下次调用会从11号数据库开始处理
  3. 随着activeExpireCycle函数的不断执行、服务器中的所有数据库被检查一遍, 重置为0, 下次继续新一轮处理

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

生成RDB文件

在执行save或者bgsave创建新的RDB文件时、程序会对键进行过期检查、已过期的key不会被保存

载入RDB文件

在启动redis服务器时、若开启了RDB功能, 对RBD文件载入时:

  1. 若服务器以主服务器模式运行、在载入RDB文件时、程序会对文件中保持的额键进行检查、未过期的键会被载入数据库、过期键直接忽略, 所以过期键对载入RDB文件的主Server不影响

  2. 若Server以从模式运行, 不检查过期、直接全部载入, 在主从同步时、从的数据库就会被清空, 一般来讲、也不会造成任何影响

  3. AOF文件写入
    Server以AOF持久化模式运行时、若key过期、但还未被惰性删除或者定期删除, AOF文件不因为key过期产生任何影响

    eg. 访问过期key msg, 会执行以下步骤

1. 从db删除msg键
2. 追加`del msg` 命令到aof文件
3. 向执行get的client返回 空 回复
AOF重写

和生成RDB文件类似、在执行AOF重写时、会对数据库键进行检查, 已过期的键不会被保存到重写后的额AOF文件中.

复制

Server运行在复制模式下时、从Server的过期key由主Server控制

  1. 当主服务器在删除一个过期key后, 会显式的向所有从Server发送一个del命令、告知删除
  2. 从Server在执行Client发送的读命令时、碰到过期键也不删除, 而是正常处理等同于未过期
  3. 从Server只有在接到主Server发来的del命令后、才会删除过期键

用这种方式来保证主从Server的一致性, 所以、对于依然存在于主Server的过期键、复制时也同样会复制

重点回顾

  1. redis server的所有数据库都保存在 redisServer.db 数组中、db的数量由 redisServer.dbnum 保存
  2. Client通过修改目标数据库指针、指向redisServer.db 数组中的不同元素来切换db
  3. 数据库主要有dictexpires 两个字典构成, dict 复制保存键值对, expires 负责保存键过期时间
  4. db由字典构成、对db的操作都建立在字典操作之上
  5. 键总是一个字符串对象、值可以是任意redis类型(String, hash, set, list, sortedSet)等
  6. expires字典的键指向db中的某个键、值记录过期时间, ms为单位的unix时间戳
  7. 使用惰性删除和定期删除两种策略来删除过期键, 惰性删除只在获取时删除, 定期删除每隔一段时间主动检测
  8. save或bgsave新产生的rdb文件、不包含过期键
  9. bgwriteaof产生的aof重写文件不包含过期键
  10. 键过期后、server会追加del命令到aof文件末尾、显式删除过期key
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,817评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,329评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,354评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,498评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,600评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,829评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,979评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,722评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,189评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,519评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,654评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,329评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,940评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,762评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,993评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,382评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,543评论 2 349