redis设计与实现之数据库篇

Redis 是一个键值对(key-value pair)数据库服务器,所有数据库都保存在服务器状态redis.h/redisServer结构的db中,db是一个redisDb数组类型,每个元素都代表一个数据库(redisDb),而服务器内部又保存一个redisClient结构,它包含了一个redisDb指针,用来表示当前所用数据库.当我们想切换数据库时,只需修改这个指针即可, 服务器中的每个数据库都由一个 redis.h/redisDb 结构表示, 其中, redisDb 结构的 dict 字典保存了数据库中的所有键值对, 我们将这个字典称为键空间(key space),数据结构如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
struct redisServer{
...
redisDb *db; //redisDb数组,表示服务器中所有的数据库
int dbnum; //数组大小,默认为16
...
};
typedef struct redisClient{
...
redisDb *db; //客户端当前所选数据库
...
}redisClient;
typedef struct redisDb {
int id; // 数据库ID标识
dict *dict; // 数据库键空间,存放着所有的键值对
dict *expires; // 键的过期时间
dict *watched_keys; // 被watch命令监控的key和相应client
long long avg_ttl; // 数据库内所有键的平均TTL(生存时间)
} redisDb;

代码中有个dict,这个字典(键空间)存放了数据库所有的键值对,它的键是字符串对象,值可以使Redis的5中任意对象之一.下面给出一个它的实例图,执行命令如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
redis> SET message "hello world"
OK
redis> RPUSH alphabet "a" "b" "c"
(integer) 3
redis> HSET book name "Redis in Action"
(integer) 1
redis> HSET book author "Josiah L. Carlson"
(integer) 1
redis> HSET book publisher "Manning"
(integer) 1

图结构如下


再执行命令如下
1
2
redis> SET date "2013.12.1"
(integer) 1

图结构如下


再执行命令如下
1
2
redis> DEL book
(integer) 1

图结构如下


再执行命令如下
1
2
redis> SET message "blah blah"
(integer) 1

图结构如下


再执行命令如下
1
2
redis> HSET book page 320
(integer) 1

图结构如下


再执行命令如下
1
2
redis> GET message
"hello world"

图结构如下


再执行命令如下
1
2
3
4
redis> LRANGE alphabet 0 -1
1) "a"
2) "b"
3) "c"

图结构如下


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

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

生存周期

  • 设置键生存时间 EXPIRE key seconds, PEXPIRE key ms
  • 设置过期时间: EXPIREAT unix时间戳(s) , PEXPIREAT unix时间戳(ms)

过期删除策略

  • 定时删除:通过设置定时器,在过期时立即删除
  • 惰性删除:放任不管,但在获取键时发现过期,则删除
  • 定期删除:每隔一段时间,检查一次数据库,删除过期键

    定时删除占用CPU,影响服务器响应时间和吞吐量;惰性删除太占用空间,可能发生内存泄露,定期删除是两者的中和,服务器需要根据情况,合理的设置删除操作的执行时长和执行频率.

下面看下代码是如何执行的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 默认每次检查的数据库数量
DEFAULT_DB_NUMBERS = 16
# 默认每个数据库检查的键数量
DEFAULT_KEY_NUMBERS = 20
# 全局变量,记录检查进度
current_db = 0
def activeExpireCycle():
#初始化要检查的数据库数量
#如果服务榕的数据库数量比DEFAULT DB NUMBERS 要小
#那么以服务器的数据库数量为准
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

activeExpireCycle函数的工作模式可以总结如下

  • 函数每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查, 并删除其中的过期键
  • 全局变量current db 会记录当前activeExpireCycle 函数检查的进度,并在下一次activeExpireCycle 函数调用时,接着上一次的进度进行处理。比如说,如果当前activeExpireCycle 函数在遍历10 号数据库时返回了,那么下次activeExpireCycle 函数执行时,将从11号数据库开始查找并删除过期键
  • 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会被检查一遍,这时函数将current_db变量重置为0,然后再次开始新一轮的检查工作