Redis 八 - 数据库

Redis 八 - 数据库

涉及文件 redis.h

服务器

1
2
3
4
5
6
7
8
9
// redis.h
#define REDIS_DEFAULT_DBNUM 16
struct redisServer {
/* ... */
/* array */
redisDb *db;
int dbnum; /* Total number of configured DBs */
/* ... */
}

切换数据库

当前正在使用的数据库:

1
2
3
4
5
6
// redis.h
typedef struct redisClient {
/* ... */
redisDb *db;
/* ... */
} redisClient;

数据库

1
2
3
4
5
6
7
8
9
10
11
12
13
/* Redis database representation. There are multiple databases identified
* by integers from 0 (the default database) up to the max configured
* database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;

dict 字典保存了数据库中的所有键值对。SET, DEL, GET 等都是对这个 dict 进行的添加、更新或者操作,FLUSHDB 删除所有键值对,RANDOMKEY 随机返回一个键,还有 DBSIZE, EXISTS, RENAME, KEYS 等操作。

过期时间

redisDb 结构体的 expires 数组保存了所有带有键的过期时间

1
redisDb.expires[key] = expire_time_in_ms

过期键删除策略

Redis 服务器实际使用的是惰性删除和定期删除两种策略

位于 db.c 文件中的 expireIfNeeded 函数实现惰性删除策略:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int expireIfNeeded(redisDb *db, robj *key) {
mstime_t when = getExpire(db,key);
mstime_t now;

if (when < 0) return 0; /* No expire for this key */

/* Don't expire anything while loading. It will be done later. */
if (server.loading) return 0;

now = server.lua_caller ? server.lua_time_start : mstime();

if (server.masterhost != NULL) return now > when;

/* Return when this key has not expired */
if (now <= when) return 0;

/* Delete the key */
server.stat_expiredkeys++;
propagateExpire(db,key);
notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,
"expired",key,db->id);
return dbDelete(db,key);
}

惰性删除对 CPU 是友好的,程序只会在取出键时才会对键进行过期检查,这可以保证删除过期键的操作只会在非做不可的情况下进行,但其缺点也很明显,它对内存不友好。那么就需要定时删除策略进行一个整合和折中:

数据库通知

1
2
// notify.c
notifyKeyspaceEvent(int type, char *event, robj *key, int dbid)

RDB 持久化

(1) RDB 持久化功能生成的是一个经过压缩的二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态:

1
2
3
4
5
6
// 同步保存
127.0.0.1:6379[1]> SAVE
OK
// 异步保存
127.0.0.1:6379[1]> BGSAVE
Background saving started

执行 SAVEBGSAVE 命令的源代码为:

1
2
// rdb.c
int rdbSave(char *filename)

在我本机上保存在了:

1
/var/lib/redis/dump.rdb

(2) 只要 Redis 服务器在启动时检测到有 RDB 文件,它就会自动加载 RDB 文件:

1
2
3
4
$ redis-server
...
10648:M 03 Jul 10:48:39.006 * DB loaded from disk: 0.000 seconds
...

载入 RDB 文件的实现位于:

1
2
// rdb.c
int rdbLoad(char *filename)

(3) 校验 dump.rdb

1
2
3
zk@zk-pc:/var/lib/redis$ redis-check-dump dump.rdb 
==== Processed 3 valid opcodes (in 18 bytes) ===================================
CRC64 checksum is OK

(4) 其他资源

redis-rdb-tools 可以将 dump.rdb 文件转为 JSON 文件:

1
2
pip install rdbtools
rdb --command json dump.rdb

解密Redis持久化 解释了 Redis 持久化功能与其他常见数据库的持久化功能之间的异同

AOF 持久化

事件

(1) 事件的调度和执行

1
2
3
4
5
// redis.c
int main(int argc, char **argv) {
// ...
aeMain(server.el);
}
1
2
3
4
5
6
7
8
9
10
11
12
// ae.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}

// 处理所有的 pending 事件,然后处理所有的文件事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags)

客户端

(1) 列出所有客户端

1
2
127.0.0.1:6379> CLIENT list
id=3 addr=127.0.0.1:40084 fd=5 name= age=6 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// redis.h
struct redisServer {
list *clients; /* List of active clients */
}

typedef struct redisClient {
// 客户端的文件描述符
int fd;
// 输入缓冲区: 保存客户端发送的请求命令
sds querybuf;
// 记录客户端的角色
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
// 存储命令长度: 3
int argc;
// 是一个字典
// argv[0] = "SET"
// argv[1] = "message"
// argv[2] = "hello world"
robj **argv;
}

服务器

(1) 调用命令:

1
2
3
4
5
6
7
// redis.c
struct redisCommand redisCommandTable[] = {
{"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
{"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
// ...
};
1
2
3
4
5
// redis.c
/* Call() is the core of Redis execution of a command */
void call(redisClient *c, int flags) {
c->cmd->proc(c);
}

(2) serverCron 函数

服务器每秒钟调用 server.hzserverCron 函数:

1
2
3
4
5
6
7
8
// redis.c
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
return 1000/server.hz;
}

void initServerConfig(void) {
server.hz = REDIS_DEFAULT_HZ;
}
1
2
// redis.h
#define REDIS_DEFAULT_HZ 10 /* Time interrupt calls/sec. */

慢查询日志

慢查询日志: 系统在命令执行前后计算每条命令的执行时间,当超过预设阈值,就将这条命令的相关信息记录到慢查询日志中。

  • slowlog-log-slower-than: 默认值是 10000 微秒 (10 毫秒) (1秒 = 1000000 微秒),高 OPS 场景下的 Redis 建议设置为 1 毫秒
  • slowlog-max-len: 最多存放多少条慢查询,当慢查询日志列表已处于其最大长度时,最早插入的一个命令将从列表中移出,默认 128,线上可以设置为 1000 以上
1
2
3
4
5
6
// 临时修改
127.0.0.1:6379> CONFIG GET slowlog-log-slower-than
1) "slowlog-log-slower-than"
2) "20000"
127.0.0.1:6379> CONFIG SET slowlog-log-slower-than 20000
OK

虽然慢查询日志存放在 Redis 内存列表中,但是 Redis 并没有暴露这个列表的键,而是通过一组命令来实现对慢查询日志的访问和管理:

1
2
3
4
5
6
7
127.0.0.1:6379> SLOWLOG GET
1) 1) (integer) 1 // 日志的唯一标识符
2) (integer) 1499139268 // 命令执行时的 UNIX 时间戳
3) (integer) 2458 // 命令执行 2458 微秒
4) 1) "set" // 命令以及命令参数
2) "message"
3) "hello"

获取慢查询日志当前长度:

1
2
127.0.0.1:6379> SLOWLOG LEN
(integer) 4

清理慢查询日志列表:

1
2
127.0.0.1:6379> SLOWLOG RESET
OK

慢查询只记录命令的执行时间,并不包括命令排队和网络传输时间,因此客户端执行命令的时间会大于命令的实际执行时间。由于慢查询是一个先进先出的队列,也就是说如果慢查询比较多的情况下,可能会丢失部分慢查询命令,为了防止这种情况发生,可以定期执行 SLOWLOG GET 命令将慢查询日志持久化到其他存储 (MySQL, ElasticSearch) 中,然后通过可视化工具进行查询。

推荐文章