数据库中的服务器
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; // 数据库键空间
}
键空间和用户所见的数据库是直接对应的:
- 键空间的键即数据库的键, 每个键都是一个字符串对象
- 键空间的值也就是数据库的值, 每个值都可以是字符串对象、列表对象、hash表对象、集合对象和有序集合对象的任意一种redis对象
添加新键
添加一个新键值对到数据库、实际上就是将一个新的键值对添加到键空间字典里, 其中键为字符串对象、而值为任意一种redis对象
删除键
删除数据库中的一个键、实际上是在键空间里删除键锁对应的键值对对象
更新键
对一个数据库键进行更新、实际上是对键空间里键所对应的值对象进行更新, 根据值对象的类型不同、更新的具体方法也会有所不同
对键取值
其实就是在键空间中取出键所对应的值对象, 根据值对象的类型不同、具体的取值方法也会有所不同
其它键空间操作
除了添加、删除、更新、取值操作之外, 还有很多针对数据库本身的redis命令, 也是通过键空间处理完成的
eg. flushdb, 就是通过删除键空间的所有键值对来实现的
randomkey是通过随机在键空间返回一个键实现的
另外, 用于返回数据库键数量的dbsize命令, 是通过返回键空间中包含的键值对的数量来实现的, 类似的命令还有 exists, rename, keys 等
读写键空间时的维护工作
- 在读取一个键之后(w/r), 服务器会根据键是否存在更新键命中(hit)或不命中(miss)次数, 这两个值可以通过
info stats
命令的keyspace_hits
属性和keyspace_misses
属性中查看 - 读取一个键后、服务器会更新键的lru时间, 这个值会用于计算键的闲置时间, 使用
object idletime
可以查看键的闲置时间 - 若服务器在读取一个键时、发现键已过期, 会先删除这个过期key, 然后执行下边的操作
- 若客户端使用
watch
监视了某个key, 服务器在对被监视的key进行修改之后、会将这个键标记为 dirty, 从而让事务程序注意到键已被修改过 - 服务器每次修改键、会对脏dirty键计数器的值+1, 它会触发服务器的持久化及复制操作
- 若服务器开启了数据库通知功能, 在对键进行修改后、服务器将按配置发送相应的数据库通知
设置键的生存时间或过期时间
通过
expire
或者pexpire
命令, 客户端可以以 s 或者 ms 为精度为数据库中的某个键设置生存时间(Time To Live, TTL), 在经过指定的秒数或者毫秒数之后、服务器会自动删除生存时间为0的key
**注意: **
setex
可以在设置一个字符串键的同事为键设置过期时间, 因为这个命令是一个类型限定的命令(只能用于字符串键), 它的原理和expire
一致
与expire
、pexpire
命令类似, 客户端可以通过expireat
或者 pexpireat
以 s 或者 ms 为精度给数据库中的某个键设置过期时间, 过期时间是一个unix时间戳, 到达过期时间时, key会被删除
ttl
和 pttl
命令接受一个带有生存时间或者过期时间的键, 返回键的剩余生存时间, 即: 还有多久键会被删除
设置过期时间
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字典保存了数据库中所有键的过期时间, 这个字典称为过期字典
- 过期字典的键是一个指针、指向键空间的键对象
- 过期字典的值是一个
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是否过期:
- 检查给定key是否存在于过期字典, 若在, 取得key的过期时间
- 检查当前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
过期键删除策略
一个键过期了、什么时候会被删除呢 ?
- 定时删除: 在设置键过期的同时, 创建一个定时器, 让定时器在键的过期时间来临时、立即执行键删除操作
- 惰性删除: 放任键过期不管, 每次从键空间获取键时、都检查取得的键是否过期、过期即删除; 否则就返回
- 定期删除: 每隔一段时间、程序就对数据库进行一次检查、删除里边的过期key, 至于删除多少过期键、及要检查多少个数据库由算法决定
其中: 第一种和第三种策略为主动删除策略, 第二种是被动删除
定时删除
定时删除是内存友好的, 可以保证过期key尽快被删除、并释放过期键占用的内存
缺点: 对CPU时间不友好, 在过期key较多时、删除过期key这一行为会占用相当一部分CPU时间, 在内存不紧张但CPU紧张时、会对服务器的响应时间和吞吐量造成影响
eg. 正有大量的请求在等待服务器处理, 且当前服务器不缺少内存, 那么服务器应优先将CPU时间用在处理客户端请求上、而不是删除过期key上
此外. 创建一个定时器需要用到redis服务器中的时间事件, 当前时间事件的实现是: 无序链表, 查找一个事件的时间复杂度是 O(N), 不能高效的处理大量时间事件
所以, 想要服务器创建大量的定时器、实现定时删除策略, 不是特别现实
惰性删除
惰性删除是CPU友好的, 程序只会在取出键时才进行过期检查, 保证删除key只会在非做不可的情况下进行, 且仅处理当前key, 不会再删除其它无用key上花费CPU时间
缺点: 内存不友好, 若key已过期, 但一直无访问, 它会一直存在, 内存不会释放
在使用惰性删除策略时、若db中有大量过期key, 且恰好未被访问到, 它们会一直占用内存, 甚至可以视为内存泄露, eg. 日志、在某个时间点之后、使用大量减少、甚至不会访问, 假设以为服务器已经自动删除, 实际上这些键依然存在, 占用的内存也未释放, 会有大量的内存浪费
定期删除
从上边的情况来看, 这两种方式各自有自己的特点:
- 定时删除占用CPU时间过多, 影响服务器的响应时间和吞吐量
- 惰性删除浪费太多内存、有内存泄露的危险
定期删除是前两种策略的整合和折中:
- 定期删除是每隔一段时间执行一次, 并通过限制删除操作执行的时长和频率减少删除操作对CPU时间的影响
- 除此之外, 定期key过期删除、有效减少因过期key带来的内存浪费
定期删除的难点在于确定删除操作执行的时长和频率:
- 若删除执行的过于频繁, 或执行的时间太长, 定期删除策略就会退化为定时删除策略, 消耗过多CPU时间在key删除上
- 若删除操作执行的少, 或执行时间太短, 定期删除策略又和惰性删除策略一样、出现内心浪费
因此、必须根据服务器情况、合理设置删除操作的执行时长和频率
redis 的过期键删除策略
上节讨论了定时删除
、惰性删除
、定期删除
三种过期key删除策略, 实际上 Redis服务器使用的是 惰性删除
和 定期删除
两种策略.
惰性删除策略实现
db.c/expireIfNeeded
函数实现, 所有的命令在执行之前都会调用它进行过期检查
- 若键已过期、
expireIfNeeded
会将key从db中删除 - 未过期、不操作
定期删除策略的实现
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
函数的工作模式总结如下:
- 函数每次运行时、从数据库抽取一定数据量的key随机检查、并删除过期key
- 全局变量
current_db
记录当前activeExpireCycle
函数检查的进度, 并在下次被调用时接上次处理, eg. 当前处理到10号数据库, 下次调用会从11号数据库开始处理 - 随着
activeExpireCycle
函数的不断执行、服务器中的所有数据库被检查一遍, 重置为0, 下次继续新一轮处理
AOF、RDB和复制功能对键过期的处理
生成RDB文件
在执行save或者bgsave创建新的RDB文件时、程序会对键进行过期检查、已过期的key不会被保存
载入RDB文件
在启动redis服务器时、若开启了RDB功能, 对RBD文件载入时:
若服务器以主服务器模式运行、在载入RDB文件时、程序会对文件中保持的额键进行检查、未过期的键会被载入数据库、过期键直接忽略, 所以过期键对载入RDB文件的主Server不影响
若Server以从模式运行, 不检查过期、直接全部载入, 在主从同步时、从的数据库就会被清空, 一般来讲、也不会造成任何影响
-
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控制
- 当主服务器在删除一个过期key后, 会显式的向所有从Server发送一个del命令、告知删除
- 从Server在执行Client发送的读命令时、碰到过期键也不删除, 而是正常处理等同于未过期
- 从Server只有在接到主Server发来的del命令后、才会删除过期键
用这种方式来保证主从Server的一致性, 所以、对于依然存在于主Server的过期键、复制时也同样会复制
重点回顾
- redis server的所有数据库都保存在 redisServer.db 数组中、db的数量由 redisServer.dbnum 保存
- Client通过修改目标数据库指针、指向redisServer.db 数组中的不同元素来切换db
- 数据库主要有
dict
和expires
两个字典构成,dict
复制保存键值对,expires
负责保存键过期时间 - db由字典构成、对db的操作都建立在字典操作之上
- 键总是一个字符串对象、值可以是任意redis类型(String, hash, set, list, sortedSet)等
-
expires
字典的键指向db中的某个键、值记录过期时间, ms为单位的unix时间戳 - 使用惰性删除和定期删除两种策略来删除过期键, 惰性删除只在获取时删除, 定期删除每隔一段时间主动检测
- save或bgsave新产生的rdb文件、不包含过期键
- bgwriteaof产生的aof重写文件不包含过期键
- 键过期后、server会追加del命令到aof文件末尾、显式删除过期key