SHAN


  • Home

  • Archives

【Redis】集群请求命令处理

Posted on 2022-05-26

集群请求命令处理

在Redis的命令处理函数processCommand(server.c)中有对集群节点的处理,满足以下条件时进入集群节点处理逻辑中:

  1. 启用了集群模式,通过server.cluster_enabled判断
  2. 发送命令的节点不是主节点
  3. 收到的命令中包含了key参数或者命令是EXEC,EXEC命令与MULTI结合使用,用于执行事务

条件三的判断条件有些绕,!cmdHasMovableKeys(c->cmd) && c->cmd->firstkey == 0意味着命令中没有key参数,c->cmd->proc != execCommand表示当前命令不是EXEC,然后对(!cmdHasMovableKeys(c->cmd) && c->cmd->firstkey == 0 && c->cmd->proc != execCommand)整体做了取反操作,那么看以下两种情况:

  • 如果命令中带有Key,那么!cmdHasMovableKeys(c->cmd)就已返回false,又因为对整体做了取反操作,所以条件成立,意味着收到命令中带有Key时需要执行重定向处理
  • 如果收到的命令是EXEC,c->cmd->proc != execCommand返回false,对整体取反变成true,所以条件也成立,意味着收到EXEC命令的时候执行重定向处理
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
38
39
40
int processCommand(client *c) {

// 省略...

/* 如果启用了集群且发送命令的节点不是主节点,并且收到的命令中包含了key参数或者命令是EXEC时 */
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(!cmdHasMovableKeys(c->cmd) && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
// 查询节点
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,&hashslot,&error_code);
if (n == NULL || n != server.cluster->myself) {
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
flagTransaction(c);
}
// 重定向
clusterRedirectClient(c,n,hashslot,error_code);
c->cmd->rejected_calls++;
return C_OK;
}
}

// 省略...

return C_OK;
}


/* 如果参数中有key将会返回1 */
static int cmdHasMovableKeys(struct redisCommand *cmd) {
return (cmd->getkeys_proc && !(cmd->flags & CMD_MODULE)) ||
cmd->flags & CMD_MODULE_GETKEYS;
}

MULTI命令的处理

上面说到,如果是EXEC命令时,也会进入到集群节点处理逻辑,EXEC命令一般与MULTI结合使用,用于执行事务。比如以下例子中,使用MULTI开启事务,执行对a账户增1,b账户减1的操作,可以看到返回结果为QUEUED,命令被缓存起来,直到执行EXEC命令,Redis才开始提交命令:

1
2
3
4
5
6
7
8
9
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> INCR a:account
QUEUED
127.0.0.1:6379> DECR b:account
QUEUED
127.0.0.1:6379> EXEC
1) (integer) 1
2) (integer) -1

由于集群也需要对EXEC命令处理,所以先看一下MULTI命令的处理逻辑,MULTI命令对应的执行函数为multiCommand,可以看到它在处理的时候为客户端设置了CLIENT_MULTI标记:

1
2
3
4
5
6
7
8
9
10
void multiCommand(client *c) {
if (c->flags & CLIENT_MULTI) {
addReplyError(c,"MULTI calls can not be nested");
return;
}
// 设置CLIENT_MULTI标记
c->flags |= CLIENT_MULTI;

addReply(c,shared.ok);
}

在Redis的命令处理函数中可以找到对CLIENT_MULTI的处理逻辑,如果客户端标记中有CLIENT_MULTI,并且当前命令不是EXEC、DISCARD、MULTI、WATCH和RESET,将调用queueMultiCommand函数,对命令进行缓存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int processCommand(client *c) {

// 省略...

/* 处理MULTI命令 */
/* 如果客户端标记中有CLIENT_MULTI,并且当前命令不是EXEC、DISCARD、MULTI、WATCH和RESET */
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand &&
c->cmd->proc != resetCommand)
{
queueMultiCommand(c); // 加入到multi队列中,先将命令缓存
addReply(c,shared.queued);
} else {
call(c,CMD_CALL_FULL);
c->woff = server.master_repl_offset;
if (listLength(server.ready_keys))
handleClientsBlockedOnKeys();
}
return C_OK;
}

MULTI命令结构体定义

在客户端结构体定义中,可以看到使用了multiState缓存MULTI命令:

1
2
3
4
5
6
// 客户端
typedef struct client {
// ...
multiState mstate; /* 存储MULTI/EXEC命令的结构体 */
// ...
}

multiState

MULTI命令对应的结构体为multiState,multiState中使用了multiCmd结构体来缓存具体的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct multiState {
multiCmd *commands; /* MULTI命令数组 */
int count; /* 缓存的命令个数 */
int cmd_flags; /* 命令标记 */
int cmd_inv_flags; /* 与cmd_flags一致 */
} multiState;

/* multi命令 */
typedef struct multiCmd {
robj **argv;
int argc;
struct redisCommand *cmd; /* 命令 */
} multiCmd;

MULTI命令的缓存

queueMultiCommand

对MULTI命令缓存的处理在queueMultiCommand函数中,它在multi.c文件中定义:

  1. 将multiCmd加入到缓存数组c->mstate.commands中,对命令进行缓存
  2. 将当前命令的内容设置到multiCmd中
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
/* 将当前命令加入到MULTI命令中 */
void queueMultiCommand(client *c) {
// MULTI命令
multiCmd *mc;
int j;
if (c->flags & CLIENT_DIRTY_EXEC)
return;
c->mstate.commands = zrealloc(c->mstate.commands,
sizeof(multiCmd)*(c->mstate.count+1));
// 到加入MULTI数组中
mc = c->mstate.commands+c->mstate.count;
// 设置命令
mc->cmd = c->cmd;
// 设置参数
mc->argc = c->argc;
mc->argv = zmalloc(sizeof(robj*)*c->argc);
memcpy(mc->argv,c->argv,sizeof(robj*)*c->argc);
for (j = 0; j < c->argc; j++)
incrRefCount(mc->argv[j]);
// 缓存的命令数加1
c->mstate.count++;
// 设置客户端标记
c->mstate.cmd_flags |= c->cmd->flags;
c->mstate.cmd_inv_flags |= ~c->cmd->flags;
}

查询节点

getNodeByQuery

getNodeByQuery函数用于根据KEY查询数据所在的节点,处理逻辑如下:

  1. 如果是EXEC命令,从客户端获取multiState,multiState中缓存了MULTI命令,如果不是MULTI命令,而是单个命令,同样使用multiState来存放命令,之后就可以统一使用multiState来获取请求中的命令

  2. 根据命令的个数进行遍历,处理每一个命令

    (1)从命令中获取key的个数,处理每一个key

    (2)查询每一个key所在的slot

    (3)如果处理的是第一个key,根据所属slot获取所在的节点,记为n,有以下三种情况:

    ​ 情况一:未获取到节点(有可能节点已下线但是还未更新状态),记录错误信息为CLUSTER_REDIR_DOWN_UNBOUND,表示key未绑定到slot,返回NULL

    ​ 情况二:可以查找到节点,并且是当前节点自己,但是key所属slot正在做数据迁出操作(从当前节点迁出),此时将migrating_slot置为1

    ​ 情况三:可以查找到节点,并且不是当前节点自己,但是key所属slot正在迁入到当前节点,此时将importing_slot置为1

    (4)如果处理的不是第一个key,判断当前key所属的slot是否与第一个key的slot一致:

    ​ 情况一:如果不一致,表示不同的key所属的slot不同,将error_code置为CLUSTER_REDIR_CROSS_SLOT,返回NULL

    ​ 情况二:如果一致,将multiple_keys置为1,表示请求中有多个KEY,做一个标记

    (5)根据migrating_slot和importing_slot的值判断key所属slot是否正在迁出或者迁入,迁出意味着key对应的数据正在从当前节点迁出到其他节点,迁入意味着key对应的数据正在迁入到当前节点,由于数据未迁移完毕,所以这两种情况都需要检查key是否在当前节点的数据库中,如果不在意味着当前节点没有该key的数据,需要记录缺失的KEY的数量,missing_keys增1

  3. 根据第二步查询后的结果,进行如下处理:

    • 未查找到节点,也就是n为空,返回当前节点自己

    • 当前节点不处于正常状态(CLUSTER_OK)

      (1)如果未开启allow_reads_when_down(在节点下线时允许读),error_code置为CLUSTER_REDIR_DOWN_STATE,并返回NULL

      (2)当前命令中有写标记,error_code置为CLUSTER_REDIR_DOWN_RO_STATE,并返回NULL

      (3)非以上两种情况,表示开启了allow_reads_when_down,并且是读操作,所以当前节点依旧可以处理请求,继续往下执行

    • 如果数据正在迁出或者正在迁入,并且当前命令是MIGRATE数据迁移的命令,返回当前节点

    • 如果key所在slot数据正在从当前节点迁出,并且当前节点数据库中有缺失的key,error_code置为CLUSTER_REDIR_ASK并返回迁出到的那个节点

    • 如果key所在slot正在迁入到当前节点,并且当前命令是ASK ,此时如果请求中有多个KEY并且当前节点存在缺失的KEY,表示有些key不在当前节点,error_code置为CLUSTER_REDIR_UNSTABLE返回NULL,否则返回当前节点即可

    • 如果客户端有只读标记、 当前命令不是写命令、当前节点是从节点并且它的主节点是根据key所属slot查找到的节点,返回当前节点,因为从节点数据是从master节点同步的,而master节点正是要查找的节点,从节点也可以处理读请求

    • 如果查询到的节点不是当前节点,将error_code置为CLUSTER_REDIR_MOVED,表示数据已经移动到其他节点,此时返回key所属slot对应的实际节点

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
clusterNode *getNodeByQuery(client *c, struct redisCommand *cmd, robj **argv, int argc, int *hashslot, int *error_code) {
// 集群节点
clusterNode *n = NULL;
// 记录命令中的第一个KEY
robj *firstkey = NULL;
int multiple_keys = 0;
multiState *ms, _ms;
multiCmd mc;
int i, slot = 0, migrating_slot = 0, importing_slot = 0, missing_keys = 0;

if (server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_REDIRECTION)
return myself;

if (error_code) *error_code = CLUSTER_REDIR_NONE;

/* 如果是EXEC命令 */
if (cmd->proc == execCommand) {
/* 校验是否有CLIENT_MULTI标记 */
if (!(c->flags & CLIENT_MULTI)) return myself;
// 获取multiState
ms = &c->mstate;
} else {
/* 如果不是MULTI命令,而是单个命令,同样使用multiState来存储命令 */
ms = &_ms;
_ms.commands = &mc;
_ms.count = 1; // 命令个数设置为1
mc.argv = argv;
mc.argc = argc;
mc.cmd = cmd; // 设置命令
}

/* 根据命令的个数进行遍历,处理每一个命令 */
for (i = 0; i < ms->count; i++) {
struct redisCommand *mcmd;
robj **margv;
int margc, *keyindex, numkeys, j;

mcmd = ms->commands[i].cmd; // 获取命令
margc = ms->commands[i].argc;
margv = ms->commands[i].argv;

getKeysResult result = GETKEYS_RESULT_INIT;
// 从命令中获取key的个数
numkeys = getKeysFromCommand(mcmd,margv,margc,&result);
keyindex = result.keys;
// 遍历每一个key
for (j = 0; j < numkeys; j++) {
// 获取key
robj *thiskey = margv[keyindex[j]];
// 查询key所在的slot
int thisslot = keyHashSlot((char*)thiskey->ptr,
sdslen(thiskey->ptr));
// 如果是第一个key
if (firstkey == NULL) {
/* 将第一个key记录在firstkey */
firstkey = thiskey;
// 记录slot
slot = thisslot;
// 根据slot获取集群节点
n = server.cluster->slots[slot];

/* 如果未获取到节点(有可能节点已下线),记录错误信息,返回NULL */
if (n == NULL) {
getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_DOWN_UNBOUND;
return NULL;
}

/* 如果根据slot查到的节点是当前节点自己,并且slot正在做数据迁出操作 */
if (n == myself &&
server.cluster->migrating_slots_to[slot] != NULL)
{
migrating_slot = 1; // migrating_slot置为1,标记正在做数据迁出操作
} else if (server.cluster->importing_slots_from[slot] != NULL) {
// 如果key所属的slot正在做数据迁入操作,importing_slot置为1
importing_slot = 1;
}
} else {
/* 如果不是第一个key*/
if (!equalStringObjects(firstkey,thiskey)) {
// 如果和第一个key的slot不一致,error_code置为CLUSTER_REDIR_CROSS_SLOT
if (slot != thisslot) {

getKeysFreeResult(&result);
if (error_code)
*error_code = CLUSTER_REDIR_CROSS_SLOT; /* 不同的key所属不同的slot */
return NULL;
} else {
/* 标记请求中有多个KEY */
multiple_keys = 1;
}
}
}

/* 如果slot正在迁入或者迁出,检查key是否在当前节点的db中,如果不在记录缺失的KEY的数量 */
if ((migrating_slot || importing_slot) &&
lookupKeyRead(&server.db[0],thiskey) == NULL)
{
missing_keys++;
}
}
getKeysFreeResult(&result);
}

/* 如果未查到,返回当前节点自己 */
if (n == NULL) return myself;

/* 如果当前节点的状态不是CLUSTER_OK状态,节点可能处于异常状态,只有在开启了allow_reads_when_down(在节点下线时允许读)并且当前命令是读操作才继续往下处理,否则记录错误信息返回NULL */
if (server.cluster->state != CLUSTER_OK) {
// 如果设置了节点下线时不允许读
if (!server.cluster_allow_reads_when_down) {
/* 记录错误信息,返回NULL */
if (error_code) *error_code = CLUSTER_REDIR_DOWN_STATE;
return NULL;
} else if (cmd->flags & CMD_WRITE) { // 如果命令中有写标记
/* The cluster is configured to allow read only commands */
if (error_code) *error_code = CLUSTER_REDIR_DOWN_RO_STATE;
return NULL;
} else {
/* Fall through and allow the command to be executed:
* this happens when server.cluster_allow_reads_when_down is
* true and the command is not a write command */
}
}

/* 更新hashslot */
if (hashslot) *hashslot = slot;

/* 如果数据正在迁出或者正在迁入,并且当前命令是MIGRATE数据迁移的命令,返回当前节点 */
if ((migrating_slot || importing_slot) && cmd->proc == migrateCommand)
return myself;

/* 如果key所在slot数据正在迁出,并且当前节点数据库中有缺失的key*/
if (migrating_slot && missing_keys) {
// error_code设置为CLUSTER_REDIR_ASK
if (error_code) *error_code = CLUSTER_REDIR_ASK;
// 返回迁出到的那个节点
return server.cluster->migrating_slots_to[slot];
}

/* 如果key所在slot正在做数据迁入,并且当前命令是ASK */
if (importing_slot &&
(c->flags & CLIENT_ASKING || cmd->flags & CMD_ASKING))
{
// 如果请求中有多个KEY并且有当前节点数据库中有缺失的key
if (multiple_keys && missing_keys) {
if (error_code) *error_code = CLUSTER_REDIR_UNSTABLE;
return NULL;
} else {
// 返回当前节点
return myself;
}
}

/* 是否是写命令 */
int is_write_command = (c->cmd->flags & CMD_WRITE) ||
(c->cmd->proc == execCommand && (c->mstate.cmd_flags & CMD_WRITE));
// 如果客户端有只读标记、当前命令不是写命令,当前节点是从节点并且它的主节点是根据key所属slot查找到节点
if (c->flags & CLIENT_READONLY &&
!is_write_command &&
nodeIsSlave(myself) &&
myself->slaveof == n)
{
// 返回当前节点即可
return myself;
}

/* 如果查询到的节点不是当前节点,将error_code置为CLUSTER_REDIR_MOVED,返回key所属slot对应的实际节点 */
if (n != myself && error_code) *error_code = CLUSTER_REDIR_MOVED;
return n;
}

根据Key从DB中查询Value

redisDb

Redis数据库对应的结构体定义为redisDb,里面有个字典类型的对象,存储键值对数据:

1
2
3
4
typedef struct redisDb {
dict *dict; /* 存储的键值对数据 */
// 省略...
} redisDb;

lookupKeyRead

lookupKeyRead函数用于从redisDb中根据key查找数据,最终是调用lookupKey函数完成的,根据Key从字典中查找并返回value:

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
robj *lookupKeyRead(redisDb *db, robj *key) {
// 调用lookupKeyReadWithFlags查找
return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
}

robj *lookupKeyWriteWithFlags(redisDb *db, robj *key, int flags) {
expireIfNeeded(db,key);
// 调用lookupKey函数查找
return lookupKey(db,key,flags);
}

robj *lookupKey(redisDb *db, robj *key, int flags) {
// 根据KEY从字典中进行查找
dictEntry *de = dictFind(db->dict,key->ptr);
// 如果不为空
if (de) {
robj *val = dictGetVal(de);
if (!hasActiveChildProcess() && !(flags & LOOKUP_NOTOUCH)){
if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {
updateLFU(val);
} else {
val->lru = LRU_CLOCK();
}
}
// 返回value
return val;
} else {
return NULL;
}
}

集群重定向

clusterRedirectClient

clusterRedirectClient用于集群重定向处理,在getNodeByQuery函数中,根据查询节点的情况对error_code设置了不同的值,在clusterRedirectClient函数中可以看到对error_code的判断,根据error_code的不同,向客户端响应不同的内容:

  1. 如果error_code是CLUSTER_REDIR_CROSS_SLOT,表示请求中有多个KEY,但是KEY所属slot不在同一个slot中
  2. 如果error_code是CLUSTER_REDIR_UNSTABLE,表示请求中有多个KEY并且在一个slot,但是数据可能正在迁入或迁出的过程中,节点中有缺失的KEY,slot处于一个不稳定的状态
  3. 如果error_code是CLUSTER_REDIR_DOWN_STATE,表示节点处于下线状态
  4. 如果error_code是CLUSTER_REDIR_DOWN_RO_STATE,表示节点处于下线状态,只接收读命令
  5. 如果error_code是CLUSTER_REDIR_DOWN_UNBOUND,标识key未绑定到节点,也就是根据key所属slot未查询到节点
  6. 如果error_code是CLUSTER_REDIR_MOVED或者CLUSTER_REDIR_ASK:
    • CLUSTER_REDIR_MOVED表示key所属slot已从当前节点迁出,此时向客户端响应MOVED命令并将迁出后slot以及所在节点ip和端口返回
    • CLUSTER_REDIR_ASK表示key所属slot正在从当前节点迁出的过程中,请求中的key有可能一部分还未迁出,一部分已经迁出完毕,此时向客户端返回ASK命令,并将slot以及迁出到的目标节点的ip和端口返回
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
void clusterRedirectClient(client *c, clusterNode *n, int hashslot, int error_code) {
if (error_code == CLUSTER_REDIR_CROSS_SLOT) {
// 如果是CLUSTER_REDIR_CROSS_SLOT,向客户端回复key不在同一个slot中
addReplyError(c,"-CROSSSLOT Keys in request don't hash to the same slot");
} else if (error_code == CLUSTER_REDIR_UNSTABLE) {
/* 请求中有多个key并且在一个slot,但是数据可能正在迁入或迁出的过程中,slot并不稳定 */
addReplyError(c,"-TRYAGAIN Multiple keys request during rehashing of slot");
} else if (error_code == CLUSTER_REDIR_DOWN_STATE) {
// 节点处于下线状态
addReplyError(c,"-CLUSTERDOWN The cluster is down");
} else if (error_code == CLUSTER_REDIR_DOWN_RO_STATE) {
// 节点已经下线只接收读命令
addReplyError(c,"-CLUSTERDOWN The cluster is down and only accepts read commands");
} else if (error_code == CLUSTER_REDIR_DOWN_UNBOUND) {
// 如果是CLUSTER_REDIR_DOWN_UNBOUND,表示根据key所属slot未查询到节点
addReplyError(c,"-CLUSTERDOWN Hash slot not served");
} else if (error_code == CLUSTER_REDIR_MOVED ||
error_code == CLUSTER_REDIR_ASK)
{
/* 如果是MOVED或者ASK,需要进行请求重定向处理,向客户端返回ASK或者MOVED命令,并将目标节点的ip和端口返回 */
int use_pport = (server.tls_cluster &&
c->conn && connGetType(c->conn) != CONN_TYPE_TLS);
int port = use_pport && n->pport ? n->pport : n->port;
// 返回响应,包括ASK或者MOVED命令、slot信息、目标节点的ip端口
addReplyErrorSds(c,sdscatprintf(sdsempty(),
"-%s %d %s:%d",
(error_code == CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
hashslot, n->ip, port));
} else {
serverPanic("getNodeByQuery() unknown error.");
}
}

参考

极客时间 - Redis源码剖析与实战(蒋德钧)
zhaiguanjie-Redis源码剖析

Redis版本:redis-6.2.5

【Redis】集群数据迁移

Posted on 2022-05-26

Redis通过对KEY计算hash,将KEY映射到slot,集群中每个节点负责一部分slot的方式管理数据,slot最大个数为16384。
在集群节点对应的结构体变量clusterNode中可以看到slots数组,数组的大小为CLUSTER_SLOTS除以8,CLUSTER_SLOTS的值是16384:

1
2
3
4
5
6
7
8
9
#define CLUSTER_SLOTS 16384

typedef struct clusterNode {

unsigned char slots[CLUSTER_SLOTS/8];

// 省略...

} clusterNode;

因为一个字符占8位,所以数组个数为16384除以8,每一位可以表示一个slot,如果某一位的值为1,表示当前节点负责这一位对应的slot。

clusterState

clusterNode里面保存了节点相关的信息,集群数据迁移信息并未保存在clusterNode中,而是使用了clusterState结构体来保存:

  • migrating_slots_to数组: 记录当前节点负责的slot迁移到了哪个节点
  • importing_slots_from数组: 记录当前节点负责的slot是从哪个节点迁入的
  • slots数组:记录每个slot是由哪个集群节点负责的
  • slots_keys_count:slot中key的数量
  • slots_to_keys:是一个字典树,记录KEY和SLOT的对应关系
1
2
3
4
5
6
7
8
9
typedef struct clusterState {
clusterNode *myself; /* 当前节点自己 */
clusterNode *migrating_slots_to[CLUSTER_SLOTS];
clusterNode *importing_slots_from[CLUSTER_SLOTS];
clusterNode *slots[CLUSTER_SLOTS];
uint64_t slots_keys_count[CLUSTER_SLOTS];
rax *slots_to_keys;
// ...
} clusterState;

clusterState与clusterNode的关系

集群数据迁移

在手动进行数据迁移时,需要执行以下步骤:

  1. 在源节点和目标节点分别使用CLUSTER SETSLOT MIGRATING和CLUSTER SETSLOT IMPORTING标记slot迁出和迁入信息
  2. 在源节点使用CLUSTER GETKEYSINSLOT命令获取待迁出的KEY
  3. 在源节点执行MIGRATE命令进行数据迁移,MIGRATE既支持单个KEY的迁移,也支持多个KEY的迁移
  4. 在源节点和目标节点使用CLUSTER SETSLOT命令标记slot最终迁移节点

标记数据迁移节点

在进行数据迁移之前,首先在需要迁入的目标节点使用SETSLOT命令标记要将SLOT从哪个节点迁入到当前节点:

  • :哈希槽的值
  • :表示slot所在的节点
1
CLUSTER  SETSLOT  <slot>  IMPORTING  <node>

然后在源节点也就是slot所在节点使用MIGRATING命令将数据迁出到目标节点:

  • :哈希槽的值
  • :表示slot要迁出到的目标节点
1
CLUSTER  SETSLOT  <slot>  MIGRATING  <node>

比如slot1当前在node1中,需要将slot1迁出到node2,那么首先在nodd2上执行IMPORTING命令,标记slot准备从node1迁到当前节点node2中:

1
CLUSTER  SETSLOT  slot1  IMPORTING  node1

然后在node1中执行MIGRATING命令标记slot1需要迁移到node2:

1
CLUSTER  SETSLOT  slot1  MIGRATING  node2

clusterCommand
SETSLOT命令的处理在clusterCommand函数(cluster.c文件中)中:

  1. 校验当前节点是否是从节点,如果当前节点是从节点,返回错误,SETSLOT只能用于主节点
  2. 如果是migrating命令,表示slot需要从当前节点迁出到其他节点,处理如下:
    (1) 如果需要迁移的slot不在当前节点,返回错误
    (2)如果要迁移到的目标slot节点未查询到,返回错误
    (3)将当前节点的migrating_slots_to[slot]的值置为迁出到的目标节点,记录slot迁移到了哪个节点
  3. 如果是importing命令,表示slot需要从其他节点迁入到当前节点
    (1)如果要迁移的slot已经在当前节点,返回slot数据已经在当前节点的响应
    (2)由于importing需要从slot所在节点迁移到当前节点,如果未从集群中查询slot当前所在节点,返回错误信息
    (3)将当前节点的importing_slots_from[slot]置为slot所在节点,记录slot是从哪个节点迁入到当前节点的
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
void clusterCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}

if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
// ...
}
// ...
else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) { // 处理setslot命令
int slot;
clusterNode *n;
// 如果当前节点是从节点,返回错误,SETSLOT只能用于主节点
if (nodeIsSlave(myself)) {
addReplyError(c,"Please use SETSLOT only with masters.");
return;
}
// 查询slot
if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;
// 处理migrating迁出
if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
// 如果需要迁移的slot不在当前节点,返回错误
if (server.cluster->slots[slot] != myself) {
addReplyErrorFormat(c,"I'm not the owner of hash slot %u",slot);
return;
}
// 如果要迁移到的目标节点未查询到,返回错误
if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",
(char*)c->argv[4]->ptr);
return;
}
// 将当前节点的migrating_slots_to[slot]置为目标节点,记录slot要迁移到的节点
server.cluster->migrating_slots_to[slot] = n;
} else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) { // 处理importing迁入
// 如果要迁移的slot已经在当前节点
if (server.cluster->slots[slot] == myself) {
addReplyErrorFormat(c,
"I'm already the owner of hash slot %u",slot);
return;
}
// importing需要从slot所在节点迁移到当前节点,如果未从集群中查询slot当前所在节点,返回错误信息
if ((n = clusterLookupNode(c->argv[4]->ptr)) == NULL) {
addReplyErrorFormat(c,"I don't know about node %s",
(char*)c->argv[4]->ptr);
return;
}
// 记录slot是从哪个节点迁移过来的
server.cluster->importing_slots_from[slot] = n;
}
// 省略其他if else
// ...
else {
addReplyError(c,
"Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
return;
}
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
addReply(c,shared.ok);
}
// ...
else {
addReplySubcommandSyntaxError(c);
return;
}
}

获取待迁出的key

在标记完迁入、迁出节点后,就可以使用CLUSTER GETKEYSINSLOT命令获取待迁出的KEY:

:哈希槽的值

:迁出KEY的数量

1
CLUSTER  GETKEYSINSLOT  <slot>  <count>

getkeysinslot命令的处理也在clusterCommand函数中,处理逻辑如下:

  1. 从命令中解析slot的值以及count的值,count的值记为maxkeys,并校验合法性
  2. 调用countKeysInSlot函数获取slot中key的数量,与maxkeys对比,如果小于maxkeys,就将maxkeys的值更新为slot中key的数量
  3. 根据获取key的个数分配相应的内存空间
  4. 从slot中获取key并将数据返回给客户端
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
38
39
40
41
42
43
44
45
46
47
48
49
50
void clusterCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}

if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
// ...
}
// ...
else if (!strcasecmp(c->argv[1]->ptr,"getkeysinslot") && c->argc == 4) {
/* CLUSTER GETKEYSINSLOT <slot> <count> */
long long maxkeys, slot;
unsigned int numkeys, j;
robj **keys;
// 从命令中获取slot的值并转为长整型
if (getLongLongFromObjectOrReply(c,c->argv[2],&slot,NULL) != C_OK)
return;
// 从命令中获取key的最大个数并转为长整型
if (getLongLongFromObjectOrReply(c,c->argv[3],&maxkeys,NULL)
!= C_OK)
return;
// 如果slot的值小于0或者大于CLUSTER_SLOTS或者key的最大个数为0
if (slot < 0 || slot >= CLUSTER_SLOTS || maxkeys < 0) {
addReplyError(c,"Invalid slot or number of keys");
return;
}

// 计算slot中key的数量
unsigned int keys_in_slot = countKeysInSlot(slot);
// 如果maxkeys大于slot中key的数量,更新maxkeys的值为slot中key的数量
if (maxkeys > keys_in_slot) maxkeys = keys_in_slot;
// 分配空间
keys = zmalloc(sizeof(robj*)*maxkeys);
// 从slot中获取key
numkeys = getKeysInSlot(slot, keys, maxkeys);
addReplyArrayLen(c,numkeys);
for (j = 0; j < numkeys; j++) {
// 返回key
addReplyBulk(c,keys[j]);
decrRefCount(keys[j]);
}
zfree(keys);
}
// ...
else {
addReplySubcommandSyntaxError(c);
return;
}
}

数据迁移

源节点数据迁移

完成上两步之后,接下来需要在源节点中执行MIGRATE命令进行数据迁移,MIGRATE既支持单个KEY的迁移,也支持多个KEY的迁移,语法如下:

1
2
3
4
5
# 单个KEY
MIGRATE host port key dbid timeout [COPY | REPLACE | AUTH password | AUTH2 username password]

# 多个KEY
MIGRATE host port "" dbid timeout [COPY | REPLACE | AUTH password | AUTH2 username password] KEYS key2 ... keyN
  • host:ip地址
  • Port:端口
  • key:迁移的key
  • KEYS:如果一次迁移多个KEY,使用KEYS,后跟迁移的key1 … keyN
  • dbid:数据库id
  • COPY:如果目标节点已经存在迁移的key,则报错,如果目标节点不存在迁移的key,则正常进行迁移,在迁移完成后删除源节点中的key
  • REPLACE:如果目标节点不存在迁移的key,正常进行迁移,如果目标节点存在迁移的key,进行替换,覆盖目标节点中已经存在的key
  • AUTH:验证密码

migrateCommand

MIGRATE命令对应的处理函数在migrateCommand中(cluster.c文件中),处理逻辑如下:

  1. 解析命令中的参数,判断是否有replace、auth、keys等参数
    • 如果有replace参数,表示在迁移数据时如果key已经在目标节点存在,进行替换
    • 如果有keys参数,表示命令中有多个key,计算命令中key的个数记为num_keys
  2. 处理命令中解析到的所有key,调用lookupKeyRead函数查找key:
    • 如果查找到,将key放入kv对象中,kv中存储实际要处理的KEY,value放入ov对象中,ov中存储key对应的value
    • 如果未查找到key,跳过当前key,处理下一个key
  3. 因为有部分key可能未查询到,所以更新实际需要处理的key的数量num_keys
  4. 根据命令中的ip端口信息,与目标节点建立连接
  5. 调用rioInitWithBuffer函数初始化一块缓冲区
  6. 处理实际需要迁移的key,主要是将数据填入缓冲区
    • 根据key获取过期时间,如果已过期不进行处理
    • 判断是否开启了集群,如果开启了集群将RESTORE-ASKING写入缓冲区,如果未开启,写入RESTORE命令
    • 将key写入缓冲区
    • 调用createDumpPayload函数,创建payload,将RDB版本、CRC64校验和以及value内容写入 ,目标节点收到数据时需要进行校验
    • 将payload数据填充到缓冲区
  7. 将缓冲区的数据按照64K的块大小发送到目标节点
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
void migrateCommand(client *c) {

// 省略...

robj **ov = NULL; /* 保存要迁移的key对应的value */
robj **kv = NULL; /* 保存要迁移的key. */

int first_key = 3; /* 第一个key */
int num_keys = 1; /* 迁移key的数量 */

/* 解析命令中的参数 */
for (j = 6; j < c->argc; j++) {
int moreargs = (c->argc-1) - j;
// 如果是copy
if (!strcasecmp(c->argv[j]->ptr,"copy")) {
copy = 1;
} else if (!strcasecmp(c->argv[j]->ptr,"replace")) { // 如果是replace
replace = 1;
} else if (!strcasecmp(c->argv[j]->ptr,"auth")) { // 如果需要验证密码
if (!moreargs) {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
j++;
// 获取密码
password = c->argv[j]->ptr;
redactClientCommandArgument(c,j);
} else if (!strcasecmp(c->argv[j]->ptr,"auth2")) {
// ...
} else if (!strcasecmp(c->argv[j]->ptr,"keys")) { // 如果一次迁移多个key
if (sdslen(c->argv[3]->ptr) != 0) {
addReplyError(c,
"When using MIGRATE KEYS option, the key argument"
" must be set to the empty string");
return;
}
// 或取第一个key
first_key = j+1;
// 计算key的数量
num_keys = c->argc - j - 1;
break; /* All the remaining args are keys. */
} else {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
}

/* 校验timeout和dbid的值 */
if (getLongFromObjectOrReply(c,c->argv[5],&timeout,NULL) != C_OK ||
getLongFromObjectOrReply(c,c->argv[4],&dbid,NULL) != C_OK)
{
return;
}
// 如果超时时间小于0,默认设置1000毫秒
if (timeout <= 0) timeout = 1000;

// 分配空间,kv记录在源节点中实际查找到的key
ov = zrealloc(ov,sizeof(robj*)*num_keys);
kv = zrealloc(kv,sizeof(robj*)*num_keys);
int oi = 0;
// 处理KEY
for (j = 0; j < num_keys; j++) {
// 如果可以从源节点查找到key
if ((ov[oi] = lookupKeyRead(c->db,c->argv[first_key+j])) != NULL) {
// 记录查找到的key
kv[oi] = c->argv[first_key+j];
// 记录查找到的个数
oi++;
}
}
// 只处理实际查找到的key
num_keys = oi;
// 如果为0,不进行处理
if (num_keys == 0) {
zfree(ov); zfree(kv); // 释放空间
addReplySds(c,sdsnew("+NOKEY\r\n")); // 返回NOKEY响应
return;
}

try_again:
write_error = 0;

/* 与目标节点建立连接 */
cs = migrateGetSocket(c,c->argv[1],c->argv[2],timeout);
if (cs == NULL) {
zfree(ov); zfree(kv);
return; /* error sent to the client by migrateGetSocket() */
}
// 初始化缓冲区
rioInitWithBuffer(&cmd,sdsempty());

/* 如果密码不为空,验证密码 */
if (password) {
int arity = username ? 3 : 2;
serverAssertWithInfo(c,NULL,rioWriteBulkCount(&cmd,'*',arity));
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"AUTH",4));
if (username) {
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,username,
sdslen(username)));
}
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,password,
sdslen(password)));
}

// ...

// 处理KEY,只保留未过期的KEY
for (j = 0; j < num_keys; j++) {
long long ttl = 0;
// 获取KEY的过期时间,返回-1表示未设置过期时间,否则返回过期时间
long long expireat = getExpire(c->db,kv[j]);
// 如果设置了过期时间
if (expireat != -1) {
// 计算ttl:过期时间减去当前时间
ttl = expireat-mstime();
// 如果已过期
if (ttl < 0) {
continue;
}
if (ttl < 1) ttl = 1;
}

/* 记录未过期的KEY */
ov[non_expired] = ov[j];
kv[non_expired++] = kv[j];

serverAssertWithInfo(c,NULL,
rioWriteBulkCount(&cmd,'*',replace ? 5 : 4));
// 是否启用集群
if (server.cluster_enabled)
serverAssertWithInfo(c,NULL,
rioWriteBulkString(&cmd,"RESTORE-ASKING",14)); // 将RESTORE-ASKING命令写入缓冲区
else
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"RESTORE",7)); // 如果未开启集群将RESTORE命令写入缓冲区
serverAssertWithInfo(c,NULL,sdsEncodedObject(kv[j]));
// 将key写入缓冲区
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,kv[j]->ptr,
sdslen(kv[j]->ptr)));
// 将ttl写入缓存区
serverAssertWithInfo(c,NULL,rioWriteBulkLongLong(&cmd,ttl));

/* 创建payload,将RDB版本、CRC64校验和以及value内容写入 */
createDumpPayload(&payload,ov[j],kv[j]);
// 将payload数据写入缓冲区
serverAssertWithInfo(c,NULL,
rioWriteBulkString(&cmd,payload.io.buffer.ptr,
sdslen(payload.io.buffer.ptr)));
sdsfree(payload.io.buffer.ptr);

/* 如果设置了REPLACE参数,将REPLACE写入缓冲区 */
if (replace)
serverAssertWithInfo(c,NULL,rioWriteBulkString(&cmd,"REPLACE",7));
}

/* 更新实际需要处理的key */
num_keys = non_expired;

/* 将缓冲区的数据按照64K的块大小发送到目标节点 */
errno = 0;
{
sds buf = cmd.io.buffer.ptr;
size_t pos = 0, towrite;
int nwritten = 0;

while ((towrite = sdslen(buf)-pos) > 0) {
// 需要发送的数据,如果超过了64K就按照64K的大小发送
towrite = (towrite > (64*1024) ? (64*1024) : towrite);
// 发送数据
nwritten = connSyncWrite(cs->conn,buf+pos,towrite,timeout);
if (nwritten != (signed)towrite) {
write_error = 1;
goto socket_err;
}
pos += nwritten;
}
}

// 省略...
}

createDumpPayload

createDumpPayload函数在cluster.c文件中:

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
/* -----------------------------------------------------------------------------
* DUMP, RESTORE and MIGRATE commands
* -------------------------------------------------------------------------- */
void createDumpPayload(rio *payload, robj *o, robj *key) {
unsigned char buf[2];
uint64_t crc;
// 初始化缓冲区
rioInitWithBuffer(payload,sdsempty());
// 将value的数据类型写入缓冲区
serverAssert(rdbSaveObjectType(payload,o));
// 将value写入缓冲区
serverAssert(rdbSaveObject(payload,o,key));

/* Write the footer, this is how it looks like:
* ----------------+---------------------+---------------+
* ... RDB payload | 2 bytes RDB version | 8 bytes CRC64 |
* ----------------+---------------------+---------------+
* RDB version and CRC are both in little endian.
*/

/* 设置RDB版本 */
buf[0] = RDB_VERSION & 0xff;
buf[1] = (RDB_VERSION >> 8) & 0xff;
payload->io.buffer.ptr = sdscatlen(payload->io.buffer.ptr,buf,2);

/* 设置CRC64校验和用于校验数据 */
crc = crc64(0,(unsigned char*)payload->io.buffer.ptr,
sdslen(payload->io.buffer.ptr));
memrev64ifbe(&crc);
payload->io.buffer.ptr = sdscatlen(payload->io.buffer.ptr,&crc,8);
}

目标节点处理数据

restoreCommand

目标节点收到迁移的数据的处理逻辑在restoreCommand中(cluster.c文件中):

  1. 解析请求中的参数,判断是否有replace
  2. 如果没有replace并且key已经在当前节点存在,返回错误信息
  3. 调用verifyDumpPayload函数校验RDB版本和CRC校验和
  4. 从请求中解析value的数据类型和value值
  5. 如果设置了replace先删除数据库中存在的key
  6. 将key和vlaue添加到节点的数据库中
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
/* RESTORE key ttl serialized-value [REPLACE] */
void restoreCommand(client *c) {
long long ttl, lfu_freq = -1, lru_idle = -1, lru_clock = -1;
rio payload;
int j, type, replace = 0, absttl = 0;
robj *obj;

/* 解析请求中的参数 */
for (j = 4; j < c->argc; j++) {
int additional = c->argc-j-1;
if (!strcasecmp(c->argv[j]->ptr,"replace")) { // 如果有replace
replace = 1; // 标记
}
// ...
else {
addReplyErrorObject(c,shared.syntaxerr);
return;
}
}

/* 如果没有replace并且key已经在数据库存在,返回错误信息 */
robj *key = c->argv[1];
if (!replace && lookupKeyWrite(c->db,key) != NULL) {
addReplyErrorObject(c,shared.busykeyerr);
return;
}

/* Check if the TTL value makes sense */
if (getLongLongFromObjectOrReply(c,c->argv[2],&ttl,NULL) != C_OK) {
return;
} else if (ttl < 0) {
addReplyError(c,"Invalid TTL value, must be >= 0");
return;
}

/* 校验RDB版本和CRC */
if (verifyDumpPayload(c->argv[3]->ptr,sdslen(c->argv[3]->ptr)) == C_ERR)
{
addReplyError(c,"DUMP payload version or checksum are wrong");
return;
}

rioInitWithBuffer(&payload,c->argv[3]->ptr);
// 解析value的数据类型和value值
if (((type = rdbLoadObjectType(&payload)) == -1) ||
((obj = rdbLoadObject(type,&payload,key->ptr)) == NULL))
{
addReplyError(c,"Bad data format");
return;
}

int deleted = 0;
// 如果设置了replace
if (replace)
deleted = dbDelete(c->db,key); // 先删除数据库中存在的key
if (ttl && !absttl) ttl+=mstime();
if (ttl && checkAlreadyExpired(ttl)) {
if (deleted) {
rewriteClientCommandVector(c,2,shared.del,key);
signalModifiedKey(c,c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"del",key,c->db->id);
server.dirty++;
}
decrRefCount(obj);
addReply(c, shared.ok);
return;
}

/* 将key和vlaue添加到节点的数据库中 */
dbAdd(c->db,key,obj);
if (ttl) {
setExpire(c,c->db,key,ttl);
}
objectSetLRUOrLFU(obj,lfu_freq,lru_idle,lru_clock,1000);
signalModifiedKey(c,c->db,key);
notifyKeyspaceEvent(NOTIFY_GENERIC,"restore",key,c->db->id);
addReply(c,shared.ok);
server.dirty++;
}

标记迁移结果

数据迁移的最后一步,需要使用CLUSTER SETSLOT命令,在源节点和目标节点执行以下命令,标记slot最终所属的节点,并清除第一步中标记的迁移信息:

:哈希槽

:哈希槽最终所在节点id

1
CLUSTER SETSLOT <slot> NODE <node>

clusterCommand

CLUSTER SETSLOT <slot> NODE <node>命令的处理依旧在clusterCommand函数中,处理逻辑如下:

  1. 根据命令中传入的nodeid查找节点记为n,如果未查询到,返回错误信息
  2. 果slot已经在当前节点,但是根据nodeid查找到的节点n不是当前节点,说明slot所属节点与命令中指定的节点不一致,返回错误信息
  3. 在源节点上执行命令时,如果slot中key的数量为0,表示slot上的数据都已迁移完毕,而migrating_slots_to[slot]记录了slot迁移到的目标节点,既然数据已经迁移完成此时需要将migrating_slots_to[slot]迁出信息清除
  4. 调用clusterDelSlot函数先将slot删除
    • 获取slot所属节点
    • 将slot所属节点ClusterNode结构体中的slots数组对应的标记位取消,表示节点不再负责此slot
    • 将slot所属节点ClusterState结构体中的slots数组对应元素置为NULL,表示当前slot所属节点为空
  5. 调用clusterAddSlot将slot添加到最终所属的节点中
  6. 在目标节点上执行命令时,如果slot所属节点为当前节点,并且importing_slots_from[slot]不为空, importing_slots_from[slot]中记录了slot是从哪个节点迁移过来,此时数据已经迁移完毕,清除 importing_slots_from[slot]中的迁入信息
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
void clusterCommand(client *c) {
if (server.cluster_enabled == 0) {
addReplyError(c,"This instance has cluster support disabled");
return;
}

if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {
// ...
}
// ...
else if (!strcasecmp(c->argv[1]->ptr,"setslot") && c->argc >= 4) { // 处理setslot命令
int slot;
clusterNode *n;

if (nodeIsSlave(myself)) {
addReplyError(c,"Please use SETSLOT only with masters.");
return;
}

if ((slot = getSlotOrReply(c,c->argv[2])) == -1) return;
if (!strcasecmp(c->argv[3]->ptr,"migrating") && c->argc == 5) {
// migrating处理
// ...
} else if (!strcasecmp(c->argv[3]->ptr,"importing") && c->argc == 5) {
// importing处理
// ...
} else if (!strcasecmp(c->argv[3]->ptr,"stable") && c->argc == 4) {
// stable处理
// ...
} else if (!strcasecmp(c->argv[3]->ptr,"node") && c->argc == 5) {
/* CLUSTER SETSLOT <SLOT> NODE <NODE ID> 命令处理 */
// 根据nodeid查找节点
clusterNode *n = clusterLookupNode(c->argv[4]->ptr);
// 如果未查询到,返回错误信息
if (!n) {
addReplyErrorFormat(c,"Unknown node %s",
(char*)c->argv[4]->ptr);
return;
}
/* 如果slot已经在当前节点,但是根据node id查找到的节点不是当前节点,返回错误信息*/
if (server.cluster->slots[slot] == myself && n != myself) {
if (countKeysInSlot(slot) != 0) {
addReplyErrorFormat(c,
"Can't assign hashslot %d to a different node "
"while I still hold keys for this hash slot.", slot);
return;
}
}

/* 在源节点上执行命令时 */
/* 如果slot中key的数量为0,表示slot上的数据都已迁移完毕,而migrating_slots_to[slot]记录了slot迁移到的目标节点,既然数据已经迁移完成此时可以将迁移信息清除*/
if (countKeysInSlot(slot) == 0 &&
server.cluster->migrating_slots_to[slot])
server.cluster->migrating_slots_to[slot] = NULL;// 清除迁移信息
// 先删除slot
clusterDelSlot(slot);
// 添加slot到节点n
clusterAddSlot(n,slot);

/* 在目标节点上执行命令时 */
/* 如果slot所属节点为当前节点,并且importing_slots_from[slot]不为空, importing_slots_from[slot]中记录了slot是从哪个节点迁移过来*/
if (n == myself &&
server.cluster->importing_slots_from[slot])
{
/* 更新节点的configEpoch */
if (clusterBumpConfigEpochWithoutConsensus() == C_OK) {
serverLog(LL_WARNING,
"configEpoch updated after importing slot %d", slot);
}
// 清除importing_slots_from[slot]迁移信息
server.cluster->importing_slots_from[slot] = NULL;
/* 广播PONG消息,让其他节点尽快知道slot的最新信息 */
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);
}
} else {
addReplyError(c,
"Invalid CLUSTER SETSLOT action or number of arguments. Try CLUSTER HELP");
return;
}
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_UPDATE_STATE);
addReply(c,shared.ok);
}
// ...
else {
addReplySubcommandSyntaxError(c);
return;
}
}

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】集群故障转移

Posted on 2022-05-19

集群故障转移

节点下线

在集群定时任务clusterCron中,会遍历集群中的节点,对每个节点进行检查,判断节点是否下线。与节点下线相关的状态有两个,分别为CLUSTER_NODE_PFAIL和CLUSTER_NODE_FAIL。

CLUSTER_NODE_PFAIL:当前节点认为某个节点下线时,会将节点状态改为CLUSTER_NODE_PFAIL,由于可能存在误判,所以需要根据集群中的其他节点共同决定是否真的将节点标记为下线状态,CLUSTER_NODE_PFAIL可以理解为疑似下线,类似哨兵集群中的主观下线。

CLUSTER_NODE_FAIL:集群中有过半的节点标认为节点已下线,此时将节点置为CLUSTER_NODE_FAIL标记节点下线,CLUSTER_NODE_FAIL表示节点真正处于下线状态,类似哨兵集群的客观下线。

1
2
#define CLUSTER_NODE_PFAIL 4      /* 疑似下线,需要根据其他节点的判断决定是否下线,类似主观下线 */
#define CLUSTER_NODE_FAIL 8 /* 节点处于下线状态,类似客观下线 */

疑似下线(PFAIL)

在集群定时任务遍历集群中的节点进行检查时,遍历到的每个节点记为node,当前节点记为myself,检查的内容主要有以下几个方面:

一、判断孤立主节点的个数

如果当前节点myself是从节点,正在遍历的节点node是主节点,并且node节点不处于下线状态,会判断孤立节点的个数,满足以下三个条件时,认定node是孤立节点,孤立节点个数增1:

  1. node的从节点中处于非下线状态的节点个数为0
  2. node负责的slot数量大于0,
  3. node节点处于CLUSTER_NODE_MIGRATE_TO状态

二、检查连接

这一步主要检查和节点间的连接是否正常,有可能节点处于正常状态,但是连接有问题,此时需要释放连接,在下次执行定时任务时会进行重连,释放连接需要同时满足以下几个条件:

  1. 与节点node之间的连接不为空,说明之前进行过连接
  2. 当前时间距离连接创建的时间超过了超时时间
  3. 距离向node发送PING消息的时间已经超过了超时时间的一半
  4. 距离收到node节点发送消息的时间超过了超时时间的一半

三、疑似下线判断

ping_delay记录了当前时间距离向node节点发送PING消息的时间,data_delayd记录了node节点向当前节点最近一次发送消息的时间,从ping_delay和data_delay中取较大的那个作为延迟时间。

如果延迟时间大于超时时间,判断node是否已经处于CLUSTER_NODE_PFAIL或者CLUSTER_NODE_FAIL状态,如果都不处于,将节点状态置为CLUSTER_NODE_PFAIL,认为节点疑似下线。

也就是说如果在规定的超时时间内,当前节点长时间未向node节点发送PING消息,或者长时间未收到node节点向当前节点发送的消息,当前节点就认为node疑似下线状态。

上述检查完成之后,会判断当前节点是否是从节点,如果不处于CLUSTER_MODULE_FLAG_NO_FAILOVER状态,调用clusterHandleSlaveFailover处理故障转移,不过需要注意此时只是将节点置为疑似下线,并不满足故障转移条件,需要等待节点被置为FAIL下线状态之后,再次执行集群定时任务进入到clusterHandleSlaveFailover函数中才可以开始处理故障转移。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
void clusterCron(void) {

// ...

orphaned_masters = 0;
max_slaves = 0;
this_slaves = 0;
di = dictGetSafeIterator(server.cluster->nodes);
// 遍历集群中的节点
while((de = dictNext(di)) != NULL) {
// 获取节点
clusterNode *node = dictGetVal(de);
now = mstime(); /* 当前时间 */

if (node->flags &
(CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR|CLUSTER_NODE_HANDSHAKE))
continue;

/* 如果当前节点myself是从节点,正在遍历的节点node是主节点,并且node节点不处于下线状态 */
if (nodeIsSlave(myself) && nodeIsMaster(node) && !nodeFailed(node)) {
// 获取不处于下线状态的从节点数量
int okslaves = clusterCountNonFailingSlaves(node);

/* 如果处于正常状态的从节点数量为0、node负责的slot数量大于0, 并且节点处于CLUSTER_NODE_MIGRATE_TO状态 */
if (okslaves == 0 && node->numslots > 0 &&
node->flags & CLUSTER_NODE_MIGRATE_TO)
{
orphaned_masters++; // 孤立主节点数量加1
}
// 更新最大从节点数量
if (okslaves > max_slaves) max_slaves = okslaves;
// 如果myself是从节点 并且myself是node的从节点
if (nodeIsSlave(myself) && myself->slaveof == node)
this_slaves = okslaves; // 记录处于正常状态的从节点数量
}

/* 这一步主要检查连接是否出现问题 */
mstime_t ping_delay = now - node->ping_sent; // 当前时间减去发送PING消息时间
mstime_t data_delay = now - node->data_received; // 当前时间减去收到node向当前节点发送消息的时间
if (node->link && /* 如果连接不为空 */
now - node->link->ctime >
server.cluster_node_timeout && /* 距离连接创建的时间超过了设置的超时时间 */
node->ping_sent && /* 发送过PING消息 */
/* 距离发送PING消息的时间已经超过了超时时间的一半 */
ping_delay > server.cluster_node_timeout/2 &&
/* 距离收到node节点发送消息的时间超过了超时时间的一半
*/
data_delay > server.cluster_node_timeout/2)
{
/* 断开连接,在下次执行定时任务时会重新连接 */
freeClusterLink(node->link);
}

/* 如果连接不为空、ping_sent为0(收到PONG消息后会将ping_sent置为0),并且当前时间减去收到node的PONG消息的时间大于超时时间的一半 */
if (node->link &&
node->ping_sent == 0 &&
(now - node->pong_received) > server.cluster_node_timeout/2)
{
// 立即发送PING消息,保持连接
clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
continue;
}

if (server.cluster->mf_end && // 手动执行故障转移时间限制不为0,表示正在执行手动故障转移
nodeIsMaster(myself) && // 如果myself是主节点
server.cluster->mf_slave == node && // 如果node是myself从节点并且正在执行手动故障转移
node->link)
{
// 发送PING消息,保持连接
clusterSendPing(node->link, CLUSTERMSG_TYPE_PING);
continue;
}

/* 如果没有活跃的PING消息. */
if (node->ping_sent == 0) continue;

/* 校验节点是否主观下线 */
// ping_delay记录了当前时间距离向node节点发送PING消息的时间
// data_delay记录了node节点向当前节点最近一次发送消息的时间
// 从ping_delay和data_delay中取较大的那个作为延迟时间
mstime_t node_delay = (ping_delay < data_delay) ? ping_delay :
data_delay;
// 如果节点的延迟时间大于超时时间
if (node_delay > server.cluster_node_timeout) {
/* 如果不处于CLUSTER_NODE_PFAIL或者CLUSTER_NODE_FAIL状态*/
if (!(node->flags & (CLUSTER_NODE_PFAIL|CLUSTER_NODE_FAIL))) {
serverLog(LL_DEBUG,"*** NODE %.40s possibly failing",
node->name);
// 将节点标记为故障状态CLUSTER_NODE_PFAIL,标记疑似下线
node->flags |= CLUSTER_NODE_PFAIL;
update_state = 1;
}
}
}

// ...

// 如果是从节点
if (nodeIsSlave(myself)) {
clusterHandleManualFailover();
// 如果不处于CLUSTER_MODULE_FLAG_NO_FAILOVER状态
if (!(server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_FAILOVER))
clusterHandleSlaveFailover(); // 处理故障转移
if (orphaned_masters && max_slaves >= 2 && this_slaves == max_slaves &&
server.cluster_allow_replica_migration)
clusterHandleSlaveMigration(max_slaves);
}

// ...
}

下线(FAIL)

当前节点认为某个node下线时,会将node状态置为CLUSTER_NODE_PFAIL疑似下线状态,在定时向集群中的节点交换信息也就是发送PING消息时,消息体中记录了node的下线状态,其他节点在处理收到的PING消息时,会将认为node节点下线的那个节点加入到node的下线链表fail_reports中,并调用markNodeAsFailingIfNeeded函数判断是否有必要将节点置为下线FAIL状态:

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
38
39
40
41
42
43
44
void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
uint16_t count = ntohs(hdr->count);
// 获取clusterMsgDataGossip数据
clusterMsgDataGossip *g = (clusterMsgDataGossip*) hdr->data.ping.gossip;
// 发送消息的节点
clusterNode *sender = link->node ? link->node : clusterLookupNode(hdr->sender);

while(count--) {

/* 根据nodename查找节点,node指向当前收到消息节点中维护的节点*/
node = clusterLookupNode(g->nodename);
// 如果节点已知
if (node) {
/* 如果发送者是主节点 */
if (sender && nodeIsMaster(sender) && node != myself) {
// 如果gossip节点是FAIL或者PFAIL状态
if (flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) {
// 将sender加入到node节点的下线链表fail_reports中
if (clusterNodeAddFailureReport(node,sender)) {
serverLog(LL_VERBOSE,
"Node %.40s reported node %.40s as not reachable.",
sender->name, node->name);
}
// 判断是否需要将节点置为下线
markNodeAsFailingIfNeeded(node);
} else {
// 校验sender是否在下线节点链表fail_reports中,如果在需要移除恢复在线状态
if (clusterNodeDelFailureReport(node,sender)) {
serverLog(LL_VERBOSE,
"Node %.40s reported node %.40s is back online.",
sender->name, node->name);
}
}
}

// ...
} else { // 如果节点未知
// ...
}

/* 遍历下一个节点 */
g++;
}
}

markNodeAsFailingIfNeeded

markNodeAsFailingIfNeeded用于判断是否有必要将某个节点标记为FAIL状态:

  1. 计算quorum,为集群节点个数一半 + 1,记为needed_quorum
  2. 如果节点已经被置为FAIL状态,直接返回即可
  3. 调用clusterNodeFailureReportsCount函数,获取节点下线链表node->fail_reports中元素的个数,node->fail_reports链表中记录了认为node下线的节点个数,节点个数记为failures
  4. 如果当前节点是主节点,failures增1,表示当前节点也认为node需要置为下线状态
  5. 判断是否有过半的节点认同节点下线,也就是failures大于等于needed_quorum,如果没有过半的节点认同node需要下线,直接返回即可
  6. 如果有过半的节点认同node需要下线,此时取消节点的疑似下线标记PFAIL状态,将节点置为FAIL状态
  7. 在集群中广播节点的下线消息,以便让其他节点知道该节点已经下线
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
void markNodeAsFailingIfNeeded(clusterNode *node) {
int failures;
// 计算quorum,为集群节点个数一半 + 1
int needed_quorum = (server.cluster->size / 2) + 1;

if (!nodeTimedOut(node)) return; /* We can reach it. */
// 如果节点已经处于下线状态
if (nodeFailed(node)) return; /* Already FAILing. */
// 从失败报告中获取认为节点已经下线的节点数量
failures = clusterNodeFailureReportsCount(node);
/* 如果当前节点是主节点 */
if (nodeIsMaster(myself)) failures++; // 认定下线的节点个数+1
// 如果没有过半的节点认同节点下线,返回即可
if (failures < needed_quorum) return;

serverLog(LL_NOTICE,
"Marking node %.40s as failing (quorum reached).", node->name);

/* 标记节点下线 */
// 取消CLUSTER_NODE_PFAIL状态
node->flags &= ~CLUSTER_NODE_PFAICLUSTER_NODE_PFAIL;
// 设置为下线状态
node->flags |= CLUSTER_NODE_FAIL;
node->fail_time = mstime();

/* 广播下线消息到集群中的节点,以便让其他节点知道该节点已经下线 */
clusterSendFail(node->name);
clusterDoBeforeSleep(CLUSTER_TODO_UPDATE_STATE|CLUSTER_TODO_SAVE_CONFIG);
}

/* 返回下线报告链表中*/
int clusterNodeFailureReportsCount(clusterNode *node) {
clusterNodeCleanupFailureReports(node);
// 返回认为node下线的节点个数
return listLength(node->fail_reports);
}

故障转移处理

clusterHandleSlaveFailover

由上面的内容可知,节点客观下线时会被置为CLUSTER_NODE_FAIL状态,下次执行集群定时任务时,在故障转移处理函数clusterHandleSlaveFailover中,就可以根据状态来检查是否需要执行故障转移。

不过在看clusterHandleSlaveFailover函数之前,先看一下clusterState中和选举以及故障切换相关的变量定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
typedef struct clusterState {

// ...

mstime_t failover_auth_time; /* 发起选举的时间 */
int failover_auth_count; /* 目前为止收到投票的数量 */
int failover_auth_sent; /* 是否发起了投票,如果已经发起,值大于0 */
int failover_auth_rank; /* 从节点排名 */
uint64_t failover_auth_epoch; /* 当前选举的纪元 */
int cant_failover_reason; /* 从节点不能执行故障转移的原因 */
mstime_t mf_end; /* 手动执行故障转移时间限制,如果未设置值为0 */
clusterNode *mf_slave; /* 执行手动故障切换的从节点 */

//...
} clusterState;

clusterHandleSlaveFailover函数中的一些变量

data_age:记录从节点最近一次与主节点进行数据同步的时间。如果与主节点处于连接状态,用当前时间减去最近一次与master节点交互的时间,否则使用当前时间减去与master主从复制中断的时间。

auth_age:当前时间减去发起选举的时间,也就是距离发起选举过去了多久,用于判断选举超时、是否重新发起选举使用。

needed_quorum:quorum的数量,为集群中节点的数量的一半再加1。

auth_timeout:等待投票超时时间。

auth_retry_time:等待重新发起选举进行投票的时间,也就是重试时间。

发起选举

一、故障转移条件检查

首先进行了一些条件检查,用于判断是否有必要执行故障转移,如果处于以下几个条件之一,将会跳出函数,结束故障转移处理:

  1. 当前节点myself是master节点,因为如果需要进行故障转移一般是master节点被标记为下线,需要从它所属的从节点中选举节点作为新的master节点,这个需要从节点发起选举,所以如果当前节点是主节点,不满足进行故障转移的条件。

  2. 当前节点myself所属的主节点为空

  3. 当前节点myself所属主节点不处于客观下线状态并且不是手动进行故障转移,可以看到这里使用的是CLUSTER_NODE_FAIL状态来判断的

    1
    #define nodeFailed(n) ((n)->flags & CLUSTER_NODE_FAIL)
  4. 如果开启了不允许从节点执行故障切换并且当前不是手动进行故障转移

  5. 当前节点myself所属主节点负责的slot数量为0

二、主从复制进度校验

cluster_slave_validity_factor设置了故障切换最大主从复制延迟时间因子,如果不为0需要校验主从复制延迟时间是否符合要求。

如果主从复制延迟时间data_age大于 mater向从节点发送PING消息的周期 + 超时时间 * 故障切换主从复制延迟时间因子并且不是手动执行故障切换,表示主从复制延迟过大,不能进行故障切换终止执行。

三、是否需要重新发起选举

如果距离上次发起选举的时间大于超时重试时间,表示可以重新发起投票。

  1. 设置本轮选举发起时间,并没有直接使用当前时间,而是使用了当前时间 + 500毫秒 + 随机值(0到500毫秒之间)进行了一个延迟,以便让上一次失败的消息尽快传播。

  2. 重置获取的投票数量failover_auth_count和是否已经发起选举failover_auth_sent为0,等待下一次执行clusterHandleSlaveFailover函数时重新发起投票。

  3. 获取当前节点在所属主节点的所有从节点中的等级排名,再次更新发起选举时间,加上当前节点的rank * 1000,以便让等级越低(rank值越高)的节点,越晚发起选举,降低选举的优先级。

    注意这里并没有恢复CLUSTER_TODO_HANDLE_FAILOVER状态,因为发起投票的入口是在集群定时任务clusterCron函数中,所以不需要恢复。

  4. 如果是手动进行故障转移,不需要设置延迟时间,直接使用当前时间,rank设置为0,然后将状态置为CLUSTER_TODO_HANDLE_FAILOVER,在下一次执行beforeSleep函数时,重新进行故障转移。

  5. 向集群中广播消息并终止执行本次故障切换。

四、延迟发起选举

  1. 如果还未发起选举投票,节点等级有可能在变化,所以此时需要更新等级以及发起投票的延迟时间。
  2. 如果当前时间小于设置的选举发起时间,需要延迟发起选举,直接返回,等待下一次执行。
  3. 如果距离发起选举的时间大于超时时间,表示本次选举已超时,直接返回。

五、发起投票

如果满足执行故障的条件,接下来需从节点想集群中的其他节点广播消息,发起投票,不过只有主节点才有投票权。failover_auth_sent为0表示还未发起投票,此时开始发起投票:

  1. 更新节点当前的投票纪元(轮次)currentEpoch,对其进行增1操作
  2. 设置本次选举的投票纪元(轮次)failover_auth_epoch,与currentEpoch一致
  3. 向集群广播,发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息到其他节点进行投票
  4. failover_auth_sent置为1 ,表示已经发起了投票
  5. 发起投票后,直接返回,等待其他节点的投票。

六、执行故障切换

当某个节点获取到了集群中大多数节点的投票,即可进行故障切换,这里先不关注,在后面的章节会讲。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
void clusterHandleSlaveFailover(void) {
// 主从复制延迟时间
mstime_t data_age;
// 当前时间减去发起选举的时间
mstime_t auth_age = mstime() - server.cluster->failover_auth_time;
// 计算quorum的数量,为集群中节点的数量的一半再加1
int needed_quorum = (server.cluster->size / 2) + 1;
// 是否手动执行故障转移
int manual_failover = server.cluster->mf_end != 0 &&
server.cluster->mf_can_start;
// 等待投票超时时间,等待重试时间
mstime_t auth_timeout, auth_retry_time;
// 取消CLUSTER_TODO_HANDLE_FAILOVER状态
server.cluster->todo_before_sleep &= ~CLUSTER_TODO_HANDLE_FAILOVER;

// 等待投票超时时间为集群中设置的超时时间的2倍
auth_timeout = server.cluster_node_timeout*2;
// 如果等待投票超时的时间小于2000毫秒,设置为2000毫秒,也就是超时时间最少为2000毫秒
if (auth_timeout < 2000) auth_timeout = 2000;
// 等待重试时间为超时时间的2倍
auth_retry_time = auth_timeout*2;

/* 校验故障转移条件,处于以下条件之一不满足故障切换条件,跳出函数 */
if (nodeIsMaster(myself) || // myself是主节点
myself->slaveof == NULL || // myself是从节点但是所属主节点为空
(!nodeFailed(myself->slaveof) && !manual_failover) || // 所属主节点不处于下线状态并且不是手动进行故障转移
(server.cluster_slave_no_failover && !manual_failover) || // 如果不允许从节点执行故障切换并且不是手动进行故障转移
myself->slaveof->numslots == 0) // 所属主节点负责的slot数量为0
{
/* 不进行故障切换 */
server.cluster->cant_failover_reason = CLUSTER_CANT_FAILOVER_NONE;
return;
}

/* 如果主从复制状态为连接状态 */
if (server.repl_state == REPL_STATE_CONNECTED) {
// 设置距离最近一次复制数据的时间,由于和master节点还处于连接状态,使用当前时间减去最近一次与master节点交互的时间
data_age = (mstime_t)(server.unixtime - server.master->lastinteraction)
* 1000;
} else { // 其他状态时
// 使用当前时间减去与master主从复制中断的时间
data_age = (mstime_t)(server.unixtime - server.repl_down_since) * 1000;
}

/* 如果data_age大于超时时间,减去超时时间 */
if (data_age > server.cluster_node_timeout)
data_age -= server.cluster_node_timeout;

/* cluster_slave_validity_factor设置了故障切换最大主从复制延迟时间因子,如果不为0需要校验主从复制延迟时间是否符合要求 */
/* 如果主从复制延迟时间 大于(master向从节点发送PING消息的周期 + 超时时间 * 故障切换主从复制延迟时间因子) ,表示主从复制延迟过大,不能进行故障切换 */
if (server.cluster_slave_validity_factor &&
data_age >
(((mstime_t)server.repl_ping_slave_period * 1000) +
(server.cluster_node_timeout * server.cluster_slave_validity_factor)))
{
// 如果不是手动执行故障切换
if (!manual_failover) {
// 设置不能执行故障切换的原因,主从复制进度不符合要求
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_DATA_AGE);
return;
}
}

/* 如果距离上次发起选举的时间大于超时重试时间,表示可以重新发起投票 */
if (auth_age > auth_retry_time) {
// 设置本轮选举发起时间,使用了当前时间 + 500毫秒 + 随机值(0到500毫秒之间),以便让上一次失败的消息尽快传播
server.cluster->failover_auth_time = mstime() +
500 +
random() % 500;
// 初始化获取的投票数量
server.cluster->failover_auth_count = 0;
// 初始化failover_auth_sent为0
server.cluster->failover_auth_sent = 0;
// 获取当前节点的等级
server.cluster->failover_auth_rank = clusterGetSlaveRank();
// 再次更新发起选举时间,加上当前节点的rank * 1000,以便让等级越低的节点,越晚发起选举,降低选举的优先级
server.cluster->failover_auth_time +=
server.cluster->failover_auth_rank * 1000;
/* 如果是手动进行故障转移,不需要设置延迟 */
if (server.cluster->mf_end) {
// 设置发起选举时间为当前时间
server.cluster->failover_auth_time = mstime();
// rank设置为0,等级最高
server.cluster->failover_auth_rank = 0;
// 设置CLUSTER_TODO_HANDLE_FAILOVER状态
clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER);
}
serverLog(LL_WARNING,
"Start of election delayed for %lld milliseconds "
"(rank #%d, offset %lld).",
server.cluster->failover_auth_time - mstime(),
server.cluster->failover_auth_rank,
replicationGetSlaveOffset());
/* 广播消息 */
clusterBroadcastPong(CLUSTER_BROADCAST_LOCAL_SLAVES);
return;
}

if (server.cluster->failover_auth_sent == 0 && // 如果还未发起选举
server.cluster->mf_end == 0) // 如果不是手动执行故障转移
{
// 获取节点等级,节点等级有可能在变化,需要更新等级
int newrank = clusterGetSlaveRank();
// 如果排名大于之前设置的等级
if (newrank > server.cluster->failover_auth_rank) {
long long added_delay =
(newrank - server.cluster->failover_auth_rank) * 1000;
// 更新发起选举时间
server.cluster->failover_auth_time += added_delay;
// 更新节点等级
server.cluster->failover_auth_rank = newrank;
serverLog(LL_WARNING,
"Replica rank updated to #%d, added %lld milliseconds of delay.",
newrank, added_delay);
}
}

/* 如果当前时间小于设置的选举发起时间,需要延迟发起选举 */
if (mstime() < server.cluster->failover_auth_time) {
// 记录延迟发起选举日志
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_DELAY);
return;
}

/* 如果距离发起选举的时间大于超时时间,表示已超时 */
if (auth_age > auth_timeout) {
// 记录选举已过期日志
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_EXPIRED);
return;
}

/* 如果failover_auth_sent为0表示还未发起投票 */
if (server.cluster->failover_auth_sent == 0) {
// 纪元加1
server.cluster->currentEpoch++;
// 设置当前选举纪元failover_auth_epoch
server.cluster->failover_auth_epoch = server.cluster->currentEpoch;
serverLog(LL_WARNING,"Starting a failover election for epoch %llu.",
(unsigned long long) server.cluster->currentEpoch);
// 广播发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,发起投票
clusterRequestFailoverAuth();
// failover_auth_sent置为1 ,表示已经发起了投票
server.cluster->failover_auth_sent = 1;
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|
CLUSTER_TODO_UPDATE_STATE|
CLUSTER_TODO_FSYNC_CONFIG);
return; /* Wait for replies. */
}

/* 校验是否获取了大多数的投票,执行故障切换 */
if (server.cluster->failover_auth_count >= needed_quorum) {

// ...

} else {
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_VOTES);
}
}

/* 发送FAILOVER_AUTH_REQUEST消息到每个节点 */
void clusterRequestFailoverAuth(void) {
clusterMsg buf[1];
clusterMsg *hdr = (clusterMsg*) buf;
uint32_t totlen;
// 设置消息头,发送CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息
clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST);
/* 如果是手动转移,设置CLUSTERMSG_FLAG0_FORCEACK标记 */
if (server.cluster->mf_end) hdr->mflags[0] |= CLUSTERMSG_FLAG0_FORCEACK;
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
hdr->totlen = htonl(totlen);
// 发送广播
clusterBroadcastMessage(buf,totlen);

获取节点等级

clusterGetSlaveRank用于计算当前节点的等级,遍历所属主节点的所有从节点,根据主从复制进度repl_offset计算,repl_offset值越大表示复制主节点的数据越多,所以等级越高,对应的rank值就越低。

从节点在发起选举使用了rank的值作为延迟时间,值越低延迟时间越小,意味着选举优先级也就越高。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int clusterGetSlaveRank(void) {
long long myoffset;
// rank初始化为0
int j, rank = 0;
clusterNode *master;
serverAssert(nodeIsSlave(myself));
// 获取当前节点所属的主节点
master = myself->slaveof;
if (master == NULL) return 0; /* 返回0 */
// 获取主从复制进度
myoffset = replicationGetSlaveOffset();
// 变量master的所有从节点
for (j = 0; j < master->numslaves; j++)
// 如果不是当前节点、节点可以用来执行故障切换并且节点的复制进度大于当前节点的进度
if (master->slaves[j] != myself &&
!nodeCantFailover(master->slaves[j]) &&
master->slaves[j]->repl_offset > myoffset) rank++; // 将当前节点的排名后移,等级越低
return rank;
}

主节点进行投票

当从节点认为主节点故障需要发起投票,重新选举主节点时,在集群中广播了CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息,对应的处理在clusterProcessPacket函数中,里面会调用clusterSendFailoverAuthIfNeeded函数进行投票:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int clusterProcessPacket(clusterLink *link) {

// ...

/* PING, PONG, MEET消息处理 */
if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
type == CLUSTERMSG_TYPE_MEET)
{
// ...
}
// ...
else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST) {// 处理CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息
if (!sender) return 1;
// 进行投票,sender为发送消息的节点,hdr为请求体
clusterSendFailoverAuthIfNeeded(sender,hdr);
}
// ...
}

clusterSendFailoverAuthIfNeeded

clusterSendFailoverAuthIfNeeded函数用于进行投票,处理逻辑如下:

  1. 由于只有主节点才可以投票,如果当前节点不是主节点或者当前节点中负责slot的个数为0,当前节点没有权限投票,直接返回。
  2. 需要保证发起请求的投票轮次要等于或者大于当前节点中记录的轮次,所以如果请求的纪元(轮次)小于当前节点中记录的纪元(轮次) ,直接返回。
  3. 如果当前节点中记录的上次投票的纪元(轮次)等于当前投票纪元(轮次),表示当前节点已经投过票,直接返回。
  4. 如果发起请求的节点是主节点或者发起请求的节点所属的主节点为空,或者主节点不处于下线状态并且不是手动执行故障转移,直接返回。
  5. 如果当前时间减去节点投票时间node->slaveof->voted_time小于超时时间的2倍,直接返回。node->slaveof->voted_time记录了当前节点的投票时间,在未超过2倍超时时间之前不进行投票。
  6. 处理slot,需要保证当前节点中记录的slot的纪元小于等于请求纪元,如果不满足此条件,终止投票,直接返回。

以上条件校验通过,表示当前节点可以投票给发送请求的节点,此时更新lastVoteEpoch,记录最近一次投票的纪元(轮次),更新投票时间node->slaveof->voted_time,然后向发起请求的节点回复CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
void clusterSendFailoverAuthIfNeeded(clusterNode *node, clusterMsg *request) {
// 发起请求的节点所属主节点
clusterNode *master = node->slaveof;
// 从请求中获取投票纪元(轮次)
uint64_t requestCurrentEpoch = ntohu64(request->currentEpoch);
//
uint64_t requestConfigEpoch = ntohu64(request->configEpoch);
// 从请求中获取节点负责的slot
unsigned char *claimed_slots = request->myslots;
// 是否是手动故障执行故障转移
int force_ack = request->mflags[0] & CLUSTERMSG_FLAG0_FORCEACK;
int j;

/* 如果当前节点不是主节点或者当前节点中负责slot的个数为0,当前节点没有权限投票,直接返回*/
if (nodeIsSlave(myself) || myself->numslots == 0) return;

/* 如果请求的纪元(轮次)小于当前节点中记录的纪元(轮次) */
if (requestCurrentEpoch < server.cluster->currentEpoch) {
serverLog(LL_WARNING,
"Failover auth denied to %.40s: reqEpoch (%llu) < curEpoch(%llu)",
node->name,
(unsigned long long) requestCurrentEpoch,
(unsigned long long) server.cluster->currentEpoch);
return;
}

/* 如果当前节点中记录的上次投票的纪元等于当前纪元,表示当前节点已经投过票,直接返回 */
if (server.cluster->lastVoteEpoch == server.cluster->currentEpoch) {
serverLog(LL_WARNING,
"Failover auth denied to %.40s: already voted for epoch %llu",
node->name,
(unsigned long long) server.cluster->currentEpoch);
return;
}

/* 如果发起请求的节点是主节点或者发起请求的节点所属的主节点为空,或者主节点不处于下线状态并且不是手动执行故障转移,直接返回 */
if (nodeIsMaster(node) || master == NULL ||
(!nodeFailed(master) && !force_ack))
{
if (nodeIsMaster(node)) {
serverLog(LL_WARNING,
"Failover auth denied to %.40s: it is a master node",
node->name);
} else if (master == NULL) {
serverLog(LL_WARNING,
"Failover auth denied to %.40s: I don't know its master",
node->name);
} else if (!nodeFailed(master)) {
serverLog(LL_WARNING,
"Failover auth denied to %.40s: its master is up",
node->name);
}
return;
}

/* 如果当前时间减去投票时间小于超时时间的2倍,直接返回 */
/* node->slaveof->voted_time记录了当前节点的投票时间,在未过2倍超时时间之前,不进行投票 */
if (mstime() - node->slaveof->voted_time < server.cluster_node_timeout * 2)
{
serverLog(LL_WARNING,
"Failover auth denied to %.40s: "
"can't vote about this master before %lld milliseconds",
node->name,
(long long) ((server.cluster_node_timeout*2)-
(mstime() - node->slaveof->voted_time)));
return;
}

/* 处理slot,需要保证当前节点中记录的slot的纪元小于等于请求纪元 */
for (j = 0; j < CLUSTER_SLOTS; j++) {
// 如果当前的slot不在发起请求节点负责的slot中,继续下一个
if (bitmapTestBit(claimed_slots, j) == 0) continue;
// 如果当前节点不负责此slot或者slot中记录的纪元小于等于请求纪元,继续下一个
if (server.cluster->slots[j] == NULL ||
server.cluster->slots[j]->configEpoch <= requestConfigEpoch)
{
continue;
}
serverLog(LL_WARNING,
"Failover auth denied to %.40s: "
"slot %d epoch (%llu) > reqEpoch (%llu)",
node->name, j,
(unsigned long long) server.cluster->slots[j]->configEpoch,
(unsigned long long) requestConfigEpoch);
return;
}

/* 走到这里表示可以投票给从节点 */
/* 将当前节点的lastVoteEpoch设置为currentEpoch */
server.cluster->lastVoteEpoch = server.cluster->currentEpoch;
/* 更新投票时间 */
node->slaveof->voted_time = mstime();
clusterDoBeforeSleep(CLUSTER_TODO_SAVE_CONFIG|CLUSTER_TODO_FSYNC_CONFIG);
/* 发送CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息 */
clusterSendFailoverAuth(node);
serverLog(LL_WARNING, "Failover auth granted to %.40s for epoch %llu",
node->name, (unsigned long long) server.cluster->currentEpoch);
}

/* 发送CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息到指定节点. */
void clusterSendFailoverAuth(clusterNode *node) {
clusterMsg buf[1];
clusterMsg *hdr = (clusterMsg*) buf;
uint32_t totlen;

if (!node->link) return;
// 设置请求体,发送CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
clusterBuildMessageHdr(hdr,CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK);
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
hdr->totlen = htonl(totlen);
// 发送消息
clusterSendMessage(node->link,(unsigned char*)buf,totlen);
}

投票回复消息处理

主节点对发起投票请求节点的回复消息CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK同样在消息处理函数clusterProcessPacket中,会对发送回复消息的节点进行验证:

  1. 发送者是主节点
  2. 发送者负责的slot数量大于0
  3. 发送者记录的投票纪元(轮次)大于或等于当前节点发起故障转移投票的轮次

同时满足以上三个条件时,表示发送者对当前节点进行了投票,更新当前节点记录的收到投票的个数,failover_auth_count加1,此时有可能获取了大多数节点的投票,先调用clusterDoBeforeSleep设置一个CLUSTER_TODO_HANDLE_FAILOVER标记,在周期执行的时间事件中会调用对状态进行判断决定是否执行故障转移。

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
int clusterProcessPacket(clusterLink *link) {

// ...

/* PING, PONG, MEET: process config information. */
if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
type == CLUSTERMSG_TYPE_MEET)
{
// 省略...
}
// 省略其他else if
// ...
else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST) { // 处理CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST消息
if (!sender) return 1;
clusterSendFailoverAuthIfNeeded(sender,hdr);
} else if (type == CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK) { // 处理CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK消息
if (!sender) return 1;
/* 如果发送者是主节点并且负责的slot数量大于0,并且CurrentEpoch大于或等于当前节点的failover_auth_epoch*/
if (nodeIsMaster(sender) && sender->numslots > 0 &&
senderCurrentEpoch >= server.cluster->failover_auth_epoch)
{
/* 当前节点的failover_auth_count加1 */
server.cluster->failover_auth_count++;
/* 有可能获取了大多数节点的投票,先设置一个CLUSTER_TODO_HANDLE_FAILOVER标记 */
clusterDoBeforeSleep(CLUSTER_TODO_HANDLE_FAILOVER);
}
}
// 省略其他else if

// ...
}

void clusterDoBeforeSleep(int flags) {
// 设置状态
server.cluster->todo_before_sleep |= flags;
}

等待处理故障转移

从节点收到投票后,会添加CLUSTER_TODO_HANDLE_FAILOVER标记,接下来看下对CLUSTER_TODO_HANDLE_FAILOVER状态的处理。

在beforeSleep函数(server.c文件中),如果开启了集群,会调用clusterBeforeSleep函数,里面就包含了对CLUSTER_TODO_HANDLE_FAILOVER状态的处理:

1
2
3
4
5
6
7
8
9
void beforeSleep(struct aeEventLoop *eventLoop) {

// ...

/* 如果开启了集群,调用clusterBeforeSleep函数 */
if (server.cluster_enabled) clusterBeforeSleep();

// ...
}

beforeSleep函数是在Redis事件循环aeMain方法中被调用的,详细内容可参考事件驱动框架源码分析 文章。

1
2
3
4
5
6
7
8
9
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 调用了aeProcessEvents处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}

clusterBeforeSleep

在clusterBeforeSleep函数中,如果节点带有CLUSTER_TODO_HANDLE_FAILOVER标记,会调用clusterHandleSlaveFailover函数进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void clusterBeforeSleep(void) {
// ...

if (flags & CLUSTER_TODO_HANDLE_MANUALFAILOVER) { // 处理CLUSTER_TODO_HANDLE_FAILOVER
// 手动执行故障转移
if(nodeIsSlave(myself)) {
clusterHandleManualFailover();
if (!(server.cluster_module_flags & CLUSTER_MODULE_FLAG_NO_FAILOVER))
clusterHandleSlaveFailover(); // 故障转移
}
} else if (flags & CLUSTER_TODO_HANDLE_FAILOVER) { // 如果是CLUSTER_TODO_HANDLE_FAILOVER状态
/* 处理故障转移 */
clusterHandleSlaveFailover();
}

// ...
}

故障转移处理

clusterHandleSlaveFailover函数在上面我们已经见到过,这次我们来关注集群的故障转移处理。

如果当前节点获取了大多数的投票,也就是failover_auth_count(得到的投票数量)大于等于needed_quorum,needed_quorum数量为集群中节点个数的一半+1,即可执行故障转移,接下来会调用clusterFailoverReplaceYourMaster函数完成故障转移。

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
void clusterHandleSlaveFailover(void) {
// 主从复制延迟时间
mstime_t data_age;
// 当前时间减去发起选举的时间
mstime_t auth_age = mstime() - server.cluster->failover_auth_time;
// 计算quorum的数量,为集群中节点的数量的一半再加1
int needed_quorum = (server.cluster->size / 2) + 1;

// ...

/* 校验是否获取了大多数的投票,failover_auth_count大于等于needed_quorum,needed_quorum数量为集群中节点个数的一半+1 */
if (server.cluster->failover_auth_count >= needed_quorum) {
/* 如果取得了大多数投票,从节点被选举为主节点*/

serverLog(LL_WARNING,
"Failover election won: I'm the new master.");

/* 更新configEpoch为选举纪元failover_auth_epoch */
if (myself->configEpoch < server.cluster->failover_auth_epoch) {
myself->configEpoch = server.cluster->failover_auth_epoch;
serverLog(LL_WARNING,
"configEpoch set to %llu after successful failover",
(unsigned long long) myself->configEpoch);
}

/* 负责master的slot */
clusterFailoverReplaceYourMaster();
} else {
clusterLogCantFailover(CLUSTER_CANT_FAILOVER_WAITING_VOTES);
}
}

执行故障转移

clusterFailoverReplaceYourMaster

如果从节点收到了集群中过半的投票,就可以成为新的master节点,并接手下线的master节点的slot,具体的处理在clusterFailoverReplaceYourMaster函数中,主要处理逻辑如下:

  1. 将当前节点设为主节点
  2. 将下线的master节点负责的所有slots设置到新的主节点中
  3. 更新相关状态并保存设置
  4. 广播PONG消息到其他节点,通知其他节点当前节点成为了主节点
  5. 如果是手动进行故障转移,清除手动执行故障状态
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
void clusterFailoverReplaceYourMaster(void) {
int j;
// 旧的主节点
clusterNode *oldmaster = myself->slaveof;

if (nodeIsMaster(myself) || oldmaster == NULL) return;

/* 将当前节点设为主节点 */
clusterSetNodeAsMaster(myself);
replicationUnsetMaster();

/* 将下线的master节点负责的所有slots设置到新的主节点中 */
for (j = 0; j < CLUSTER_SLOTS; j++) {
if (clusterNodeGetSlotBit(oldmaster,j)) {
clusterDelSlot(j);
clusterAddSlot(myself,j);
}
}

/* 更新状态并保存设置*/
clusterUpdateState();
clusterSaveConfigOrDie(1);

/* 广播PONG消息到其他节点,通知其他节点当前节点成为了主节点 */
clusterBroadcastPong(CLUSTER_BROADCAST_ALL);

/* 如果是手动进行故障转移,清除状态 */
resetManualFailover();
}

Redis版本:redis-6.2.5

【Redis】Redis Cluster初始化及PING消息的发送

Posted on 2022-05-15

Cluster消息类型定义

1
2
3
4
#define CLUSTERMSG_TYPE_PING 0          /* Ping消息类型,节点间进行通信交换信息的消息 */
#define CLUSTERMSG_TYPE_PONG 1 /* Pong消息类型 (Ping命令的回复) */
#define CLUSTERMSG_TYPE_MEET 2 /* Meet消息类型,表示节点加入集群 */
#define CLUSTERMSG_TYPE_FAIL 3 /* FAIL消息类型,表示节点下线*/

在Redis初始化服务initServer函数中,调用aeCreateTimeEvent注册了时间事件,周期性的执行serverCron函数,在serverCron中可以看到每隔100ms调用一次clusterCron函数,执行Redis Cluster定时任务:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void initServer(void) {

// 省略...
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
// 省略...
}

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 省略...

/* Redis Cluster 定时任务,每隔100ms调用一次 */
run_with_period(100) {
if (server.cluster_enabled) clusterCron();
}

// 省略...
}

clusterCron

clusterCron是集群相关的定时执行函数,每100ms执行一次:

  1. 遍历集群中的所有节点,校验是否有连接中断的节点并进行重新连接
    • 如果节点是自身或者是没有地址的节点,跳过
    • 如果节点处于握手状态并且已经超时,跳过
    • 如果连接为空,调用connConnect进行连接,回调函数为clusterLinkConnectHandler
  2. 每执行10次clusterCron函数时随机选取五个节点,然后从这五个节点选出最早收到PONG回复的那个节点,也就是找出最久没有进行通信的那个节点,向其发送PING消息,clusterCron每100ms执行一次,执行10次是1000ms,也就是说每1秒选取一个节点调用clusterSendPing函数发送一次PING消息
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
void clusterCron(void) {

// ...

/* 校验是否有连接中断的节点并进行重新连接 */
di = dictGetSafeIterator(server.cluster->nodes);
server.cluster->stats_pfail_nodes = 0;
// 遍历集群中的所有节点
while((de = dictNext(di)) != NULL) {
// 获取集群节点
clusterNode *node = dictGetVal(de);

/* 如果节点是自身或者是没有地址的节点,跳过 */
if (node->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_NOADDR)) continue;

if (node->flags & CLUSTER_NODE_PFAIL)
server.cluster->stats_pfail_nodes++;

/* 如果节点处于握手状态并且已经超时 */
if (nodeInHandshake(node) && now - node->ctime > handshake_timeout) {
clusterDelNode(node);
continue;
}

if (node->link == NULL) {
// 创建clusterLink
clusterLink *link = createClusterLink(node);
link->conn = server.tls_cluster ? connCreateTLS() : connCreateSocket();
connSetPrivateData(link->conn, link);
// 建立连接,监听函数为clusterLinkConnectHandler
if (connConnect(link->conn, node->ip, node->cport, NET_FIRST_BIND_ADDR,
clusterLinkConnectHandler) == -1) {
if (node->ping_sent == 0) node->ping_sent = mstime();
serverLog(LL_DEBUG, "Unable to connect to "
"Cluster Node [%s]:%d -> %s", node->ip,
node->cport, server.neterr);

freeClusterLink(link);
continue;
}
// 设置link
node->link = link;
}
}
dictReleaseIterator(di);

/* 每执行10次clusterCron函数,发送一次PING消息 */
if (!(iteration % 10)) {
int j;

/* 随机选取节点并找到最早收到pong消息的节点 */
for (j = 0; j < 5; j++) {
// 随机选取节点
de = dictGetRandomKey(server.cluster->nodes);
// 获取节点
clusterNode *this = dictGetVal(de);

/* 如果节点的连接已中断或者本次PING命令处于活跃状态 */
if (this->link == NULL || this->ping_sent != 0) continue;
if (this->flags & (CLUSTER_NODE_MYSELF|CLUSTER_NODE_HANDSHAKE))
continue;
// 查找最早收到PONG消息的那个节点
if (min_pong_node == NULL || min_pong > this->pong_received) {
min_pong_node = this;
min_pong = this->pong_received;
}
}
// 如果是最早收到PONG消息的节点
if (min_pong_node) {
serverLog(LL_DEBUG,"Pinging node %.40s", min_pong_node->name);
// 发送PING消息
clusterSendPing(min_pong_node->link, CLUSTERMSG_TYPE_PING);
}
}

// ...
}

clusterNode

clusterNode是集群中节点对应的结构体,包含了以下内容:

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
typedef struct clusterNode {
mstime_t ctime; /* 节点对象创建时间 */
char name[CLUSTER_NAMELEN]; /* 节点名称 */
int flags; /* 节点标识 */
uint64_t configEpoch; /* configEpoch */
unsigned char slots[CLUSTER_SLOTS/8]; /* 节点负责的slots */
sds slots_info; /* Slots信息 */
int numslots; /* 节点负责的slots数量 */
int numslaves; /* 从节点的数量 */
struct clusterNode **slaves; /* 指向从节点的指针 */
struct clusterNode *slaveof; /* 指向主节点的指针 */
mstime_t ping_sent; /* 最近一次发送PING消息的时间 */
mstime_t pong_received; /* 收到pong消息的时间 */
mstime_t data_received; /* Unix time we received any data */
mstime_t fail_time; /* 标记FAIL状态的时间 */
mstime_t voted_time; /* 最近一次投票的时间 */
mstime_t repl_offset_time; /* 收到主从复制offset的时间*/
mstime_t orphaned_time; /* Starting time of orphaned master condition */
long long repl_offset; /* 节点主从复制offset */
char ip[NET_IP_STR_LEN]; /* IP */
int port; /* 客户端通信端口 */
int pport; /* 使用TLS协议的端口 */
int cport; /* 集群通信端口 */
clusterLink *link; /* TCP/IP连接相关信息 */
list *fail_reports; /* List of nodes signaling this as failing */
} clusterNode;

clusterLinkConnectHandler

clusterLinkConnectHandler是建立连接的监听函数,当连接建立时会调用clusterLinkConnectHandler进行处理,在clusterLinkConnectHandler函数中可以看到,又调用了connSetReadHandler注册了可读事件的监听,对应的回调函数为clusterReadHandler,当收到其他节点发送的通信消息时会调用clusterReadHandler函数处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void clusterLinkConnectHandler(connection *conn) {
clusterLink *link = connGetPrivateData(conn);
clusterNode *node = link->node;

/* 校验连接是否成功 */
if (connGetState(conn) != CONN_STATE_CONNECTED) {
serverLog(LL_VERBOSE, "Connection with Node %.40s at %s:%d failed: %s",
node->name, node->ip, node->cport,
connGetLastError(conn));
freeClusterLink(link);
return;
}

/* 注册readHandler,监听函数为clusterReadHandler */
connSetReadHandler(conn, clusterReadHandler);

// 省略..
}

集群间通信

通信消息结构体定义

clusterMsg

集群间通信的消息对应的结构体为clusterMsg,里面包含了消息类型、发送消息节点的slots信息以及节点间通信的消息体clusterMsgData等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
typedef struct {
char sig[4]; /* "RCmb"签名 */
uint32_t totlen; /* 消息总长度 */
uint16_t ver; /* 协议版本, 当前设置为1 */
uint16_t port; /* 端口 */
uint16_t type; /* 消息类型 */

// 省略...

char sender[CLUSTER_NAMELEN]; /* 发送消息节点的名称 */
unsigned char myslots[CLUSTER_SLOTS/8]; /* 发送消息节点的slots信息 */
char slaveof[CLUSTER_NAMELEN];
char myip[NET_IP_STR_LEN]; /* 发送消息节点的ip */
char notused1[32]; /* 32字节的保留数据 */
uint16_t pport; /* 使用TLS协议时的端口 */
uint16_t cport; /* 发送消息节点的集群总线端口,也就是用于集群间通信的端口 */
uint16_t flags; /* 发送消息节点的flags标识 */
unsigned char state; /* 发送消息节点的集群状态 */
unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
union clusterMsgData data; // 集群通信的实际消息
} clusterMsg;

clusterMsgData

clusterMsgData里面存储了节点间进行通信的实际消息,不同消息类型对应不同的数据结构:

  1. clusterMsgDataGossip:PING, MEET 和 PONG消息对应的数据结构
  2. clusterMsgDataFail:FAIL消息对应的数据结构
  3. clusterMsgDataPublish:PUBLISH消息对应的数据结构
  4. clusterMsgDataUpdate:UPDATE消息对应的数据结构
  5. clusterMsgModule:MODULE消息对应的数据结构
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
union clusterMsgData {
/* PING, MEET and PONG消息对应的数据结构 */
struct {
clusterMsgDataGossip gossip[1];
} ping;

/* FAIL消息对应的数据结构 */
struct {
clusterMsgDataFail about;
} fail;

/* PUBLISH消息对应的数据结构 */
struct {
clusterMsgDataPublish msg;
} publish;

/* UPDATE消息对应的数据结构 */
struct {
clusterMsgDataUpdate nodecfg;
} update;

/* MODULE消息对应的数据结构 */
struct {
clusterMsgModule msg;
} module;
};

clusterMsgDataGossip

clusterMsgDataGossip是集群间发送PING、MEET 和 PONG消息对应的数据结构,里面包含以下信息:

1
2
3
4
5
6
7
8
9
10
11
typedef struct {
char nodename[CLUSTER_NAMELEN]; /* 节点名称 */
uint32_t ping_sent; /* 发送PING命令的时间 */
uint32_t pong_received; /* 收到PONG命令的时间 */
char ip[NET_IP_STR_LEN]; /* 节点的IP */
uint16_t port; /* 用于客户端通信的端口 */
uint16_t cport; /* 集群间通信的端口 */
uint16_t flags; /* 节点的flags标识 */
uint16_t pport; /* 使用TLS协议时的端口 */
uint16_t notused1;
} clusterMsgDataGossip;

PING消息的发送

clusterSendPing

clusterSendPing函数用于向指定节点发送PING消息,Ping消息中不仅包含当前节点的信息,也会随机选取一些其他的节点,将其他节点的信息封装在消息体中进行发送,随机选取节点的个数计算规则如下:

  • wanted:随机选取的节点个数,默认是集群中节点的数量除以10
  • freshnodes:随机选取的节点个数的最大值,默认集群中节点的数量减2

如果wanted小于3,那么将wanted置为3,也就是最少选取3个节点;

如果wanted大于freshnodes,将wanted置为freshnodes的值,也就是最大可以选取freshnodes个节点;

选取的节点个数wanted确定之后,处理逻辑如下:

  1. 调用clusterBuildMessageHdr函数构建消息头

  2. 根据wanted的数量随机选取节点,处于以下几种情况的节点将被跳过

    • FAIL下线状态的节点
    • 处于握手状态的节点

    • 没有地址信息的节点

    • 失去连接的节点并且没有配置slots.
  3. 调用clusterSetGossipEntry函数将选取的节点信息加入到消息体中

  4. 调用clusterSendMessage函数发送消息

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
void clusterSendPing(clusterLink *link, int type) {
unsigned char *buf; /* 发送的消息数据*/
clusterMsg *hdr; /* 节点间通信消息 */
int gossipcount = 0;
int wanted; /* 选取的节点个数 */
int totlen; /* 总长度 */

// 集群中节点的数量 - 2
int freshnodes = dictSize(server.cluster->nodes)-2;
// 集群中节点的数量除以10
wanted = floor(dictSize(server.cluster->nodes)/10);
// 如果wanted小于3,则设置为3
if (wanted < 3) wanted = 3;
// 如果大于最大节点数,设置为freshnodes
if (wanted > freshnodes) wanted = freshnodes;

//...

if (totlen < (int)sizeof(clusterMsg)) totlen = sizeof(clusterMsg);
buf = zcalloc(totlen); // 分配空间
hdr = (clusterMsg*) buf;
if (link->node && type == CLUSTERMSG_TYPE_PING)
link->node->ping_sent = mstime();// 更新发送PING消息时间
// 构建消息头
clusterBuildMessageHdr(hdr,type);

/* 计算 gossip */
int maxiterations = wanted*3;
while(freshnodes > 0 && gossipcount < wanted && maxiterations--) {
// 随机选取节点
dictEntry *de = dictGetRandomKey(server.cluster->nodes);
clusterNode *this = dictGetVal(de);

/* 如果是自身 */
if (this == myself) continue;

/* 如果是FAIL状态,跳过 */
if (this->flags & CLUSTER_NODE_PFAIL) continue;

/* 以下节点跳过:
* 1) 处于握手状态的节点.
* 3) 没有地址信息的节点.
* 4) 失去连接的节点并且没有配置slots.
*/
if (this->flags & (CLUSTER_NODE_HANDSHAKE|CLUSTER_NODE_NOADDR) ||
(this->link == NULL && this->numslots == 0))
{
freshnodes--; /* Technically not correct, but saves CPU. */
continue;
}

/* 如果节点已经添加 */
if (clusterNodeIsInGossipSection(hdr,gossipcount,this)) continue;

/* 添加到消息体中 */
clusterSetGossipEntry(hdr,gossipcount,this);
freshnodes--;
gossipcount++;
}
// ...

totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
totlen += (sizeof(clusterMsgDataGossip)*gossipcount);
hdr->count = htons(gossipcount);
hdr->totlen = htonl(totlen);
// 发送消息
clusterSendMessage(link,buf,totlen);
zfree(buf);

}

构建消息头

clusterBuildMessageHdr

clusterBuildMessageHdr函数用于构建消息头,设置了消息发送者的节点相关信息:

  1. 设置了签名、消息类型、节点IP、端口等信息
  2. 设置发送消息节点的slots信息,如果发送消息的节点是从节点,需要使用它对应的主节点的slots信息
  3. 计算集群消息的总长度totlen,并设置到消息头中
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void clusterBuildMessageHdr(clusterMsg *hdr, int type) {
int totlen = 0;
uint64_t offset;
clusterNode *master;

/* 如果是从节点, 使用它对应的主节点的信息 */
master = (nodeIsSlave(myself) && myself->slaveof) ?
myself->slaveof : myself;

memset(hdr,0,sizeof(*hdr));
hdr->ver = htons(CLUSTER_PROTO_VER);
// 设置签名
hdr->sig[0] = 'R';
hdr->sig[1] = 'C';
hdr->sig[2] = 'm';
hdr->sig[3] = 'b';
// 设置消息类型
hdr->type = htons(type);
memcpy(hdr->sender,myself->name,CLUSTER_NAMELEN);
memset(hdr->myip,0,NET_IP_STR_LEN);
if (server.cluster_announce_ip) {
// 设置ip
strncpy(hdr->myip,server.cluster_announce_ip,NET_IP_STR_LEN);
hdr->myip[NET_IP_STR_LEN-1] = '\0';
}

/* 处理端口 */
int announced_port, announced_pport, announced_cport;
deriveAnnouncedPorts(&announced_port, &announced_pport, &announced_cport);
// 设置当前节点的slots信息
memcpy(hdr->myslots,master->slots,sizeof(hdr->myslots));
memset(hdr->slaveof,0,CLUSTER_NAMELEN);
if (myself->slaveof != NULL)
memcpy(hdr->slaveof,myself->slaveof->name, CLUSTER_NAMELEN);
// 设置端口
hdr->port = htons(announced_port);
hdr->pport = htons(announced_pport);
hdr->cport = htons(announced_cport);
// 设置标识
hdr->flags = htons(myself->flags);
// 设置集群状态
hdr->state = server.cluster->state;

/* 设置currentEpoch和configEpoch */
hdr->currentEpoch = htonu64(server.cluster->currentEpoch);
hdr->configEpoch = htonu64(master->configEpoch);

/* 设置主从复制的offset. */
if (nodeIsSlave(myself))
offset = replicationGetSlaveOffset();
else
offset = server.master_repl_offset;
hdr->offset = htonu64(offset);

if (nodeIsMaster(myself) && server.cluster->mf_end)
hdr->mflags[0] |= CLUSTERMSG_FLAG0_PAUSED;

/* 计算消息总长度 */
if (type == CLUSTERMSG_TYPE_FAIL) {
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
totlen += sizeof(clusterMsgDataFail);
} else if (type == CLUSTERMSG_TYPE_UPDATE) {
totlen = sizeof(clusterMsg)-sizeof(union clusterMsgData);
totlen += sizeof(clusterMsgDataUpdate);
}
// 设置消息总长度
hdr->totlen = htonl(totlen);
}

构建消息体

clusterSetGossipEntry

clusterSetGossipEntry函数用于构建消息体,将随机选取的其他节点信息加入到ping消息对应的数组hdr->data.ping.gossip[i]中,并设置节点的相关信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void clusterSetGossipEntry(clusterMsg *hdr, int i, clusterNode *n) {
clusterMsgDataGossip *gossip;
gossip = &(hdr->data.ping.gossip[i]);
memcpy(gossip->nodename,n->name,CLUSTER_NAMELEN);
// 设置PING消息发送时间
gossip->ping_sent = htonl(n->ping_sent/1000);
// 设置收到PONG消息时间
gossip->pong_received = htonl(n->pong_received/1000);
// 设置IP
memcpy(gossip->ip,n->ip,sizeof(n->ip));
// 设置端口
gossip->port = htons(n->port);
// 设置集群端口
gossip->cport = htons(n->cport);
// 设置标识
gossip->flags = htons(n->flags);
gossip->pport = htons(n->pport);
gossip->notused1 = 0;
}

PING消息的处理

clusterReadHandler

由上面的clusterLinkConnectHandler函数可知,收到其他节点发送的通信消息时会调用clusterReadHandler函数处理,在clusterReadHandler函数中会开启while循环,不断读取数据,直到获取完整的数据(收到的数据长度rcvbuflen等于消息中设置数据总长度时),调用clusterProcessPacket函数处理收到的消息:

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
void clusterReadHandler(connection *conn) {
clusterMsg buf[1];
ssize_t nread;
clusterMsg *hdr;
clusterLink *link = connGetPrivateData(conn);
unsigned int readlen, rcvbuflen;

while(1) {
rcvbuflen = link->rcvbuf_len;

// 省略...

// 读取数据
nread = connRead(conn,buf,readlen);
// 如果数据读取完毕
if (nread == -1 && (connGetState(conn) == CONN_STATE_CONNECTED)) return;

// 省略...

/* 如果已经获取完整数据(rcvbuflen等于消息中设置数据总长度),处理数据包 */
if (rcvbuflen >= 8 && rcvbuflen == ntohl(hdr->totlen)) {
// 处理消息
if (clusterProcessPacket(link)) {
if (link->rcvbuf_alloc > RCVBUF_INIT_LEN) {
zfree(link->rcvbuf);
link->rcvbuf = zmalloc(link->rcvbuf_alloc = RCVBUF_INIT_LEN);
}
link->rcvbuf_len = 0;
} else {
return;
}
}
}
}

clusterProcessPacket

clusterProcessPacket函数用于处理收到的通信消息,可以看到有许多if else分支,根据消息类型的不同,进行了不同的处理,这里先只关注PING消息的处理:

  1. 如果消息类型是PING或者MEET,调用clusterSendPing函数发送PONG消息,传入的消息类型为CLUSTERMSG_TYPE_PONG,说明PING和PONG消息都是通过clusterSendPing函数实现的,PING和PONG消息的数据结构一致,那么回复的PONG消息中也会带上回复者的节点信息以及回复者随机选取的其他节点信息,以此达到节点间交换信息的目的
  2. 如果是PING, PONG或者MEET消息,并且sender不为空,不为空表示发送消息的节点是当前节点已知的,调用clusterProcessGossipSection函数处理消息体中的Gossip数据
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
38
39
40
41
42
43
44
45
46
47
int clusterProcessPacket(clusterLink *link) {
// 获取发送的消息
clusterMsg *hdr = (clusterMsg*) link->rcvbuf;
// 消息长度
uint32_t totlen = ntohl(hdr->totlen);
// 消息类型
uint16_t type = ntohs(hdr->type);
mstime_t now = mstime();
uint16_t flags = ntohs(hdr->flags);
uint64_t senderCurrentEpoch = 0, senderConfigEpoch = 0;
clusterNode *sender;

// 省略...

/* 校验发送者是否是已知的节点 */
sender = clusterLookupNode(hdr->sender);

/* 更是发送者收到数据的时间*/
if (sender) sender->data_received = now;

// 省略...

/* 如果是PING消息或者MEET消息 */
if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_MEET) {
serverLog(LL_DEBUG,"Ping packet received: %p", (void*)link->node);

if (!sender && type == CLUSTERMSG_TYPE_MEET)
clusterProcessGossipSection(hdr,link);

/* 发送PONG消息,这里传入的类型是CLUSTERMSG_TYPE_PONG */
clusterSendPing(link,CLUSTERMSG_TYPE_PONG);
}

/* PING, PONG, MEET 消息 */
if (type == CLUSTERMSG_TYPE_PING || type == CLUSTERMSG_TYPE_PONG ||
type == CLUSTERMSG_TYPE_MEET)
{

// 省略...

/* 处理消息体中的Gossip节点数据 */
if (sender) clusterProcessGossipSection(hdr,link);
} else {
serverLog(LL_WARNING,"Received unknown packet type: %d", type);
}
return 1;
}

clusterProcessGossipSection

clusterProcessGossipSection函数用于处理clusterMsg中的Gossip节点信息g,它从集群消息中获取Gossip节点数据,根据节点数量进行遍历:

  1. 调用clusterLookupNode函数根据nodename从当前收到消息的节点的集群中查找Gossip节点,查找结果记为node

    • 如果node如果不为空,说明可以从当前节点的集群中找到,Gossip节点针对当前节点是已知的,需要注意node指向的是当前收到消息节点中维护的相同nodename的节点,g指向当前正在遍历的gossip节点(sender发送的消息中携带gossip数组),注意两者的区别
    • 如果node如果为空,说明Gossip节点针对当前节点是未知的,之前不在当前节点维护的集群节点中
  2. 如果node不为空,也就是当前收到消息这个节点的集群中已经存在node节点,进行如下处理:

    (1)发送消息的节点sender是主节点时有以下两种情况:

    • 如果node是FAIL或者PFAIL状态,需要将sender加入到node节点的下线链表fail_reports中,表示sender认为node节点下线(clusterNodeAddFailureReport函数)
    • 断是否有必要将node标记为下线状态(markNodeAsFailingIfNeeded函数)
    • 如果node不是FAIL或者PFAIL状态,需要校验node是否已经在sender的下线节点链表fail_reports中,如果在需要从中移除

    (2)如果node节点不是FAIL、PFAIL、NOADDR状态,并且node的ip或者端口与g指向的gossip节点中的ip或者端口不一致,需要更新node中的ip和端口

  3. 如果node为空,说明之前不在当前节点维护的集群节点中,如果gossip节点不处于NOADDR状态并且不在nodes_black_list中,新建节点,加入到当前收到消息的节点维护的集群数据server.cluster中

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
void clusterProcessGossipSection(clusterMsg *hdr, clusterLink *link) {
uint16_t count = ntohs(hdr->count);
// 获取clusterMsgDataGossip数据
clusterMsgDataGossip *g = (clusterMsgDataGossip*) hdr->data.ping.gossip;
// 发送消息的节点
clusterNode *sender = link->node ? link->node : clusterLookupNode(hdr->sender);

while(count--) {
// 获取节点标识
uint16_t flags = ntohs(g->flags);
clusterNode *node;
sds ci;

if (server.verbosity == LL_DEBUG) {
ci = representClusterNodeFlags(sdsempty(), flags);
serverLog(LL_DEBUG,"GOSSIP %.40s %s:%d@%d %s",
g->nodename,
g->ip,
ntohs(g->port),
ntohs(g->cport),
ci);
sdsfree(ci);
}

/* 根据nodename查找节点,node指向当前收到消息节点中维护的节点*/
node = clusterLookupNode(g->nodename);
// 如果节点已知
if (node) {
/* 如果发送者是主节点 */
if (sender && nodeIsMaster(sender) && node != myself) {
// 如果gossip节点是FAIL或者PFAIL状态
if (flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) {
// 将sender加入到node节点的下线链表fail_reports中,表示sender认为node节点下线
if (clusterNodeAddFailureReport(node,sender)) {
serverLog(LL_VERBOSE,
"Node %.40s reported node %.40s as not reachable.",
sender->name, node->name);
}
// 判断是否有必要将节点置为客观下线
markNodeAsFailingIfNeeded(node);
} else {
// 校验节点是否在下线节点链表fail_reports中,如果在需要移除恢复在线状态
if (clusterNodeDelFailureReport(node,sender)) {
serverLog(LL_VERBOSE,
"Node %.40s reported node %.40s is back online.",
sender->name, node->name);
}
}
}

/* 如果节点不是FAIL或者PFAIL状态,并且node中记录的ping发送时间为0,并且node不在fail_reports中*/
if (!(flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) &&
node->ping_sent == 0 &&
clusterNodeFailureReportsCount(node) == 0)
{
mstime_t pongtime = ntohl(g->pong_received);
pongtime *= 1000; /* 转为毫秒 */
if (pongtime <= (server.mstime+500) &&
pongtime > node->pong_received)
{
node->pong_received = pongtime; // 更新收到pong消息时间
}
}

/* 如果node节点不是FAIL、PFAIL、NOADDR状态,并且node的ip或者端口与g节点中的ip或者端口不一致,需要更新node中的ip和端口 */
/* 需要注意node节点和g节点的区别,node节点是从当前收到消息节点中根据节点id查找到的节点,也就是接收者自己记录的节点信息 */
/* g指向当前在遍历的那个gossip节点,也就是发送者带过来的节点信息 */
if (node->flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL) &&
!(flags & CLUSTER_NODE_NOADDR) &&
!(flags & (CLUSTER_NODE_FAIL|CLUSTER_NODE_PFAIL)) &&
(strcasecmp(node->ip,g->ip) ||
node->port != ntohs(g->port) ||
node->cport != ntohs(g->cport)))
{
if (node->link) freeClusterLink(node->link);
// 更新node节点中的端口、ip信息
memcpy(node->ip,g->ip,NET_IP_STR_LEN);
node->port = ntohs(g->port);
node->pport = ntohs(g->pport);
node->cport = ntohs(g->cport);
node->flags &= ~CLUSTER_NODE_NOADDR;
}
} else { // 如果节点未知
/* 如果节点不处于NOADDR状态并且不在nodes_black_list中 */
if (sender &&
!(flags & CLUSTER_NODE_NOADDR) &&
!clusterBlacklistExists(g->nodename))
{
clusterNode *node;
// 创建节点
node = createClusterNode(g->nodename, flags);
memcpy(node->ip,g->ip,NET_IP_STR_LEN);
node->port = ntohs(g->port);
node->pport = ntohs(g->pport);
node->cport = ntohs(g->cport);
// 加入到当前节点维护的集群server.cluster中
clusterAddNode(node);
}
}

/* 遍历下一个节点 */
g++;
}
}

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)
zhaiguanjie-Redis源码剖析

Redis版本:redis-6.2.5

【Redis】客观下线

Posted on 2022-05-11

在sentinelHandleRedisInstance函数中,如果是主节点,需要做如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) { 
// 省略...

// 如果是主节点
if (ri->flags & SRI_MASTER) {
// 检查是否主观下线
sentinelCheckObjectivelyDown(ri);
// 是否需要开始故障切换
if (sentinelStartFailoverIfNeeded(ri))
// 获取其他哨兵实例对节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 故障切换状态机
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

节点的状态定义

1
2
3
4
5
6
7
8
#define SRI_MASTER  (1<<0) /* master主节点 */
#define SRI_SLAVE (1<<1) /* slave从节点 */
#define SRI_SENTINEL (1<<2) /* 哨兵节点 */
#define SRI_S_DOWN (1<<3) /* 主观下线 */
#define SRI_O_DOWN (1<<4) /* 客观下线 */
#define SRI_MASTER_DOWN (1<<5) /* 节点下线 */
#define SRI_FAILOVER_IN_PROGRESS (1<<6) /* 正在执行master节点的故障切换
换 */

客观下线

sentinelCheckObjectivelyDown

sentinelCheckObjectivelyDown函数用于判断master节点是否客观下线:

  1. 首先确认主节点是否已经被哨兵标记为主观下线,如果已经主观下线,quorum的值置为1,表示当前已经有1个哨兵认为master主观下线,进行第2步
  2. master->sentinels存储了监控当前master的其他哨兵节点,遍历其他哨兵节点,通过flag标识中是否有SRI_MASTER_DOWN状态判断其他哨兵对MASTER节点下线的判断,如果有则认为MASTER下线,对quorum数量加1
  3. 遍历结束后判断quorum的数量是否大于master->quorum设置的数量,也就是是否有过半的哨兵认为主节点下线,如果是将odown置为1,认为主节点客观下线
  4. 根据odown的值判断主节点是否客观下线
    • 如果客观下线,确认master->flags是否有SRI_O_DOWN状态,如果没有,发布+odown客观下线事件并将master->flags置为SRI_O_DOWN状态
    • 如果没有客观下线,校验master->flags是否有SRI_O_DOWN状态,如果有,需要发布-odown事件,取消master的客观下线标记(master->flags的SRI_O_DOWN状态取消)

可以看到,master节点客观下线需要根据其他哨兵实例对主节点的判断来共同决定,具体是通过其他哨兵实例的flag中是否有SRI_MASTER_DOWN状态来判断的,如果认为master下线的哨兵个数超过了master节点中的quorum设置,master节点将被认定为客观下线,发布+odown客观下线事件,关于SRI_MASTER_DOWN状态是在哪里设置的在后面会讲到。

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
38
39
40
41
42
void sentinelCheckObjectivelyDown(sentinelRedisInstance *master) {
dictIterator *di;
dictEntry *de;
unsigned int quorum = 0, odown = 0;
// 判断主节点是否被标记为主观下线SRI_S_DOWN
if (master->flags & SRI_S_DOWN) {
/* quorum初始化为1 */
quorum = 1;
/* 遍历监听主节点的其他哨兵实例. */
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取哨兵实例
sentinelRedisInstance *ri = dictGetVal(de);
// 判断其他哨兵实例是否把master节点标记为下线
if (ri->flags & SRI_MASTER_DOWN) quorum++;
}
dictReleaseIterator(di);
// 如果大于master->quorum,也就是过半的哨兵认为主节点已经下线,标记客观下线
if (quorum >= master->quorum) odown = 1;
}

// 如果客观下线
if (odown) {
if ((master->flags & SRI_O_DOWN) == 0) {
// 发布+odown客观下线事件
sentinelEvent(LL_WARNING,"+odown",master,"%@ #quorum %d/%d",
quorum, master->quorum);
// 将MASTER节点标记为SRI_O_DOWN
master->flags |= SRI_O_DOWN;
// 标记客观下线的时间
master->o_down_since_time = mstime();
}
} else {
// 非客观下线,判断master是否有客观下线标识
if (master->flags & SRI_O_DOWN) {
// 发布-odown事件
sentinelEvent(LL_WARNING,"-odown",master,"%@");
// 取消master的客观下线标识
master->flags &= ~SRI_O_DOWN;
}
}
}

是否需要执行故障切换

sentinelStartFailoverIfNeeded

sentinelStartFailoverIfNeeded用于判断是否需要执行故障切换,可以开始故障切换的条件有三个:

  1. master节点被认为客观下线(SRI_O_DOWN)
  2. 当前没有在进行故障切换(状态不是SRI_FAILOVER_IN_PROGRESS)
  3. 距离上次执行故障切换的时间,超过了故障切换超时时间设置的2倍,意味着上一次执行故障切换的时间已超时,可以重新进行故障切换

同时满足以上三个条件,达到执行故障切换的标准,调用sentinelStartFailover函数,将故障切换的状态改为待执行。

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
int sentinelStartFailoverIfNeeded(sentinelRedisInstance *master) {
/* 如果MASTER不是客观下线,直接返回 */
if (!(master->flags & SRI_O_DOWN)) return 0;

/* 如果当前已经在执行故障切换直接返回 */
if (master->flags & SRI_FAILOVER_IN_PROGRESS) return 0;

/* 判断距离上次执行故障切换的时间,如果小于failover_timeout配置项的2倍,表示上次故障切换还未达到超时设置,所以本次不能执行*/
if (mstime() - master->failover_start_time <
master->failover_timeout*2)
{
if (master->failover_delay_logged != master->failover_start_time) {
time_t clock = (master->failover_start_time +
master->failover_timeout*2) / 1000;
char ctimebuf[26];

ctime_r(&clock,ctimebuf);
ctimebuf[24] = '\0'; /* Remove newline. */
master->failover_delay_logged = master->failover_start_time;
serverLog(LL_WARNING,
"Next failover delay: I will not start a failover before %s",
ctimebuf);
}
return 0;
}
// 开始故障切换(只更改了故障切换状态)
sentinelStartFailover(master);
return 1;
}

sentinelStartFailover

可以看到sentinelStartFailover函数并没有直接进行故障切换,而是更改了一些状态:

  1. 将failover_state置为了SENTINEL_FAILOVER_STATE_WAIT_START等待开始执行状态
  2. 将master节点的flags设置为SRI_FAILOVER_IN_PROGRESS故障切换执行中状态
  3. 将master的failover_epoch设置为当前哨兵的投票轮次current_epoch + 1 ,在选举leader时会用到
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void sentinelStartFailover(sentinelRedisInstance *master) {
serverAssert(master->flags & SRI_MASTER);
// 将状态更改为等待开始执行故障切换
master->failover_state = SENTINEL_FAILOVER_STATE_WAIT_START;
// 设置为故障切换执行中状态
master->flags |= SRI_FAILOVER_IN_PROGRESS;
// 设置failover_epoch故障切换轮次
master->failover_epoch = ++sentinel.current_epoch;
sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
// 发布事件
sentinelEvent(LL_WARNING,"+try-failover",master,"%@");
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
master->failover_state_change_time = mstime();
}

获取哨兵实例对主节点状态判断

在sentinelHandleRedisInstance函数中,可以看到sentinelStartFailoverIfNeeded条件成立时以及函数的最后都调用了sentinelAskMasterStateToOtherSentinels,接下来就去看看sentinelAskMasterStateToOtherSentinels里面都做了什么:

1
2
3
4
5
6
7
8
9
if (ri->flags & SRI_MASTER) {
sentinelCheckObjectivelyDown(ri);
if (sentinelStartFailoverIfNeeded(ri))
// 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_ASK_FORCED
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对主节点的状态判断,这里传入的参数是SENTINEL_NO_FLAGS
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}

is-master-down-by-addr命令发送

sentinelAskMasterStateToOtherSentinels

sentinelAskMasterStateToOtherSentinels函数用于向其他哨兵实例发送is-master-down-by-addr命令获取其他哨兵实例对主节点状态的判断,它会遍历监听同一主节点的其他哨兵实例进行处理:

  1. 获取每一个哨兵实例
  2. 计算距离每个哨兵实例上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
  3. 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空哨兵节点flag中的SRI_MASTER_DOWN状态和leader
  4. 如果master节点已经是下线状态SRI_S_DOWN,不需要进行处理,回到第一步处理下一个哨兵
  5. 如果哨兵节点用于发送命令的link连接处于未连接状态,不处理,回到第一步处理下一个哨兵
  6. 如果不是强制发送命令(入参的flag是SENTINEL_ASK_FORCED时),并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
  7. 通过redisAsyncCommand函数发送发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数,redisAsyncCommand函数有如下参数:
    • 用于发送请求的连接:ri->link->cc
    • 收到命令返回结果时对应的处理函数:sentinelReceiveIsMasterDownReply
    • master节点的ip:announceSentinelAddr(master->addr)
    • master节点端口:port
    • 当前哨兵的投票轮次:sentinel.current_epoch
    • 实例ID:master->failover_state > SENTINEL_FAILOVER_STATE_NONE时表示要执行故障切换,此时传入当前哨兵的myid,否则传入*
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
38
39
40
41
42
43
44
45
void sentinelAskMasterStateToOtherSentinels(sentinelRedisInstance *master, int flags) {
dictIterator *di;
dictEntry *de;
// 遍历监听主节点的其他哨兵实例
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取每一个哨兵实例
sentinelRedisInstance *ri = dictGetVal(de);
// 计算距离上一次收到IS-MASTER-DOWN-BY-ADDR命令回复时间的间隔
mstime_t elapsed = mstime() - ri->last_master_down_reply_time;
char port[32];
int retval;

/* 如果距离上次收到回复的时间已经超过了SENTINEL_ASK_PERIOD周期的5倍,清空相关设置 */
if (elapsed > SENTINEL_ASK_PERIOD*5) {
// 取消SRI_MASTER_DOWN状态
ri->flags &= ~SRI_MASTER_DOWN;
sdsfree(ri->leader);
// leader置为null
ri->leader = NULL;
}

// 如果master已经是下线状态
if ((master->flags & SRI_S_DOWN) == 0) continue;
// 如果已经连接中断
if (ri->link->disconnected) continue;
// 如果不是强制发送命令状态SENTINEL_ASK_FORCED,并且距离上次收到回复命令的时间还在SENTINEL_ASK_PERIOD周期内,不处理,回到第一步处理下一个哨兵
if (!(flags & SENTINEL_ASK_FORCED) &&
mstime() - ri->last_master_down_reply_time < SENTINEL_ASK_PERIOD)
continue;

ll2string(port,sizeof(port),master->addr->port);
// 发送is-master-down-by-addr命令,sentinelReceiveIsMasterDownReply为处理函数
retval = redisAsyncCommand(ri->link->cc,
sentinelReceiveIsMasterDownReply, ri,
"%s is-master-down-by-addr %s %s %llu %s",
sentinelInstanceMapCommand(ri,"SENTINEL"),
announceSentinelAddr(master->addr), port,
sentinel.current_epoch,
(master->failover_state > SENTINEL_FAILOVER_STATE_NONE) ?
sentinel.myid : "*");
if (retval == C_OK) ri->link->pending_commands++;
}
dictReleaseIterator(di);
}

is-master-down-by-addr命令处理

sentinelCommand

其他哨兵实例收到is-master-down-by-addr命令之后的处理逻辑在sentinelCommand函数中可以找到:

  1. 根据请求传入的ip和端口信息获取主节点的sentinelRedisInstance实例对象(在发送is-master-down-by-addr命令的redisAsyncCommand函数中传入了主节点的ip和端口)
  2. 如果不是TILT模式,校验sentinelRedisInstance对象是否是主节点并且主节点被标记为主观下线,如果条件都成立表示主节点已经主观下线,将isdown置为1
  3. 判断请求参数中的runid是否不为*,如果不为*表示当前需要进行leader选举,调用sentinelVoteLeader选举哨兵Leader
  4. 发送is-master-down-by-addr命令的回复,将对主节点主观下线的判断、选出的leader节点的runid、投票轮次leader_epoch返回给发送命令哨兵
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
void sentinelCommand(client *c) {
if (c->argc == 2 && !strcasecmp(c->argv[1]->ptr,"help")) {

}
// 省略其他else if...
// 如果是is-master-down-by-addr命令
else if (!strcasecmp(c->argv[1]->ptr,"is-master-down-by-addr")) {
/* SENTINEL IS-MASTER-DOWN-BY-ADDR <ip> <port> <current-epoch> <runid>
*
* 参数说明:
* ip和端口:哨兵检测的主节点的ip和端口
* current-epoch:是故障切换中当前投票的轮次,每一个哨兵在一轮投票中只能投一次
* runid:如果需要执行故障切换,传入的是哨兵的myid,否则传入的是 *
*/
sentinelRedisInstance *ri;
long long req_epoch;
uint64_t leader_epoch = 0; // 默认为0
char *leader = NULL;
long port;
int isdown = 0;

if (c->argc != 6) goto numargserr;
if (getLongFromObjectOrReply(c,c->argv[3],&port,NULL) != C_OK ||
getLongLongFromObjectOrReply(c,c->argv[4],&req_epoch,NULL)
!= C_OK)
return;
// 根据请求传入的ip和端口信息获取对应的哨兵实例,也就是监控的master节点
ri = getSentinelRedisInstanceByAddrAndRunID(sentinel.masters,
c->argv[2]->ptr,port,NULL);

/* 如果不是TILT模式,校验是否是主节点并且主节点被标记为主观下线 */
if (!sentinel.tilt && ri && (ri->flags & SRI_S_DOWN) &&
(ri->flags & SRI_MASTER))
isdown = 1;// 确定主观下线

/* 如果是主节点并且传入的runid不为*,调用sentinelVoteLeader选举Leader执行故障切换 */
if (ri && ri->flags & SRI_MASTER && strcasecmp(c->argv[5]->ptr,"*")) {
// 选举leader
leader = sentinelVoteLeader(ri,(uint64_t)req_epoch,
c->argv[5]->ptr,
&leader_epoch);
}

/* 发送回复包含三部分:
* 下线状态, 选出的leader, leader的投票轮次leader_epoch */
addReplyArrayLen(c,3);
// 下线状态
addReply(c, isdown ? shared.cone : shared.czero);
// leader不为空传入leader否则传入*
addReplyBulkCString(c, leader ? leader : "*");
// 投票轮次
addReplyLongLong(c, (long long)leader_epoch);
if (leader) sdsfree(leader);
}
// 省略其他else if...
else {
addReplySubcommandSyntaxError(c);
}
return;
}

is-master-down-by-addr回复处理

sentinelReceiveIsMasterDownReply

在sentinelCommand中对命令处理之后发送了返回数据,数据里面包含主观下线的判断、leader的runid以及投票轮次leader_epoch,对返回数据的处理在sentinelReceiveIsMasterDownReply函数中:

  1. 如果回复者也标记了节点主观下线,将哨兵实例的flags状态置为SRI_MASTER_DOWN下线状态,SRI_MASTER_DOWN状态就是在这里设置的
  2. 如果返回的leader runid不是*,意味着哨兵实例对leader进行了投票,需要更新哨兵实例中的leader和leader_epoch
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
38
// 这里的privdata指向回复is-master-down-by-addr命令的那个哨兵实例
void sentinelReceiveIsMasterDownReply(redisAsyncContext *c, void *reply, void *privdata) {
// 每个哨兵实例监控的master节点中,保存了其他监控该主节点的哨兵实例,这里的privdata就指向master节点存储的其他哨兵实例中回复了is-master-down-by-addr命令的那个哨兵实例
sentinelRedisInstance *ri = privdata;
instanceLink *link = c->data;
redisReply *r;

if (!reply || !link) return;
link->pending_commands--;
r = reply;
if (r->type == REDIS_REPLY_ARRAY && r->elements == 3 &&
r->element[0]->type == REDIS_REPLY_INTEGER &&
r->element[1]->type == REDIS_REPLY_STRING &&
r->element[2]->type == REDIS_REPLY_INTEGER)
{
ri->last_master_down_reply_time = mstime();
// 如果回复主观下线
if (r->element[0]->integer == 1) {
// 将回复命令的哨兵节点的flags状态改为SRI_MASTER_DOWN
ri->flags |= SRI_MASTER_DOWN;
} else {
ri->flags &= ~SRI_MASTER_DOWN;
}
// 如果runid不是*
if (strcmp(r->element[1]->str,"*")) {
sdsfree(ri->leader);
if ((long long)ri->leader_epoch != r->element[2]->integer)
serverLog(LL_WARNING,
"%s voted for %s %llu", ri->name,
r->element[1]->str,
(unsigned long long) r->element[2]->integer);
// 更新回复命令的哨兵存储的leader
ri->leader = sdsnew(r->element[1]->str);
// 更新投票轮次
ri->leader_epoch = r->element[2]->integer;
}
}
}

故障切换状态机

在sentinelHandleRedisInstance函数中,判断是否需要执行故障切换之后,就会调用sentinelFailoverStateMachine函数进入故障切换状态机,根据failover_state故障切换状态调用不同的方法,我们先关注以下两种状态:

  1. SENTINEL_FAILOVER_STATE_WAIT_START:等待执行状态,表示需要执行故障切换但还未开始,在sentinelStartFailoverIfNeeded函数中可以看到如果需要执行故障切换,会调用sentinelStartFailover函数将状态置为SENTINEL_FAILOVER_STATE_WAIT_START,对应的处理函数为sentinelFailoverWaitStart,sentinelFailoverWaitStart中会判断是当前哨兵节点是否是执行故障切换的leader,如果是将状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE。

  2. SENTINEL_FAILOVER_STATE_SELECT_SLAVE:从SLAVE节点中选举Master节点的状态,处于这个状态意味着需要从Master的从节点中选举出可以替代Master节点的从节点,进行故障切换,对应的处理函数为sentinelFailoverSelectSlave。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void sentinelFailoverStateMachine(sentinelRedisInstance *ri) {
serverAssert(ri->flags & SRI_MASTER);
// 如果已经在故障切换执行中,直接返回
if (!(ri->flags & SRI_FAILOVER_IN_PROGRESS)) return;
// 状态机
switch(ri->failover_state) {
case SENTINEL_FAILOVER_STATE_WAIT_START:// 等待执行
sentinelFailoverWaitStart(ri);
break;
case SENTINEL_FAILOVER_STATE_SELECT_SLAVE: // 选举master节点
sentinelFailoverSelectSlave(ri);
break;
case SENTINEL_FAILOVER_STATE_SEND_SLAVEOF_NOONE:
sentinelFailoverSendSlaveOfNoOne(ri);
break;
case SENTINEL_FAILOVER_STATE_WAIT_PROMOTION:
sentinelFailoverWaitPromotion(ri);
break;
case SENTINEL_FAILOVER_STATE_RECONF_SLAVES:
sentinelFailoverReconfNextSlave(ri);
break;
}
}

sentinelFailoverWaitStart

sentinelFailoverWaitStart函数的处理逻辑如下:

  1. 调用sentinelGetLeader获取执行故障切换的leader
  2. 对比当前哨兵是与获取到执行故障切换leader的myid是否一致,判断当前哨兵是否是执行故障切换的leader
  3. 如果当前哨兵不是故障切换leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换
  4. 如果当前哨兵是故障切换的leader节点,将故障切换状态改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点进行故障切换
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
void sentinelFailoverWaitStart(sentinelRedisInstance *ri) {
char *leader;
int isleader;

/* 获取执行故障切换的leader */
leader = sentinelGetLeader(ri, ri->failover_epoch);
// leader不为空并且与当前哨兵的myid一致
isleader = leader && strcasecmp(leader,sentinel.myid) == 0;
sdsfree(leader);

/* 如果当前哨兵不是leader, 并且不是强制执行状态SRI_FORCE_FAILOVER,当前哨兵不能执行故障切换 */
if (!isleader && !(ri->flags & SRI_FORCE_FAILOVER)) {
int election_timeout = SENTINEL_ELECTION_TIMEOUT;
if (election_timeout > ri->failover_timeout)
election_timeout = ri->failover_timeout;
/* 在超时时终止故障切换 */
if (mstime() - ri->failover_start_time > election_timeout) {
sentinelEvent(LL_WARNING,"-failover-abort-not-elected",ri,"%@");
sentinelAbortFailover(ri);
}
return;
}
sentinelEvent(LL_WARNING,"+elected-leader",ri,"%@");
if (sentinel.simfailure_flags & SENTINEL_SIMFAILURE_CRASH_AFTER_ELECTION)
sentinelSimFailureCrash();
// 更改为SENTINEL_FAILOVER_STATE_SELECT_SLAVE状态,在下一次执行故障切换状态机时会从slave节点选出master节点
ri->failover_state = SENTINEL_FAILOVER_STATE_SELECT_SLAVE;
ri->failover_state_change_time = mstime();
sentinelEvent(LL_WARNING,"+failover-state-select-slave",ri,"%@");
}

Leader选举

sentinelGetLeader

sentinelGetLeader函数用于从指定的投票轮次epoch中获取Leader节点,成为一个Leader节点需要获取大多数的投票,处理逻辑如下:

  1. 创建了一个counters字典,里面记录了每个哨兵得到的投票数,其中key为哨兵实例的id
    • counters的数据来源:遍历master->sentinels获取其他哨兵实例,判断哨兵实例记录的leader是否为空并且投票轮次与当前指定的epoch一致,如果一致加入counters中并将投票数增加一票
  2. 从counters中获取投票数最多的哨兵实例记为winner,最大投票数记为max_votes
  3. 判断winner是否为空
    • 如果不为空,在master节点中记录的leader节点和winner节点中,选出纪元(投票轮次)最新的节点记为myvote
    • 如果为空,在master节点中记录的leader节点和当前哨兵实例中,选出纪元(投票轮次)最新的节点记为myvote
  4. 经过上一步之后,如果myvote不为空并且leader_epoch与调用sentinelGetLeader函数时指定的epoch一致,当前哨兵给myvote增加一票,然后判断myvote得到的投票数是否大于max_votes,如果是将winner获胜者更新为myvote
  5. 到这里,winner中记录了本轮投票的获胜者,也就是得到票数最多的那个,max_votes记录了获得投票数,能否成为leader还需满足以下两个条件,,保证选举出的leader得到了过半哨兵的投票:
    • 条件一:得到的投票数max_votes大于voters_quorum,voters_quorum为哨兵实例个数的一半+1,也就是需要有过半的哨兵实例
    • 条件二:得到的投票数max_votes大于master->quorum,这个值是在 sentinel.conf 配置文件中设置的,一般设置为哨兵总数的一半+1
  6. 选举结束,返回winner中记录的实例id作为leader
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
char *sentinelGetLeader(sentinelRedisInstance *master, uint64_t epoch) {
dict *counters;
dictIterator *di;
dictEntry *de;
unsigned int voters = 0, voters_quorum;
char *myvote;
char *winner = NULL;
uint64_t leader_epoch;
uint64_t max_votes = 0;

serverAssert(master->flags & (SRI_O_DOWN|SRI_FAILOVER_IN_PROGRESS));
// 创建字典
counters = dictCreate(&leaderVotesDictType,NULL);
// 获取所有的哨兵实例个数包含当前哨兵
voters = dictSize(master->sentinels)+1;

/* 遍历哨兵实例 */
di = dictGetIterator(master->sentinels);
while((de = dictNext(di)) != NULL) {
// 获取每一个哨兵节点
sentinelRedisInstance *ri = dictGetVal(de);
// 如果某个哨兵实例的leader不为空并且leader的轮次等于指定的轮次
if (ri->leader != NULL && ri->leader_epoch == sentinel.current_epoch)
sentinelLeaderIncr(counters,ri->leader); // ri->leader的投票数加1
}
dictReleaseIterator(di);
di = dictGetIterator(counters);
while((de = dictNext(di)) != NULL) {
// 获取投票数
uint64_t votes = dictGetUnsignedIntegerVal(de);
// 获取投票数最多的节点的runid,记录在winner中
if (votes > max_votes) {
max_votes = votes;
winner = dictGetKey(de);
}
}
dictReleaseIterator(di);

/* 如果winner不为空,在master节点和winner节点中获取epoch投票轮次最新的当做当前节点的投票者*/
/* 如果winner为空,在master节点和当前哨兵节点中选取epoch投票轮次最新的节点*/
if (winner)
myvote = sentinelVoteLeader(master,epoch,winner,&leader_epoch);
else
myvote = sentinelVoteLeader(master,epoch,sentinel.myid,&leader_epoch);
//
if (myvote && leader_epoch == epoch) {
// 为myvote增加一票
uint64_t votes = sentinelLeaderIncr(counters,myvote);
// 如果超过了max_votes
if (votes > max_votes) {
max_votes = votes; // 更新max_votes
winner = myvote; // 更新winner
}
}
// voters_quorum,为哨兵实例个数的一半+1
voters_quorum = voters/2+1;
// 如果投票数量max_votes小于quorum,说明未达到过半的投票数
if (winner && (max_votes < voters_quorum || max_votes < master->quorum))
winner = NULL;

winner = winner ? sdsnew(winner) : NULL;
sdsfree(myvote);
dictRelease(counters);
// 返回获胜的节点,也就是leader节点
return winner;
}

投票

sentinelVoteLeader

sentinelVoteLeader函数入参中可以看到传入了master节点,和候选节点的IDreq_runid以及候选节点的投票纪元(轮次)req_epoch。master节点中记录了当前哨兵节点选举的执行故障切换的leader节点,在master->leader中保存。sentinelVoteLeader就用于在这两个节点中选出获胜的那个节点:

  1. 如果候选节点的req_epoch大于当前sentinel实例的epoch,将当前哨兵实例的current_epoch置为请求轮次req_epoch
  2. 如果master节点记录的leader_epoch小于候选节点的req_epoch,并且当前实例的轮次小于等于候选节点轮次,将master节点中的leader改为候选节点
  3. 返回master节点中记录的leader
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
char *sentinelVoteLeader(sentinelRedisInstance *master, uint64_t req_epoch, char *req_runid, uint64_t *leader_epoch) {
// 如果请求的轮次req_epoch大于当前sentinel实例的轮次
if (req_epoch > sentinel.current_epoch) {
// 将哨兵实例的current_epoch置为请求轮次
sentinel.current_epoch = req_epoch;
sentinelFlushConfig();
sentinelEvent(LL_WARNING,"+new-epoch",master,"%llu",
(unsigned long long) sentinel.current_epoch);
}
// 如果master节点记录的轮次leader_epoch小于请求轮次,并且当前实例的轮次小于等于请求轮次
if (master->leader_epoch < req_epoch && sentinel.current_epoch <= req_epoch)
{
sdsfree(master->leader);
// 将leader置为传入的runid
master->leader = sdsnew(req_runid);
// 更改master的leader_epoch
master->leader_epoch = sentinel.current_epoch;
sentinelFlushConfig();
sentinelEvent(LL_WARNING,"+vote-for-leader",master,"%s %llu",
master->leader, (unsigned long long) master->leader_epoch);
if (strcasecmp(master->leader,sentinel.myid))
master->failover_start_time = mstime()+rand()%SENTINEL_MAX_DESYNC;
}

*leader_epoch = master->leader_epoch;
return master->leader ? sdsnew(master->leader) : NULL;
}

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】哨兵初始化和主观下线

Posted on 2022-05-03

在的redis启动函数main(server.c文件)中,对哨兵模式进行了检查,如果是哨兵模式,将调用initSentinelConfig和initSentinel进行初始化,initServer函数中会注册哨兵的时间事件,最后调用sentinelIsRunning运行哨兵实例,

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
int main(int argc, char **argv) {
// 省略...

// 检查哨兵模式
server.sentinel_mode = checkForSentinelMode(argc,argv);
initServerConfig();
// 省略...

if (server.sentinel_mode) {
initSentinelConfig(); // 初始化哨兵配置
initSentinel(); // 初始化哨兵
}

// 省略...
// 初始化服务
initServer();
// 省略...
if (!server.sentinel_mode) { // 非哨兵模式
// 省略...
} else {
ACLLoadUsersAtStartup();
InitServerLast();
// 运行哨兵实例
sentinelIsRunning();
if (server.supervised_mode == SUPERVISED_SYSTEMD) {
redisCommunicateSystemd("STATUS=Ready to accept connections\n");
redisCommunicateSystemd("READY=1\n");
}
}
// 省略...

aeMain(server.el);
aeDeleteEventLoop(server.el);
return 0;
}

哨兵初始化

哨兵模式校验

checkForSentinelMode

checkForSentinelMode函数在server.c文件中,用于校验是否是哨兵模式,可以看到有两种方式校验哨兵模式:

  1. 直接执行redis-sentinel命令
  2. 执行redis-server命令,命令参数中带有–sentinel参数
1
2
3
4
5
6
7
8
int checkForSentinelMode(int argc, char **argv) {
int j;
// 直接执行redis-sentinel
if (strstr(argv[0],"redis-sentinel") != NULL) return 1;
for (j = 1; j < argc; j++)
if (!strcmp(argv[j],"--sentinel")) return 1; // 执行的命令参数中,是否有–sentinel
return 0;
}

初始化配置项

initSentinelConfig

initSentinelConfig函数在sentinel.c文件中,用于初始化哨兵配置:

  1. 将哨兵实例的端口号设置为REDIS_SENTINEL_PORT,默认值26379
  2. 将protected_mode设置为0,表示允许外部链接哨兵实例,而不是只能通过127.0.0.1本地连接 server
1
2
3
4
5
6
#define REDIS_SENTINEL_PORT 26379

void initSentinelConfig(void) {
server.port = REDIS_SENTINEL_PORT; /* 设置默认端口 */
server.protected_mode = 0; /* 允许外部链接哨兵实例 */
}

initSentinel

在看initSentinel函数之前,首先看下Redis中哨兵sentinel对象对应的结构体sentinelState:

  • current_epoch:当前纪元,投票选举Leader时使用,纪元可以理解为投票的轮次
  • masters:监控的master节点哈希表,Key为master节点名称, value为master节点对应sentinelRedisInstance实例的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct sentinelState {
char myid[CONFIG_RUN_ID_SIZE+1]; /* sentinel ID */
uint64_t current_epoch; /* 当前的纪元(投票轮次)*/
dict *masters; /* 监控的master节点哈希表,Key为master节点名称, value为master节点对应的实例对象的指针 */
int tilt; /* TILT模式 */
int running_scripts;
mstime_t tilt_start_time;
mstime_t previous_time;
list *scripts_queue;
char *announce_ip;
int announce_port;
unsigned long simfailure_flags;
int deny_scripts_reconfig;
char *sentinel_auth_pass;
char *sentinel_auth_user;
int resolve_hostnames;
int announce_hostnames;
} sentinel;

sentinelRedisInstance是一个通用的结构体,在sentinel.c文件中定义,它既可以表示主节点,也可以表示从节点或者其他哨兵实例,从中选选出了一些主要的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef struct  {
int flags; /* 一些状态标识 */
char *name; /* Master name from the point of view of this sentinel. */
char *runid; /* 实例的运行ID */
uint64_t config_epoch; /* 配置的纪元. */
mstime_t s_down_since_time; /* 主观下线时长 */
mstime_t o_down_since_time; /* 客观下线时长 */
dict *sentinels; /* 监控同一主节点的其他哨兵实例 */
dict *slaves; /* slave节点(从节点) */
/* 故障切换 */
char *leader; /* 如果是master节点,保存了需要执行故障切换的哨兵leader的runid,如果是一个哨兵,保存的是这个哨兵投票选举的leader*/
uint64_t leader_epoch; /* leader纪元 */
uint64_t failover_epoch;
int failover_state; /* 故障切换状态 */

// 省略...
} sentinelRedisInstance;

initSentinel函数同样在sentinel.c文件中,用于初始化哨兵,由于哨兵实例与普通Redis实例不一样,所以需要替换Redis中的命令,添加哨兵实例命令,哨兵实例使用的命令在sentinelcmds中定义:

  1. 将server.commands和server.orig_commands保存的常规Redis命令清除
  2. 遍历sentinelcmds哨兵实例专用命令,将命令添加到server.commands和server.orig_commands中
  3. 初始化sentinel实例中的数据项
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 哨兵实例下的命令
struct redisCommand sentinelcmds[] = {
{"ping",pingCommand,1,"fast @connection",0,NULL,0,0,0,0,0},
{"sentinel",sentinelCommand,-2,"admin",0,NULL,0,0,0,0,0},
{"subscribe",subscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"unsubscribe",unsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"psubscribe",psubscribeCommand,-2,"pub-sub",0,NULL,0,0,0,0,0},
{"punsubscribe",punsubscribeCommand,-1,"pub-sub",0,NULL,0,0,0,0,0},
{"publish",sentinelPublishCommand,3,"pub-sub fast",0,NULL,0,0,0,0,0},
{"info",sentinelInfoCommand,-1,"random @dangerous",0,NULL,0,0,0,0,0},
{"role",sentinelRoleCommand,1,"fast read-only @dangerous",0,NULL,0,0,0,0,0},
{"client",clientCommand,-2,"admin random @connection",0,NULL,0,0,0,0,0},
{"shutdown",shutdownCommand,-1,"admin",0,NULL,0,0,0,0,0},
{"auth",authCommand,-2,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"hello",helloCommand,-1,"no-auth fast @connection",0,NULL,0,0,0,0,0},
{"acl",aclCommand,-2,"admin",0,NULL,0,0,0,0,0,0},
{"command",commandCommand,-1, "random @connection", 0,NULL,0,0,0,0,0,0}
};

/* 初始化哨兵 */
void initSentinel(void) {
unsigned int j;

/* 将常规的Redis命令移除,增加哨兵实例专用的命令 */
dictEmpty(server.commands,NULL);
dictEmpty(server.orig_commands,NULL);
ACLClearCommandID();
for (j = 0; j < sizeof(sentinelcmds)/sizeof(sentinelcmds[0]); j++) {
int retval;
struct redisCommand *cmd = sentinelcmds+j;
cmd->id = ACLGetCommandID(cmd->name);
// 添加到server.commands
retval = dictAdd(server.commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
// 添加到server.orig_commands
retval = dictAdd(server.orig_commands, sdsnew(cmd->name), cmd);
serverAssert(retval == DICT_OK);
if (populateCommandTableParseFlags(cmd,cmd->sflags) == C_ERR)
serverPanic("Unsupported command flag");
}

/* 初始化其他数据项 */
// current_epoch初始化为0
sentinel.current_epoch = 0;
// 监控的master节点实例对象
sentinel.masters = dictCreate(&instancesDictType,NULL);
sentinel.tilt = 0;
sentinel.tilt_start_time = 0;
sentinel.previous_time = mstime();
sentinel.running_scripts = 0;
sentinel.scripts_queue = listCreate();
sentinel.announce_ip = NULL;
sentinel.announce_port = 0;
sentinel.simfailure_flags = SENTINEL_SIMFAILURE_NONE;
sentinel.deny_scripts_reconfig = SENTINEL_DEFAULT_DENY_SCRIPTS_RECONFIG;
sentinel.sentinel_auth_pass = NULL;
sentinel.sentinel_auth_user = NULL;
sentinel.resolve_hostnames = SENTINEL_DEFAULT_RESOLVE_HOSTNAMES;
sentinel.announce_hostnames = SENTINEL_DEFAULT_ANNOUNCE_HOSTNAMES;
memset(sentinel.myid,0,sizeof(sentinel.myid));
server.sentinel_config = NULL;
}

启动哨兵实例

sentinelIsRunning

sentinelIsRunning函数在sentinel.c文件中,用于启动哨兵实例:

  1. 校验是否设置了哨兵实例的ID,如果未设置,将随机生成一个ID
  2. 调用sentinelGenerateInitialMonitorEvents向监控的主节点发送+monitor事件
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
void sentinelIsRunning(void) {
int j;

/* 校验myid是否为0 */
for (j = 0; j < CONFIG_RUN_ID_SIZE; j++)
if (sentinel.myid[j] != 0) break;

if (j == CONFIG_RUN_ID_SIZE) {
/* 随机生成ID */
getRandomHexChars(sentinel.myid,CONFIG_RUN_ID_SIZE);
sentinelFlushConfig();
}

serverLog(LL_WARNING,"Sentinel ID is %s", sentinel.myid);

/* 向监控的主节点发送+monitor事件 */
sentinelGenerateInitialMonitorEvents();
}

/* 向监控的主节点发布事件 */
void sentinelGenerateInitialMonitorEvents(void) {
dictIterator *di;
dictEntry *de;
// 获取监控的主节点
di = dictGetIterator(sentinel.masters);
while((de = dictNext(di)) != NULL) {
sentinelRedisInstance *ri = dictGetVal(de);
// 向主节点发送监控事件
sentinelEvent(LL_WARNING,"+monitor",ri,"%@ quorum %d",ri->quorum);
}
dictReleaseIterator(di);
}

哨兵时间事件

在initServer函数中,调用aeCreateTimeEvent注册了时间事件,周期性的执行serverCron函数,serverCron函数中通过server.sentinel_mode判断是否是哨兵模式,如果是哨兵模式,调用sentinelTimer执行哨兵事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void initServer(void) {

// 省略...
if (aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
serverPanic("Can't create event loop timers.");
exit(1);
}
// 省略...
}

int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
// 省略...

// 如果是哨兵模式,调用sentinelTimer执行哨兵事件
if (server.sentinel_mode) sentinelTimer();

// 省略...
}

sentinelTimer

sentinelTimer在sentinel.c文件中,sentinelTimer函数会周期性的执行:

1
2
3
4
5
6
7
8
9
10
void sentinelTimer(void) {
sentinelCheckTiltCondition();
// 处理RedisInstances,传入的参数是当前哨兵实例维护的主节点的哈希表,里面记录当前节点监听的主节点
sentinelHandleDictOfRedisInstances(sentinel.masters);
sentinelRunPendingScripts();
sentinelCollectTerminatedScripts();
sentinelKillTimedoutScripts();
// 调整sentinelTimer的执行频率
server.hz = CONFIG_DEFAULT_HZ + rand() % CONFIG_DEFAULT_HZ;
}

sentinelHandleDictOfRedisInstances

sentinelHandleDictOfRedisInstances函数中会对传入的当前哨兵实例监听的主节点哈希表进行遍历:

  1. 获取哈希表中的每一个节点,节点类型是sentinelRedisInstance
  2. 调用sentinelHandleRedisInstance检测哨兵监听节点的状态
  3. 如果sentinelHandleRedisInstance是主节点,由于主节点里面保存了监听该主节点的其他哨兵实例以及从节点,所以递归调用sentinelHandleDictOfRedisInstances对其他的节点进行检测
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
/* sentinelHandleDictOfRedisInstances */
void sentinelHandleDictOfRedisInstances(dict *instances) {
dictIterator *di;
dictEntry *de;
sentinelRedisInstance *switch_to_promoted = NULL;
di = dictGetIterator(instances);
// 遍历所有的sentinelRedisInstance实例
while((de = dictNext(di)) != NULL) {
// 获取每一个sentinelRedisInstance
sentinelRedisInstance *ri = dictGetVal(de);
// 调用sentinelHandleRedisInstance检测哨兵监听节点的状态
sentinelHandleRedisInstance(ri);
// 如果是sentinelRedisInstance是主节点,主节点里面保存了监听该主节点的其他哨兵实例以及从节点
if (ri->flags & SRI_MASTER) {
// 递归调用,处理从节点
sentinelHandleDictOfRedisInstances(ri->slaves);
// 递归调用,处理其他哨兵实例
sentinelHandleDictOfRedisInstances(ri->sentinels);
if (ri->failover_state == SENTINEL_FAILOVER_STATE_UPDATE_CONFIG) {
switch_to_promoted = ri;
}
}
}
if (switch_to_promoted)
sentinelFailoverSwitchToPromotedSlave(switch_to_promoted);
dictReleaseIterator(di);
}

检测哨兵监听的节点状态

sentinelHandleRedisInstance

sentinelHandleRedisInstance函数对传入的sentinelRedisInstance实例,进行状态检测,主要处理逻辑如下:

  1. 调用sentinelReconnectInstance对实例的连接状态进行判断,如果连接中断尝试重新与实例建立连接
  2. 调用sentinelSendPeriodicCommands向实例发送PING INFO等命令
  3. sentinelCheckSubjectivelyDown判断实例是否主观下线
  4. 如果实例是master节点,调用sentinelCheckObjectivelyDown判断是否客观下线、是否需要执行故障切换
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

/* Perform scheduled operations for the specified Redis instance. */
void sentinelHandleRedisInstance(sentinelRedisInstance *ri) {

// 如果监听的节点连接中断,尝试重新建立连接
sentinelReconnectInstance(ri);
// 发送PING INFO等命令
sentinelSendPeriodicCommands(ri);

// 检查是否是TILT模式
if (sentinel.tilt) {
if (mstime()-sentinel.tilt_start_time < SENTINEL_TILT_PERIOD) return;
sentinel.tilt = 0;
sentinelEvent(LL_WARNING,"-tilt",NULL,"#tilt mode exited");
}

// 判断主观下线
sentinelCheckSubjectivelyDown(ri);

/* Masters and slaves */
if (ri->flags & (SRI_MASTER|SRI_SLAVE)) {
}

// 如果是master节点
if (ri->flags & SRI_MASTER) {
// 判断客观下线
sentinelCheckObjectivelyDown(ri);
// 是否需要启动故障切换
if (sentinelStartFailoverIfNeeded(ri))
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_ASK_FORCED);
// 执行故障切换
sentinelFailoverStateMachine(ri);
// 获取其他哨兵实例对master节点状态的判断
sentinelAskMasterStateToOtherSentinels(ri,SENTINEL_NO_FLAGS);
}
}

重新连接

sentinelReconnectInstance

sentinelReconnectInstance函数用于检测实例的连接状态,如果中断进行重连,主要处理逻辑如下:

  1. 检测连接是否中断,如果未中断直接返回

  2. 检查端口是否为0,0被认为是不合法的端口

  3. 从sentinelRedisInstance实例中获取instanceLink,instanceLink的定义在sentinel.c文件中,里面记录了哨兵和主节点的两个连接,分别为用于发送命令的连接cc和用于发送Pub/Sub消息的连接pc:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    typedef struct instanceLink {
    int refcount;
    int disconnected;
    int pending_commands;
    redisAsyncContext *cc; /* 用于发送命令的连接 */
    redisAsyncContext *pc; /* 用于发送Pub/Sub消息的连接 */
    mstime_t cc_conn_time; /* cc 连接时间 */
    mstime_t pc_conn_time; /* pc 连接时间 */
    mstime_t pc_last_activity;
    mstime_t last_avail_time; /* 上一次收到实例回复PING命令(需要被认定为合法)的时间 */
    mstime_t act_ping_time; /* 当收到PONG消息的时候会设置为0,在下次发送PING命令时设置为当前时间 */
    mstime_t last_ping_time; /* 上次发送PING命令时间,在出现故障时可以通过判断发送时间避免多次发送PING命令 */
    mstime_t last_pong_time; /* 上次收到PONG消息的时间 */
    mstime_t last_reconn_time; /* 上次执行重连的时间 */
    } instanceLink;
  4. 校验距离上次重连时间是否小于PING的检测周期SENTINEL_PING_PERIOD,如果小于说明距离上次重连时间过近,直接返回即可

    SENTINEL_PING_PERIOD在server.c中定义,默认1000毫秒

    1
    #define SENTINEL_PING_PERIOD 1000
  5. 对用于发送命令的连接判断,如果连接为NULL,调用redisAsyncConnectBind函数进行重连

  6. 对用于处理发送 Pub/Sub 消息的连接进行判断,如果连接为NULL,调用redisAsyncConnectBind函数进行重连

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
void sentinelReconnectInstance(sentinelRedisInstance *ri) {
// 检查连接是否中断
if (ri->link->disconnected == 0) return;
if (ri->addr->port == 0) return; /* 检查端口是否为0,0被认为是不合法的端口 */
// 获取instanceLink
instanceLink *link = ri->link;
mstime_t now = mstime();
// 校验距离上次重连时间是否小于哨兵PING的周期设置
if (now - ri->link->last_reconn_time < SENTINEL_PING_PERIOD) return;
ri->link->last_reconn_time = now;

/* 处理用于发送命令的连接 */
if (link->cc == NULL) {
// 进行连接
link->cc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
if (link->cc && !link->cc->err) anetCloexec(link->cc->c.fd);
// 省略...
}
/* 处理用于发送 Pub/Sub 消息的连接 */
if ((ri->flags & (SRI_MASTER|SRI_SLAVE)) && link->pc == NULL) {
link->pc = redisAsyncConnectBind(ri->addr->ip,ri->addr->port,NET_FIRST_BIND_ADDR);
if (link->pc && !link->pc->err) anetCloexec(link->pc->c.fd);
// 省略...
}
if (link->cc && (ri->flags & SRI_SENTINEL || link->pc))
link->disconnected = 0;
}

发送命令

sentinelSendPeriodicCommands

sentinelSendPeriodicCommands用于向实例发送命令:

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
void sentinelSendPeriodicCommands(sentinelRedisInstance *ri) {
mstime_t now = mstime();
mstime_t info_period, ping_period;
int retval;

// 省略...

/* 向主节点和从节点发送INFO命令 */
if ((ri->flags & SRI_SENTINEL) == 0 &&
(ri->info_refresh == 0 ||
(now - ri->info_refresh) > info_period))
{
// 发送INFO命令
retval = redisAsyncCommand(ri->link->cc,
sentinelInfoReplyCallback, ri, "%s",
sentinelInstanceMapCommand(ri,"INFO"));
if (retval == C_OK) ri->link->pending_commands++;
}

if ((now - ri->link->last_pong_time) > ping_period &&
(now - ri->link->last_ping_time) > ping_period/2) {
// 向实例发送PING命令
sentinelSendPing(ri);
}

/* PUBLISH hello messages to all the three kinds of instances. */
if ((now - ri->last_pub_time) > SENTINEL_PUBLISH_PERIOD) {
// 发送PUBLISH命令
sentinelSendHello(ri);
}
}

主观下线

sentinelCheckSubjectivelyDown

sentinelCheckSubjectivelyDown函数用于判断是否主观下线。

标记主观下线的两个条件

  • 距离上次发送PING命令的时长超过了down_after_period的值,down_after_period的值在sentinel.conf 配置文件中配置,对应的配置项为down-after-milliseconds ,默认值30s
  • 哨兵认为实例是主节点(ri->flags & SRI_MASTE),但是实例向哨兵返回的角色是从节点(ri->role_reported == SRI_SLAVE) 并且当前时间-实例报告消息的时间role_reported_time大于down_after_period加上SENTINEL_INFO_PERIOD乘以2的时间 ,SENTINEL_INFO_PERIOD 是发送INFO命令的时间间隔,也就是说实例上次成功向哨兵报告角色的时间,已经超过了限定时间(down_after_period加上SENTINEL_INFO_PERIOD*2)

满足以上两个条件之一哨兵将会把sentinelRedisInstance判断为主观下线,flag标记会添加SRI_S_DOWN状态。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
void sentinelCheckSubjectivelyDown(sentinelRedisInstance *ri) {
mstime_t elapsed = 0;
// 如果act_ping_time不为0
if (ri->link->act_ping_time)
elapsed = mstime() - ri->link->act_ping_time; // 计算距离上次发送PING命令的间隔时间
else if (ri->link->disconnected) // 如果连接断开
elapsed = mstime() - ri->link->last_avail_time; // 计算距离最近一次收到PING命令回复的间隔时间

if (ri->link->cc &&
(mstime() - ri->link->cc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
ri->link->act_ping_time != 0 &&
(mstime() - ri->link->act_ping_time) > (ri->down_after_period/2) &&
(mstime() - ri->link->last_pong_time) > (ri->down_after_period/2))
{
instanceLinkCloseConnection(ri->link,ri->link->cc);
}

if (ri->link->pc &&
(mstime() - ri->link->pc_conn_time) >
SENTINEL_MIN_LINK_RECONNECT_PERIOD &&
(mstime() - ri->link->pc_last_activity) > (SENTINEL_PUBLISH_PERIOD*3))
{
instanceLinkCloseConnection(ri->link,ri->link->pc);
}

/*
* 标记主观下线的两个条件(或的关系)
* 1) 距离上次发送PING命令的时长超过了down_after_period
* 2) 哨兵认为实例是主节点(ri->flags & SRI_MASTE),但是实例向哨兵返回的角色是从节点(ri->role_reported == SRI_SLAVE) 并且当前时间-实例返回消息的时间大于down_after_period加上SENTINEL_INFO_PERIOD*2的时间 */
if (elapsed > ri->down_after_period ||
(ri->flags & SRI_MASTER &&
ri->role_reported == SRI_SLAVE &&
mstime() - ri->role_reported_time >
(ri->down_after_period+SENTINEL_INFO_PERIOD*2)))
{
/* 主观下线 */
if ((ri->flags & SRI_S_DOWN) == 0) {
// 发送+sdown事件
sentinelEvent(LL_WARNING,"+sdown",ri,"%@");
ri->s_down_since_time = mstime();
ri->flags |= SRI_S_DOWN; // 更改状态
}
} else {
/* Is subjectively up */
if (ri->flags & SRI_S_DOWN) {
sentinelEvent(LL_WARNING,"-sdown",ri,"%@");
ri->flags &= ~(SRI_S_DOWN|SRI_SCRIPT_KILL_SENT);
}
}
}

客观下线

如果是主节点,将会调用sentinelCheckObjectivelyDown函数判断客观下线,之后调用sentinelStartFailoverIfNeeded判断是否需要执行故障切换。

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】事件驱动源码分析(多线程)

Posted on 2022-04-30

IO线程初始化

Redis在6.0版本中引入了多线程,提高IO请求处理效率。

在Redis Server启动函数main(server.c文件)中初始化服务之后,又调用了InitServerLast函数:

1
2
3
4
5
6
7
8
9
10
11
12
int main(int argc, char **argv) {
// ...
// 初始化服务
initServer();
// ...
// InitServerLast
InitServerLast();
// ...
// 事件循环
aeMain(server.el);
// ...
}

InitServerLast函数在server.c文件中,它调用了initThreadedIO函数对IO线程初始化:

1
2
3
4
5
6
7
void InitServerLast() {
bioInit();
// 初始化IO线程
initThreadedIO();
set_jemalloc_bg_thread(server.jemalloc_bg_thread);
server.initial_memory_usage = zmalloc_used_memory();
}

initThreadedIO

initThreadedIO的实现在networking.c文件中:

  1. 初始化全局变量 server.io_threads_active线程活跃状态为0,表示未激活IO多线程
  2. 对server.io_threads_num的值进行判断,io_threads_num表示设置的IO线程数量
    • 如果线程数设置为1,表示不开启多线程直接返回即可
    • 如果线程数超过了IO_THREADS_MAX_NUM设置的最大值(128),则报错并停止redis服务
  3. 根据线程数的设置创建线程
    • 初始化io_threads_list[i],io_threads_list是一个数组,数组中的每一个元素是一个list,里面存储每个线程要处理的客户端列表,下标为0的元素也就是io_threads_lis[0]存储的是主线程要处理的客户端列表,这里先调用listCreate创建列表,为io_threads_list[i]初始化
    • 初始化io_threads_pending[i]为0,io_threads_pending数组存储每个线程等待处理的客户端个数
    • 调用pthread_create创建线程,并传入了线程的运行函数IOThreadMain,之后将线程保存在io_threads中,io_threads数组存储了创建的线程描述符
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
38
39

/* 初始化线程 */
void initThreadedIO(void) {
server.io_threads_active = 0; /* 初始化线程活跃状态为0,表示未激活IO多线程 */

/* 如果IO线程数为1,直接返回即可 */
if (server.io_threads_num == 1) return;
/* 如果IO线程数超过了最大限制,打印错误,停止redis服务 */
if (server.io_threads_num > IO_THREADS_MAX_NUM) {
serverLog(LL_WARNING,"Fatal: too many I/O threads configured. "
"The maximum number is %d.", IO_THREADS_MAX_NUM);
exit(1);
}

/* 根据线程数设置创建线程 */
for (int i = 0; i < server.io_threads_num; i++) {
/* 创建List */
io_threads_list[i] = listCreate();
if (i == 0) continue; /* 下标为0的存储的是主线程 */
pthread_t tid;
pthread_mutex_init(&io_threads_mutex[i],NULL);
// 初始化待处理的客户端数量为0
setIOPendingCount(i, 0);
pthread_mutex_lock(&io_threads_mutex[i]); /* Thread will be stopped. */
// 创建线程, 线程的运行函数为IOThreadMain
if (pthread_create(&tid,NULL,IOThreadMain,(void*)(long)i) != 0) {
serverLog(LL_WARNING,"Fatal: Can't initialize IO thread.");
exit(1);
}
/* 将创建的线程加入io_threads线程组中*/
io_threads[i] = tid;
}
}

// setIOPendingCount在networking.c文件
static inline void setIOPendingCount(int i, unsigned long count) {
// 设置io_threads_pending[i]的值为count
atomicSetWithSync(io_threads_pending[i], count);
}

io_threads_list

1
2
/* io_threads_list存储每个线程要处理的客户端 */
list *io_threads_list[IO_THREADS_MAX_NUM];

io_threads

1
2
/* 存储创建的线程*/
pthread_t io_threads[IO_THREADS_MAX_NUM];

io_threads_pending

1
2
/* 存储每个线程要等待处理的客户端个数 */
redisAtomic unsigned long io_threads_pending[IO_THREADS_MAX_NUM];

IO_THREADS_MAX_NUM定义

1
#define IO_THREADS_MAX_NUM 128

初始化流程图

IO线程运行函数

IO线程运行函数IOThreadMain在networking.c文件中,函数的入参传入的是线程id,它开启了一个while(1)循环,主要处理逻辑如下:

  1. 从io_threads_list数组中获取当前线程id要处理的客户端列表,放入到列表迭代器li中
  2. 遍历迭代器,获取每一个待处理的客户端client,根据io_threads_op线程的操作状态判断读写状态
    • 如果是写状态,调用调用writeToClient处理
    • 如果是读状态,调用readQueryFromClient处理
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
38
39
40
41
42
43
44
45
46
47
void *IOThreadMain(void *myid) {
/* myid是线程ID,从0开始,到 server.iothreads_num-1,0号线程存储的是主线程 */
long id = (unsigned long)myid;
char thdname[16];

snprintf(thdname, sizeof(thdname), "io_thd_%ld", id);
redis_set_thread_title(thdname);
redisSetCpuAffinity(server.server_cpulist);
makeThreadKillable();
// 循环
while(1) {
for (int j = 0; j < 1000000; j++) {
if (getIOPendingCount(id) != 0) break;
}
/* Give the main thread a chance to stop this thread. */
if (getIOPendingCount(id) == 0) {
pthread_mutex_lock(&io_threads_mutex[id]);
pthread_mutex_unlock(&io_threads_mutex[id]);
continue;
}

serverAssert(getIOPendingCount(id) != 0);

listIter li;
listNode *ln;
// 获取每一个IO线程要处理的客户端,将其放入到迭代器li,这里的id指的线程id
listRewind(io_threads_list[id],&li);
// 遍历列表
while((ln = listNext(&li))) {
// 获取每一个待处理的客户端
client *c = listNodeValue(ln);
// 如果是写事件
if (io_threads_op == IO_THREADS_OP_WRITE) {
// 调用writeToClient处理
writeToClient(c,0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
// 如果是读事件,调用readQueryFromClient
readQueryFromClient(c->conn);
} else {
serverPanic("io_threads_op value is unknown");
}
}
listEmpty(io_threads_list[id]);
// 处理完毕后,io_threads_pending数组中对应的数量设置为0,表示所有客户端已处理完毕
setIOPendingCount(id, 0);
}
}

io_threads_list数组中存储了每一个线程要处理的客户端列表,在线程运行函数IOThreadMain中,获取待处理的客户端列表,遍历每一个客户端,根据读写类型调用不同的方法进行处理,接下来就去看下Redis在何时将待处理的客户端加入到io_threads_list列表中的。

延迟读写操作

Redis在处理客户端读事件和写事件时会根据一定条件推迟客户端的读取操作或者往客户端写数据操作,将待处理的读客户端和待处理的写客户端分别加入到全局变量server的clients_pending_read和clients_pending_write列表中,全局变量server对应的结构体为redisServer:

全局变量server定义,在server.c文件:

1
2
/* 全局变量server */
struct redisServer server;

redisServer的结构体定义在server.h中:

1
2
3
4
5
6
struct redisServer {

list *clients_pending_write; /* list类型,记录延迟写回数据的客户端 */
list *clients_pending_read; /* list类型,记录延迟读取数据的客户端*/
// 省略...
}

推迟客户端读操作

readQueryFromClient

readQueryFromClient主要处理从客户端读取数据,在networking.c中实现,里面调用了postponeClientRead函数判断是否需要推迟客户端的读取操作 :

1
2
3
4
5
6
7
8
9
10
11
12
13
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
int nread, readlen;
size_t qblen;

/* 判断是否需要推迟客户端的读取操作 */
if (postponeClientRead(c)) return;

// 省略...

// 处理数据执行命令
processInputBuffer(c);
}

postponeClientRead

postponeClientRead函数用于判断是否延迟从客户端读取数据,包含四个条件:

  1. server.io_threads_active为1,表示激活了IO多线程
  2. server.io_threads_do_reads为1,表示IO多线程可以延迟执行客户端的读取操作,在配置文件中定义,可以通过修改配置文件来开启延迟读取客户端数据
  3. ProcessingEventsWhileBlocked值为0,processEventsWhileBlokced函数在执行时会将ProcessingEventsWhileBlocked的值置为1,执行完毕后置为0,Redis在读取RDB或者AOF文件时会调用processEventsWhileBlokced函数,为了避免读取RDB或AOF文件时阻塞无法及时处理请求,processEventsWhileBlokced函数在执行时不能推迟客户端数据读取。
  4. 客户端的现有标识不能有CLIENT_MASTER、CLIENT_SLAVE、CLIENT_PENDING_READ、CLIENT_BLOCKED等状态
    • CLIENT_MASTER、CLIENT_SLAVE表示是用于主从复制的客户端
    • CLIENT_PENDING_READ表示客户端本身已经是推迟读取状态
    • CLIENT_BLOCKED表示客户端是阻塞状态

满足以上四个条件时将推迟从客户端读取数据,会将客户端标识置为CLIENT_PENDING_READ延迟读状态,并将待读取数据的客户端client加入到server.clients_pending_read中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int postponeClientRead(client *c) {
if (server.io_threads_active &&
server.io_threads_do_reads &&
!ProcessingEventsWhileBlocked &&
!(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ|CLIENT_BLOCKED)))
{
c->flags |= CLIENT_PENDING_READ;
// 将客户端加入到clients_pending_read链表中
listAddNodeHead(server.clients_pending_read,c);
return 1;
} else {
return 0;
}
}

推迟客户端写操作

在往客户端写数据的addReply(networking.c)函数中,调用了prepareClientToWrite判断是否准备往客户端写数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void addReply(client *c, robj *obj) {
// 调用prepareClientToWrite往客户端写数据
if (prepareClientToWrite(c) != C_OK) return;

if (sdsEncodedObject(obj)) {
if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
_addReplyProtoToList(c,obj->ptr,sdslen(obj->ptr));
} else if (obj->encoding == OBJ_ENCODING_INT) {
char buf[32];
size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);
if (_addReplyToBuffer(c,buf,len) != C_OK)
_addReplyProtoToList(c,buf,len);
} else {
serverPanic("Wrong obj->encoding in addReply()");
}
}

prepareClientToWrite

prepareClientToWrite(networking.c)中,首先对客户端标识状态进行了一系列的判断,然后调用了clientHasPendingReplies函数判断输出缓冲区是否有还有数据等待写回到客户端,如果没有,判断客户端的标识是否是CLIENT_PENDING_READ已延迟读,如果不是CLIENT_PENDING_READ状态,调用clientInstallWriteHandler处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int prepareClientToWrite(client *c) {
if (c->flags & (CLIENT_LUA|CLIENT_MODULE)) return C_OK;

if (c->flags & CLIENT_CLOSE_ASAP) return C_ERR;

if (c->flags & (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) return C_ERR;

if ((c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_MASTER_FORCE_REPLY)) return C_ERR;

if (!c->conn) return C_ERR;

/*
* 如果缓冲区的数据都已写回到客户端并且客户端标识不是推迟读状态
*/
if (!clientHasPendingReplies(c) && !(c->flags & CLIENT_PENDING_READ))
clientInstallWriteHandler(c);// 调用clientInstallWriteHandler

return C_OK;
}

clientInstallWriteHandler

clientInstallWriteHandler(networking.c)函数中对是否推迟客户端写操作进行了判断:

  1. 客户端标识不是CLIENT_PENDING_WRITE,对应条件为!(c->flags & CLIENT_PENDING_WRITE),表示客户端本身不是推迟写状态
  2. 客户端未在进行主从复制(对应条件为c->replstate == REPL_STATE_NONE) 或者 客户端是主从复制的从节点,但全量复制的 RDB 文件已经传输完成,客户端可以接收请求(对应条件 !c->repl_put_online_on_ack))

满足以上两个条件时将推迟客户端写操作,将客户端的标识置为延迟写CLIENT_PENDING_WRITE状态,并将客户端加入到待写回的列表server.clients_pending_write中。

1
2
3
4
5
6
7
8
9
10
11
12
13

void clientInstallWriteHandler(client *c) {
/* 如果客户端的标识不是推迟写状态,并且客户端未在进行主从复制或者客户端是主从复制的从节点并能接收请求 */
if (!(c->flags & CLIENT_PENDING_WRITE) &&
(c->replstate == REPL_STATE_NONE ||
(c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
{
/* 将客户端的标识置为延迟写 */
c->flags |= CLIENT_PENDING_WRITE;
// 将客户端加入到待写回的列表clients_pending_write中
listAddNodeHead(server.clients_pending_write,c);
}
}

IO线程的分配

上面我们已经知道了IO线程的初始化、IO线程的运行函数IOThreadMain主要处理逻辑,以及延迟读写的客户端是何时分别加入到server全局变量的clients_pending_read和clients_pending_write中的,接下来去看下时何时为客户端分配线程。

在aeProcessEvents处理事件的函数中,等待事件产生之前,调用了beforeSleep(networking.c)方法,beforeSleep中又调用了handleClientsWithPendingReadsUsingThreads为延迟读取操作的客户端分配线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void beforeSleep(struct aeEventLoop *eventLoop) {
UNUSED(eventLoop);

// 省略...

handleBlockedClientsTimeout();

/* 调用了handleClientsWithPendingReadsUsingThreads为延迟读客户端分配线程 */
handleClientsWithPendingReadsUsingThreads();

// 省略...

/* 调用了handleClientsWithPendingWritesUsingThreads为延迟写客户端分配线程 */
handleClientsWithPendingWritesUsingThreads();

// 省略...
}

延迟读操作的客户端分配线程

handleClientsWithPendingReadsUsingThreads

handleClientsWithPendingReadsUsingThreads(networking.c)主要逻辑如下:

  1. 从server.clients_pending_read获取延迟读取操作的客户端,将其加入到迭代列表

  2. 遍历延迟读操作的客户端列表,获取每一个待处理的客户端client,item_id表示每个客户端的序号,从0开始,每处理一个客户端就增1,用序号对线程数server.io_threads_num取模,得到一个target_id,客户端会被加入到io_threads_list[target_id]对应的列表中,也就是使用取模的方式轮询为每一个客户端分配对应线程,然后将客户端加入到该线程待处理的客户端列表中,此时客户端已分配到线程,在线程的运行函数IOThreadMain会调用readQueryFromClient处理客户端数据,需要注意多线程只是从客户端数据读取数据解析命令,并不会执行命令,在processInputBuffer中可以看到在IO多线程下只会将flags状态标记为CLIENT_PENDING_COMMAND,不会执行processCommandAndResetClient函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
     void processInputBuffer(client *c) {
    while(c->qb_pos < sdslen(c->querybuf)) {
    // 省略...
    if (c->argc == 0) {
    resetClient(c);
    } else {
    /* 在IO多线程情况下不能在这里执行命令,所以在这里将client标记为CLIENT_PENDING_COMMAND然后返回,等待主线程同步执行命令 */
    if (c->flags & CLIENT_PENDING_READ) {
    c->flags |= CLIENT_PENDING_COMMAND;
    break;
    }
    /* 准备执行命令 */
    if (processCommandAndResetClient(c) == C_ERR) {
    return;
    }
    }
    }
    // 省略...
    }
  3. 将io_threads_op线程操作状态置为读操作

  4. 遍历线程数,获取每一个线程要处理的客户端个数,将其设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程等待处理的客户端个数

  5. 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据,因为当前执行handleClientsWithPendingReadsUsingThreads函数的线程正是主线程,所以让主线程来处理io_threads_list[0]中存放的待处理客户端

  6. 主线程遍历io_threads_list[0]中每一个待处理的客户端,调用readQueryFromClient处理,从客户端读取数据

  7. 主线程开启一个while(1)循环等待其他IO线程处理完毕,结束条件是pending为0,pending记录了所有线程要处理的客户端数量总和,在前面IOThreadMain函数中可以看到线程在处理完毕之后会将对应io_threads_pending数组中记录的个数置为0,当pending为0表示所有的线程都已将各自复制的客户端数据处理完毕

  8. 主线程开启while循环准备执行客户端命令(注意这里才开始执行命令,多线程只负责解析不负责执行),循环条件是server.clients_pending_read列表的长度不为0,主线程需要保证客户端的请求顺序,所从clients_pending_read列表中的第一个元素开始向后遍历:

    (1)调用listNodeValue获取列表中的元素,也就是待处理的客户端client

    (2)调用listDelNode将获取到的元素从列表删除,因为在第7步中,主线程已经等待其他所有的线程执行完毕,此时所有的线程已经将各自负责的客户端数据处理完成,所以可以将客户端从server.clients_pending_read中移除

    (3)调用processPendingCommandsAndResetClient函数判断客户端标识是否是CLIENT_PENDING_COMMAND状态,CLIENT_PENDING_COMMAND状态表示客户端的请求命令已经被IO线程解析(processInputBuffer方法中可以看到状态被标记为CLIENT_PENDING_COMMAND),可以开始执行命令,接着调用processCommandAndResetClient函数执行客户端发送的请求命令

    (4)由于客户端输入缓冲区可能有其他的命令未读取,这里调用processInputBuffer处理输入缓冲区数据继续解析命令并执行

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
int handleClientsWithPendingReadsUsingThreads(void) {
if (!server.io_threads_active || !server.io_threads_do_reads) return 0;
int processed = listLength(server.clients_pending_read);
if (processed == 0) return 0;

listIter li;
listNode *ln;
// 获取待读取的客户端列表clients_pending_read加入到迭代链表中
listRewind(server.clients_pending_read,&li);
int item_id = 0;
// 遍历待读取的客户端
while((ln = listNext(&li))) {
// 获取客户端
client *c = listNodeValue(ln);
// 根据线程数取模,轮询分配线程
int target_id = item_id % server.io_threads_num;
// 分配线程,加入到线程对应的io_threads_list
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}

/* 将线程的操作状态置为读操作*/
io_threads_op = IO_THREADS_OP_READ;
// 遍历线程数
for (int j = 1; j < server.io_threads_num; j++) {
// 获取每个线程待处理客户端的个数
int count = listLength(io_threads_list[j]);
// 将待处理客户端的个数设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程要处理的客户端个数
setIOPendingCount(j, count);
}

/* 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据*/
/* handleClientsWithPendingReadsUsingThreads函数的执行者刚好就是主线程,所以让主线程处理io_threads_list[0]中的数据*/
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 调用readQueryFromClient
readQueryFromClient(c->conn);
}
listEmpty(io_threads_list[0]);

/* 等待其他线程处理完毕 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
// 获取每一个客户端处理的客户端个数
pending += getIOPendingCount(j);
// 如果为0表示所有线程对应的客户端都处理完毕
if (pending == 0) break;
}

/* 再次判断server.clients_pending_read是否有待处理的客户端*/
while(listLength(server.clients_pending_read)) {
// 获取列表第一个元素
ln = listFirst(server.clients_pending_read);
// 获取客户端
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_READ;
// 删除节点
listDelNode(server.clients_pending_read,ln);

serverAssert(!(c->flags & CLIENT_BLOCKED));
// processPendingCommandsAndResetClient函数中会判断客户端标识是否是CLIENT_PENDING_COMMAND状态,如果是调用processCommandAndResetClient函数处理请求命令
if (processPendingCommandsAndResetClient(c) == C_ERR) {
continue;
}
// 由于客户端输入缓冲区可能有其他的命令未读取,这里解析命令并执行
processInputBuffer(c);

if (!(c->flags & CLIENT_PENDING_WRITE) && clientHasPendingReplies(c))
clientInstallWriteHandler(c);
}

/* Update processed count on server */
server.stat_io_reads_processed += processed;

return processed;
}

processPendingCommandsAndResetClient

processPendingCommandsAndResetClient函数在networking.c中,它先判断客户端标识是否是CLIENT_PENDING_COMMAND状态,CLIENT_PENDING_COMMAND状态表示客户端的请求命令已经被IO线程解析,可以被执行,所以如果处于CLIENT_PENDING_COMMAND状态,接下来会调用processCommandAndResetClient函数处理客户端命令,具体是调用processCommand函数执行命令的:

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
/*processPendingCommandsAndResetClient函数(networking.c中) */
int processPendingCommandsAndResetClient(client *c) {
// 判断客户端标识是否是CLIENT_PENDING_COMMAND
if (c->flags & CLIENT_PENDING_COMMAND) {
// 取消CLIENT_PENDING_COMMAND状态
c->flags &= ~CLIENT_PENDING_COMMAND;
// 调用processCommandAndResetClient执行命令
if (processCommandAndResetClient(c) == C_ERR) {
return C_ERR;
}
}
return C_OK;
}

/* processCommandAndResetClient函数(networking.c中) */
int processCommandAndResetClient(client *c) {
int deadclient = 0;
client *old_client = server.current_client;
server.current_client = c;
// 调用processCommand执行命令
if (processCommand(c) == C_OK) {
commandProcessed(c);
}
if (server.current_client == NULL) deadclient = 1;
server.current_client = old_client;
return deadclient ? C_ERR : C_OK;
}

processCommand

processCommand函数在server.c文件中,它调用了addReply函数将需要返回给客户端的数据先写入缓冲区:

1
2
3
4
5
6
7
8
9
10
11
12
int processCommand(client *c) {
// 省略...

if (!strcasecmp(c->argv[0]->ptr,"quit")) {
// 调用addReply函数将需要返回给客户端的数据先写入缓冲区
addReply(c,shared.ok);
c->flags |= CLIENT_CLOSE_AFTER_REPLY;
return C_ERR;
}

// 省略...
}

数据读取的整体过程如下,IO多线程只是负责从客户端读取数据解析命令,执行命令的过程仍然是单线程的:

延迟写操作的客户端分配线程

handleClientsWithPendingWritesUsingThreads

延迟写操作的客户端分配线程在handleClientsWithPendingWritesUsingThreads中实现(networking.c),处理逻辑与handleClientsWithPendingReadsUsingThreads类似:

  1. 从server.clients_pending_write获取延迟写操作的客户端,将其加入到迭代列表

  2. 遍历延迟写操作的客户端列表,获取每一个待处理的客户端client,使用取模的方式轮询为每一个客户端分配线程,然后将客户端加入到该线程待处理的客户端列表中,此时客户端已分配到线程,在线程的运行函数IOThreadMain会处待写回数据的客户端

  3. 将io_threads_op线程操作状态置为写操作

  4. 遍历线程数,获取每一个线程要处理的客户端个数,将其设置到线程对应的io_threads_pending[j]中,io_threads_pending数组中记录了每个线程等待处理的客户端个数

  5. 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据,因为当前执行handleClientsWithPendingWritesUsingThreads函数的线程正是主线程,所以让主线程来处理io_threads_list[0]中存放的待处理客户端

  6. 主线程遍历io_threads_list[0]中每一个待处理的客户端,调用writeToClient往客户端写数据

  7. 主线程开启一个while(1)循环等待其他IO线程处理完毕

  8. 主线程开启while循环,循环条件是server.clients_pending_write列表的长度不为0,遍历clients_pending_write中待处理的写客户端:

    (1)调用listNodeValue获取待处理的客户端client

    (2)判断缓冲区数据是否全部写回到客户端,如果未全部写回调用connSetWriteHandler向内核注册写事件监听,回调函数为sendReplyToClient,待事件循环流程再次执行时,注册的可写事件会通过回调函数sendReplyToClient 处理,把缓冲区中的数据写回客户端。

  9. 调用listEmpty函数清空server.clients_pending_write列表

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
int handleClientsWithPendingWritesUsingThreads(void) {
int processed = listLength(server.clients_pending_write);
if (processed == 0) return 0;

if (server.io_threads_num == 1 || stopThreadedIOIfNeeded()) {
return handleClientsWithPendingWrites();
}
if (!server.io_threads_active) startThreadedIO();

listIter li;
listNode *ln;
// 获取待写回客户端列表clients_pending_write加入到迭代链表中
listRewind(server.clients_pending_write,&li);
int item_id = 0;
// 遍历待写的客户端
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
if (c->flags & CLIENT_CLOSE_ASAP) {
listDelNode(server.clients_pending_write, ln);
continue;
}
// 根据线程数取模,轮询分配线程
int target_id = item_id % server.io_threads_num;
// 分配线程,加入到对应线程的io_threads_list
listAddNodeTail(io_threads_list[target_id],c);
item_id++;
}

/* 将io_threads_op线程操作状态置为写操作 */
io_threads_op = IO_THREADS_OP_WRITE;
for (int j = 1; j < server.io_threads_num; j++) {
int count = listLength(io_threads_list[j]);
// 设置每个线程需要处理的客户端个数
setIOPendingCount(j, count);
}


/* 获取io_threads_list[0]中待处理的客户端列表,io_threads_list[0]存储的是主线程的数据*/
/* handleClientsWithPendingWritesUsingThreads函数的执行者刚好就是主线程,所以让主线程处理io_threads_list[0]中的数据*/
listRewind(io_threads_list[0],&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
// 调用writeToClient往客户写数据
writeToClient(c,0);
}
listEmpty(io_threads_list[0]);

/* 等待其他线程处理完毕 */
while(1) {
unsigned long pending = 0;
for (int j = 1; j < server.io_threads_num; j++)
pending += getIOPendingCount(j);
if (pending == 0) break;
}

/* 再次获取server.clients_pending_read所有待写的客户端*/
listRewind(server.clients_pending_write,&li);
// 遍历
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);

/* 如果缓冲区数据未全部写回调用connSetWriteHandler注册可写事件,回调函数为sendReplyToClient*/
if (clientHasPendingReplies(c) &&
connSetWriteHandler(c->conn, sendReplyToClient) == AE_ERR)
{
freeClientAsync(c);
}
}
// 清空clients_pending_write
listEmpty(server.clients_pending_write);

server.stat_io_writes_processed += processed;

return processed;
}

connSetWriteHandler

connSetWriteHandler函数在connection.c文件中,它通过set_write_handler注册了写handler,set_write_handler对应的是connSocketSetWriteHandler函数,所以connSetWriteHandler会被映射为connSocketSetWriteHandler,connSocketSetWriteHandler函数调用了aeCreateFileEvent向内核中注册可写事件监听,上面可知回调函数为sendReplyToClient ,等事件循环流程再次执行时,handleClientsWithPendingWritesUsingThreads 函数注册的可写事件会通过回调函数sendReplyToClient 处理,把缓冲区中的数据写回客户端。

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
38
39
40
41
42
43
ConnectionType CT_Socket = {
.ae_handler = connSocketEventHandler,
.close = connSocketClose,
.write = connSocketWrite,
.read = connSocketRead,
.accept = connSocketAccept,
.connect = connSocketConnect,
.set_write_handler = connSocketSetWriteHandler, // set_write_handler对应connSocketSetWriteHandler函数
.set_read_handler = connSocketSetReadHandler,
.get_last_error = connSocketGetLastError,
.blocking_connect = connSocketBlockingConnect,
.sync_write = connSocketSyncWrite,
.sync_read = connSocketSyncRead,
.sync_readline = connSocketSyncReadLine,
.get_type = connSocketGetType
};

/*
* connSetWriteHandler
*/
static inline int connSetWriteHandler(connection *conn, ConnectionCallbackFunc func) {
// 注册写handler, set_write_handler对应的是connSocketSetWriteHandler函数
return conn->type->set_write_handler(conn, func, 0);
}

/*
* connSocketSetWriteHandler注册写事件
*/
static int connSocketSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) {
if (func == conn->write_handler) return C_OK;

conn->write_handler = func;
if (barrier)
conn->flags |= CONN_FLAG_WRITE_BARRIER;
else
conn->flags &= ~CONN_FLAG_WRITE_BARRIER;
if (!conn->write_handler)
aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE);
else
if (aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE, // 向内核注册写事件
conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
return C_OK;
}

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

Redis版本:redis-6.2.5

【Redis】事件驱动源码分析

Posted on 2022-04-25

aeEventLoop初始化

在server.c文件的initServer函数中,对aeEventLoop进行了初始化:

  1. 调用aeCreateEventLoop函数创建aeEventLoop结构体,对aeEventLoop结构体中的变量进行了初始化,之后调用了aeApiCreate函数创建epoll实例
  2. 调用aeCreateFileEvent函数向内核注册监听事件,由参数可知,注册的是对TCP文件描述符的可读事件监听,回调函数是acceptTcpHandler,当内核监听到TCP文件描述符有可读事件时,Redis将调用acceptTcpHandler函数对事件进行处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void initServer(void) {
// 创建aeEventLoop结构体
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
if (server.el == NULL) {
serverLog(LL_WARNING,
"Failed creating the event loop. Error message: '%s'",
strerror(errno));
exit(1);
}
// 省略其他代码...
for (j = 0; j < server.ipfd_count; j++) {
// 注册监听事件,server.ipfd是TCP文件描述符,AE_READABLE可读事件,acceptTcpHandler事件处理回调函数
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
serverPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
// 省略其他代码...
}

在aeCreateEventLoop函数调用时,传入的最大文件描述符个数为客户端最大连接数+宏定义CONFIG_FDSET_INCR的大小,CONFIG_FDSET_INCR的定义在server.h中:

1
2
#define CONFIG_FDSET_INCR (CONFIG_MIN_RESERVED_FDS+96)
#define CONFIG_MIN_RESERVED_FDS 32

aeEventLoop结构体创建

aeEventLoop结构体定义,在ae.h中:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct aeEventLoop {
int maxfd; /* 记录最大的文件描述符 */
int setsize; /* 最大文件描述符个数 */
long long timeEventNextId;
time_t lastTime;
aeFileEvent *events; /* IO事件集合,记录了每个文件描述符产生事件时的回调函数 */
aeFiredEvent *fired; /* 记录已触发的事件 */
aeTimeEvent *timeEventHead; /* 时间事件 */
int stop;
void *apidata; /* IO多路复用API接口相关数据 */
aeBeforeSleepProc *beforesleep;/* 进入事件循环流程前的执行函数 */
aeBeforeSleepProc *aftersleep;/* 退出事件循环流程后的执行函数 */
} aeEventLoop;

aeCreateEventLoop

aeEventLoop结构体创建在aeCreateEventLoop函数中(ae.c文件):

  1. 分配aeEventLoop结构体所需内存
  2. 分配aeEventLoop结构体中其他变量所需内存
  3. 调用aeApiCreate函数创建epoll实例
  4. 对IO事件集合events的mask掩码初始化为AE_NONE,表示当前没有事件监听
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
aeEventLoop *aeCreateEventLoop(int setsize) {
// aeEventLoop结构体
aeEventLoop *eventLoop;
int i;
// 分配eventLoop内存
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;
// 分配IO事件内存
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
eventLoop->setsize = setsize;
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
// 创建poll实例
if (aeApiCreate(eventLoop) == -1) goto err;
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE; // 初始化为空事件
return eventLoop;

err:
if (eventLoop) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
}
return NULL;
}

创建epoll实例

aeApiState结构体定义,在ae_epoll.c中:

  • epfd:创建的epoll实例文件描述符

  • events:记录文件描述符产生的事件

1
2
3
4
typedef struct aeApiState {
int epfd; // epoll实例文件描述符
struct epoll_event *events; // 记录就绪的事件
} aeApiState;

aeApiCreate

epoll实例的的创建在aeApiCreate函数(ae_epoll.c文件)中,处理逻辑如下:

  1. 为aeApiState结构体分配内存空间

  2. 为aeApiState中的events分配内存空间,events数组个数为eventLoop中的最大文件描述个数

  3. 调用epoll_create函数创建epoll实例,将返回的epoll文件描述符保存在epfd中

  4. 将eventLoop的apidata指向创建的aeApiState,之后就可以通过eventLoop获取到epoll实例并且注册监听事件了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static int aeApiCreate(aeEventLoop *eventLoop) {
// 分配内存
aeApiState *state = zmalloc(sizeof(aeApiState));

if (!state) return -1;
// 为epoll事件分配内存
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// epoll_create创建epoll实例,返回文件描述符,保存在state的epfd中
state->epfd = epoll_create(1024);
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
// 将aeApiState设置到eventLoop的apidata
eventLoop->apidata = state;
return 0;
}

注册事件

IO 事件的数据结构是 aeFileEvent 结构体,在ae.c中定义:

  • mask:事件类型掩码,共有READABLE、WRITABLE、BARRIER三种事件,分别为可读事件、可写事件和屏障事件

  • rfileProc:写事件回调函数

  • wfileProc:读事件回调函数

1
2
3
4
5
6
typedef struct aeFileEvent {
int mask; /* 事件类型掩码 READABLE|WRITABLE|BARRIER */
aeFileProc *rfileProc; /* 写事件回调函数 */
aeFileProc *wfileProc; /* 读事件回调函数 */
void *clientData; /* 客户端数据 */
} aeFileEvent;

aeCreateFileEvent

aeCreateFileEvent函数在ae.c文件中,主要处理逻辑如下:

  1. 根据传入的文件描述符,在eventLoop中获取对应的IO事件aeFileEvent fe
  2. 调用aeApiAddEvent方法注册要监听的事件
  3. 设置读写事件的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {
errno = ERANGE;
return AE_ERR;
}
// 根据传入的文件描述符获取对应的IO事件
aeFileEvent *fe = &eventLoop->events[fd];
// 注册要监听的事件,让内核可以监听到当前文件描述符上的IO事件
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc; // 设置写事件的回调函数
if (mask & AE_WRITABLE) fe->wfileProc = proc; // 设置读事件的回调函数
fe->clientData = clientData;
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
return AE_OK;
}

aeApiAddEvent

aeApiAddEvent用于注册事件(ae_epoll.c文件中):

  1. 从eventLoop获取aeApiState,因为aeApiState中的epfd记录了epoll实例
  2. 创建了epoll_event类型的变量ee,用于记录操作类型、要监听的文件描述符以及事件类型,在调用函数时使用
  3. 根据掩码mask判断操作类型,如果文件描述符还未设置监听事件mask掩码为AE_NONE, 类型设置为添加,否则设置为修改,操作类型有如下三种:
    • EPOLL_CTL_ADD:用于向epoll添加监听事件
    • EPOLL_CTL_MOD:用于修改已经注册过的监听事件
    • EPOLL_CTL_ADD:用于删除监听事件
  4. 将redis的可读、可写事件类型转换为epoll的类型,读事件类型为EPOLLIN,写事件为EPOLLOUT,并设置到ee的events中
  5. 调用epoll_ctl函数添加文件描述符的监听事件,参数分别为epoll实例、操作类型、要监听的文件描述符、epoll_event类型变量ee
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
// 获取aeApiState
aeApiState *state = eventLoop->apidata;
// 创建epoll_event类型的变量ee,添加监听事件的时候使用
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* 如果fd文件描述符还未设置监听事件, 类型设置为添加,否则设置为修改,简言之就是根据掩码判断是添加还是修改监听事件 */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;

mask |= eventLoop->events[fd].mask;
// 如果是可读事件,转换为epoll的读事件监听类型EPOLLIN,并设置到ee的events中
if (mask & AE_READABLE) ee.events |= EPOLLIN;
// 如果是可写事件,转换为epoll的写事件监听类型EPOLLOUT,并设置到ee的events中
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
// 记录要监听的文件描述符
ee.data.fd = fd;
// 调用epoll_ctl函数向epoll添加监听事件,参数分别为epoll实例、操作类型、要监听的文件描述符、epoll_event类型变量ee
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}

总结

Redis在启动时,调用aeCreateEventLoop创建aeEventLoop结构体和epoll实例,之后调用aeCreateFileEvent函数向内核注册TCP文件描述符的监听事件,当有客户端连接Redis服务时,TCP文件描述符产生可读事件,通过epoll可以获取产生事件的文件描述符,Redis就可以对连接请求进行处理。

1
2
3
4
5
// server.el是eventLoop
// server.ipfd[j]是监听端口的文件描述符
// AE_READABLE是读事件
// acceptTcpHandler是事件产生时的回调函数
aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler, NULL)

事件处理

aeMain函数在ae.c文件中,里面是一个while循环,它的处理逻辑如下:

  1. 通过eventLoop的stop判断是否处于停止状态,如果非停止状态进入第2步
  2. 判断eventLoop的beforesleep是否为空,如果不为空,调用beforesleep函数
  3. 调用了aeProcessEvents函数处理IO事件
1
2
3
4
5
6
7
8
9
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 调用了aeProcessEvents处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}

aeProcessEvents

aeProcessEvents函数在ae.c文件中,处理逻辑如下:

  1. 调用aeApiPoll函数等待就绪的事件,如果有事件产生,返回就绪的文件描述符个数,aeApiPoll函数中对就绪文件描述符处理时将其放在了fired中
  2. for循环中处理就绪的事件,通过fired可以获取到每一个产生事件的文件描述符fd,根据文件描述符fd可以在eventLoop的events中获取对应的事件aeFileEvent,aeFileEvent中记录了事件的回调函数,之后根据事件类型,调用对应的回调函数,调用回调函数的入参分别为eventLoop、文件描述符、aeFileEvent的clientData、事件类型掩码
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;

/* 如果没有事件 */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

/* 如果有IO事件或者时间事件 */
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;

// 省略代码...

// 等待事件,返回就绪文件描述符的数量
numevents = aeApiPoll(eventLoop, tvp);

/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);

// 处理就绪的事件
for (j = 0; j < numevents; j++) {
// aeApiPoll中已将就绪的事件放在了fired中,通过fired可以获取到产生事件的文件描述符fd
// 根据文件描述符fd获取对应的事件aeFileEvent,aeFileEvent中记录了事件的回调函数
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
// 获取文件描述符
int fd = eventLoop->fired[j].fd;
int fired = 0; /* Number of events fired for current fd. */

/* 判断屏障 */
int invert = fe->mask & AE_BARRIER;

/* 处理可读事件 */
if (!invert && fe->mask & mask & AE_READABLE) {
// 如果是可读事件,调用可读事件的回调函数,参数分别为eventLoop、文件描述符、aeFileEvent的clientData、事件类型掩码
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}

/* 处理可写事件 */
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
// 如果是写事件,调用写事件的回调函数,参数分别为eventLoop、文件描述符、aeFileEvent的clientData、事件类型掩码
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}

/* If we have to invert the call, fire the readable event now
* after the writable one. */
if (invert && fe->mask & mask & AE_READABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}

processed++;
}
}
/* 如果有时间事件 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);

return processed; /* return the number of processed file/time events */
}

aeApiPoll

aeApiPoll处理就绪的事件:

  1. 调用IO多路复用epoll_wait函数等待事件的产生,epoll_wait函数需要传入epoll实例、记录就绪事件集合的epoll_event,这两个参数分别在aeApiState的epfd和events中,当监听的文件描述符有事件产生时,epoll_wait返回就绪的文件描述符个数

  2. 对epoll_wait返回的就绪事件进行处理,事件记录在events变量中,遍历每一个就绪的事件,将事件对应的文件描述符设置在eventLoop的fire中,后续通过fire对事件进行处理

    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
    static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    // 获取aeApiState,aeApiState记录了epoll实例,events记录了产生的事件
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
    // 等待事件的产生,epoll_wait返回就绪的文件描述符个数,就绪的事件记录在state->events中
    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
    tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
    if (retval > 0) {
    int j;

    numevents = retval;
    // 处理返回的就绪事件
    for (j = 0; j < numevents; j++) {
    int mask = 0;
    // 获取每一个就绪的事件
    struct epoll_event *e = state->events+j;

    if (e->events & EPOLLIN) mask |= AE_READABLE;
    if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
    if (e->events & EPOLLERR) mask |= AE_WRITABLE;
    if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
    // 将就绪事件的文件描述符设置到已触发的事件fired的fd中
    eventLoop->fired[j].fd = e->data.fd;
    // 设置事件类型掩码
    eventLoop->fired[j].mask = mask;
    }
    }
    return numevents;
    }

处理客户端连接

acceptTcpHandler

由上面的调用可知,Redis在启动时,注册了AE_READABLE读事件,回调函数为acceptTcpHandler(network.c文件中)用于处理客户端连接,当有客户端与Redis连接时,epoll返回就绪的文件描述符,Redis在处理就绪的事件时调用acceptTcpHandler进行处理:

  1. 调用anetTcpAccept建立连接,并返回已连接的套接字文件描述符cfd
  2. 调用acceptCommonHandler(network.c文件中)函数,它又调用了createClient函数,在createClient函数中调用了aeCreateFileEvent,向内核注册已连接套接字的可读监听事件
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
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[NET_IP_STR_LEN];
UNUSED(el);
UNUSED(mask);
UNUSED(privdata);
while(max--) {
// 建立连接,返回已连接的套接字文件描述符cfd
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
serverLog(LL_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
// 调用acceptCommonHandler处理连接,这里传入的文件描述符为已连接的套接字
acceptCommonHandler(cfd,0,cip);
}
}

static void acceptCommonHandler(int fd, int flags, char *ip) {
client *c;
// 调用createClient
if ((c = createClient(fd)) == NULL) {
serverLog(LL_WARNING,
"Error registering fd event for the new client: %s (fd=%d)",
strerror(errno),fd);
close(fd); /* May be already closed, just ignore errors */
return;
}
// ..
}

createClient

createClient函数中调用了aeCreateFileEvent方法向内核中注册可读事件,上文可知传入的描述符是已连接套接字cfd,回调函数为readQueryFromClient,此时事件驱动框架增加了对客户端已连接套接字的监听,当客户端有数据发送到服务端时,Redis调用readQueryFromClient函数处理读事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
if (fd != -1) {
anetNonBlock(NULL,fd);
anetEnableTcpNoDelay(NULL,fd);
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
// 注册已连接套接字的可读事件,回调函数为readQueryFromClient
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
}
// ...
}

处理读事件

readQueryFromClient

readQueryFromClient函数在network.c文件中,是可读事件的回调函数,用于处理已连接套接字上的读事件,处理逻辑如下:

  1. 从已连接的套接字中读取客户端的请求数据到输入缓冲区
  2. 调用processInputBufferAndReplicate函数处理输入缓冲区的数据
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
38
39
40
41
42
// aeProcessEvents中调用回调函数时,传入的参数分别为aeEventLoop、已连接套接字的文件描述符、aeFileEvent的clientData私有数据、事件类型掩码
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
int nread, readlen;
size_t qblen;
UNUSED(el);
UNUSED(mask);

readlen = PROTO_IOBUF_LEN;
if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
&& c->bulklen >= PROTO_MBULK_BIG_ARG)
{
ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf);
if (remaining > 0 && remaining < readlen) readlen = remaining;
}

qblen = sdslen(c->querybuf);
if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
// 从已连接的套接字中读取客户端的请求数据到输入缓冲区
nread = read(fd, c->querybuf+qblen, readlen);
if (nread == -1) {
if (errno == EAGAIN) {
return;
} else {
serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));
freeClient(c);
return;
}
} else if (nread == 0) {
serverLog(LL_VERBOSE, "Client closed connection");
freeClient(c);
return;
} else if (c->flags & CLIENT_MASTER) {
c->pending_querybuf = sdscatlen(c->pending_querybuf,
c->querybuf+qblen,nread);
}
// 省略...

/* 处理输入缓冲区数据 */
processInputBufferAndReplicate(c);
}

处理写事件

在aeMain调用aeProcessEvents之前,先调用了beforeSleep方法,beforeSleep中又调用了handleClientsWithPendingWrites,它会将Redis Server缓冲区的数据写回到客户端:

1
2
3
4
5
6
7
8
9
void beforeSleep(struct aeEventLoop *eventLoop) {

// 省略...

/* Handle writes with pending output buffers. */
handleClientsWithPendingWrites();

// 省略...
}.

handleClientsWithPendingWrites

Redis Server收到客户端的请求命令后,需要处理请求,然后将要返回的数据写回到客户端,写回到客户端的逻辑在handleClientsWithPendingWrites函数中,处理逻辑如下:

  1. 获取待写回数据的客户端列表
  2. 遍历每一个待写回数据的客户端,调用writeToClient方法将缓冲区的数据写到客户端socket中,然后调用clientHasPendingReplies方法判断数据是否全部写回,如果为否,则调用aeCreateFileEvent向内核注册客户端文件描述符的可写事件监听,交由回调函数sendReplyToClient处理
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
int handleClientsWithPendingWrites(void) {
listIter li;
listNode *ln;
int processed = listLength(server.clients_pending_write);
// 获取待写回数据的客户端列表
listRewind(server.clients_pending_write,&li);
// 遍历每一个待写回数据的客户端
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
listDelNode(server.clients_pending_write,ln);
if (c->flags & CLIENT_PROTECTED) continue;

/* 将缓冲区的数据写到客户端socket中 */
if (writeToClient(c->fd,c,0) == C_ERR) continue;

/* 如果数据未全部写回到客户端 */
if (clientHasPendingReplies(c)) {
int ae_flags = AE_WRITABLE;
if (server.aof_state == AOF_ON &&
server.aof_fsync == AOF_FSYNC_ALWAYS)
{
ae_flags |= AE_BARRIER;
}
// 调用aeCreateFileEvent方法,向内核注册客户端文件描述符的可写事件监听,交由回调函数sendReplyToClient处理
if (aeCreateFileEvent(server.el, c->fd, ae_flags,
sendReplyToClient, c) == AE_ERR)
{
freeClientAsync(c);
}
}
}
return processed;
}

clientHasPendingReplies

有时由于网络原因或者其他原因,可能只发出去了部分数据,客户端如果一直未从缓冲区读取数据,在缓冲区已满的情况,服务端将无法往客户端发送数据,所以调用clientHasPendingReplies函数判断数据是否写回完毕,如果未写回完毕交由事件循环驱动处理,提高处理效率。

整体流程图

总结

参考

极客时间 - Redis源码剖析与实战(蒋德钧)

【osc_avxkth26】Redis 网络通信模块源码分析(3)

网络通信 –> epoll用法

Redis版本:redis-5.0.8

【Spring】AOP实现原理(三):创建代理(基于注解)

Posted on 2022-04-19

AbstractAutoProxyCreator

在AbstractAutoProxyCreator的wrapIfNecessary方法中,调用getAdvicesAndAdvisorsForBean方法获取到所有的Advisor之后,就可以创建代理对象了,创建的具体过程在createProxy方法中:

  1. 创建代理工厂ProxyFactory
  2. 调用buildAdvisors构建Advisor,入参是getAdvicesAndAdvisorsForBean获取到的Advice和Advisor,里面又调用了AdvisorAdapterRegistry的wrap方法判断Advice是否是Advisor类型,这一步主要是对通知Advice进行校验,如果通知不是Advisor类型将其包装为Advisor
  3. 通过代理工厂ProxyFactory生成代理对象
    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
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
    implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
    /**
    * 是否有必要生成代理对象
    * @param bean
    * @param beanName
    * @param cacheKey
    * @return
    */
    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
    return bean;
    }
    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
    return bean;
    }
    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
    }

    // 获取Advices和Advisors
    Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
    if (specificInterceptors != DO_NOT_PROXY) {
    this.advisedBeans.put(cacheKey, Boolean.TRUE);
    // 创建代理,specificInterceptors就是获取到的Advices和Advisors
    Object proxy = createProxy(
    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
    this.proxyTypes.put(cacheKey, proxy.getClass());
    return proxy;
    }

    this.advisedBeans.put(cacheKey, Boolean.FALSE);
    return bean;
    }

    /**
    * 创建代理对象
    */
    protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
    @Nullable Object[] specificInterceptors, TargetSource targetSource) {

    if (this.beanFactory instanceof ConfigurableListableBeanFactory) {
    AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass);
    }
    // 1.创建代理工厂
    ProxyFactory proxyFactory = new ProxyFactory();
    proxyFactory.copyFrom(this);

    if (!proxyFactory.isProxyTargetClass()) {
    if (shouldProxyTargetClass(beanClass, beanName)) {
    proxyFactory.setProxyTargetClass(true);
    }
    else {
    evaluateProxyInterfaces(beanClass, proxyFactory);
    }
    }
    // 2.构建Advisor
    Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
    // 设置Advisor
    proxyFactory.addAdvisors(advisors);
    proxyFactory.setTargetSource(targetSource);
    customizeProxyFactory(proxyFactory);

    proxyFactory.setFrozen(this.freezeProxy);
    if (advisorsPreFiltered()) {
    proxyFactory.setPreFiltered(true);
    }
    // 3.生成代理对象
    return proxyFactory.getProxy(getProxyClassLoader());
    }
    }

构建Advisor

AbstractAutoProxyCreator的buildAdvisors主要是对通知Advice进行校验,如果通知不是Advisor类型将其包装为Advisor,具体是通过AdvisorAdapterRegistry的wrap方法实现的:

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
38
39
40
41
42
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

// 调用GlobalAdvisorAdapterRegistry的getInstance获取AdvisorAdapterRegistry实例
private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance();

/**
* 构建Advisors,specificInterceptors是上一步获取到的Advice和Advisor
*/
protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) {
// Handle prototypes correctly...
Advisor[] commonInterceptors = resolveInterceptorNames();

List<Object> allInterceptors = new ArrayList<>();
if (specificInterceptors != null) {
// 将获取到的Advice和Advisor添加到拦截器中
allInterceptors.addAll(Arrays.asList(specificInterceptors));
if (commonInterceptors.length > 0) {
if (this.applyCommonInterceptorsFirst) {
allInterceptors.addAll(0, Arrays.asList(commonInterceptors));
}
else {
allInterceptors.addAll(Arrays.asList(commonInterceptors));
}
}
}
if (logger.isTraceEnabled()) {
int nrOfCommonInterceptors = commonInterceptors.length;
int nrOfSpecificInterceptors = (specificInterceptors != null ? specificInterceptors.length : 0);
logger.trace("Creating implicit proxy for bean '" + beanName + "' with " + nrOfCommonInterceptors +
" common interceptors and " + nrOfSpecificInterceptors + " specific interceptors");
}
// 创建Advisor集合
Advisor[] advisors = new Advisor[allInterceptors.size()];
for (int i = 0; i < allInterceptors.size(); i++) {
// 调用AdvisorAdapterRegistry的wrap方法将通知包装为Advisor进行增强,在DefaultAdvisorAdapterRegistry中实现
advisors[i] = this.advisorAdapterRegistry.wrap(allInterceptors.get(i));
}
return advisors;
}

}

AdvisorAdapterRegistry

在GlobalAdvisorAdapterRegistry中可以看到使用的是DefaultAdvisorAdapterRegistry:

1
2
3
4
5
6
7
8
9
10
11
12
13
public final class GlobalAdvisorAdapterRegistry {
/**
* 使用DefaultAdvisorAdapterRegistry
*/
private static AdvisorAdapterRegistry instance = new DefaultAdvisorAdapterRegistry();

/**
* 返回AdvisorAdapterRegistry
*/
public static AdvisorAdapterRegistry getInstance() {
return instance;
}
}

DefaultAdvisorAdapterRegistry

DefaultAdvisorAdapterRegistry中的wrap方法主要逻辑如下:

  1. 如果当前的通知已经是Advisor类型直接返回即可
  2. 如果当前的通知不是Advice类型,抛出异常
  3. 将通知转为Advice,对通知进行判断
    • 如果通知是MethodInterceptor方法拦截器,将其包装为DefaultPointcutAdvisor
    • 如果不是MethodInterceptor,遍历Adapter适配器,找出支持当前通知的适配器,再将通知包装为DefaultPointcutAdvisor返回
  4. 非以上几种情况抛出异常
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
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {
@Override
public Advisor wrap(Object adviceObject) throws UnknownAdviceTypeException {
// 如果已经是Advisor,返回即可
if (adviceObject instanceof Advisor) {
return (Advisor) adviceObject;
}
// 如果不是Advice类型,抛出异常
if (!(adviceObject instanceof Advice)) {
throw new UnknownAdviceTypeException(adviceObject);
}
// 转为Advice
Advice advice = (Advice) adviceObject;
// 如果是一个方法拦截器
if (advice instanceof MethodInterceptor) {
// 包装为DefaultPointcutAdvisor
return new DefaultPointcutAdvisor(advice);
}
for (AdvisorAdapter adapter : this.adapters) {
// 判断Advisor适配器是否支持当前的Advice
if (adapter.supportsAdvice(advice)) {
// 包装为DefaultPointcutAdvisor
return new DefaultPointcutAdvisor(advice);
}
}
// 抛出异常
throw new UnknownAdviceTypeException(advice);
}
}

生成代理对象

上一步中已经对所有的Advice进行了校验,转为了Advisor进行增强,接下来就可以生成代理对象了,具体实现在ProxyFactory的getProxy中:

  1. 调用createAopProxy获取AopProxy,AopProxy是一个接口,定义了getProxy获取代理对象的方法,它有两个实现类分别为CglibAopProxy和JdkDynamicAopProxy

  2. 调用AopProxy的getProxy方法获取代理对象

1
2
3
4
5
6
7
8
9
10
public class ProxyFactory extends ProxyCreatorSupport {
/**
* 获取代理对象
*/
public Object getProxy(@Nullable ClassLoader classLoader) {
// 通过工厂创建AOP代理对象,createAopProxy方法在ProxyCreatorSupport中实现
return createAopProxy().getProxy(classLoader);
}

}

createAopProxy创建AopProxy

createAopProxy的实现逻辑在ProxyFactory的父类ProxyCreatorSupport中实现,它使用了工厂模式生成代理对象:

  1. 调用getAopProxyFactory方法获取AopProxyFactory,在无参构造函数中可以看的默认使用的工厂是DefaultAopProxyFactory
  2. 调用AopProxyFactory的createAopProxy方法创建代理对象
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
38
39
40
// ProxyCreatorSupport
public class ProxyCreatorSupport extends AdvisedSupport {

// AOP代理工厂
private AopProxyFactory aopProxyFactory;

/**
* 无参构造函数
*/
public ProxyCreatorSupport() {
// 工厂的实现默认使用DefaultAopProxyFactory
this.aopProxyFactory = new DefaultAopProxyFactory();
}

/**
* 带有AopProxyFactory参数的构造函数
*/
public ProxyCreatorSupport(AopProxyFactory aopProxyFactory) {
Assert.notNull(aopProxyFactory, "AopProxyFactory must not be null");
this.aopProxyFactory = aopProxyFactory;
}

/**
* 创建AopProxy
*/
protected final synchronized AopProxy createAopProxy() {
if (!this.active) {
activate();
}
// 1.调用getAopProxyFactory获取AopProxy,2.调用AopProxy的createAopProxy创建代理对象
return getAopProxyFactory().createAopProxy(this);
}

/**
* 返回AOP代理工厂
*/
public AopProxyFactory getAopProxyFactory() {
return this.aopProxyFactory;
}
}

DefaultAopProxyFactory

DefaultAopProxyFactory中创建AOP代理的逻辑如下:

  1. 获取目标对象的Class信息
  2. 对Class进行判断:
    • 如果是一个接口或者isProxyClass返回true使用JDK动态代理生成代理对象,isProxyClass方法在JDK的Proxy中实现,返回true的条件为目标类是java.lang.reflect.Proxy的子类并且缓存中包含目标类
    • 如果上一个条件不满足则使用CGLIB生成代理对象
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
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {

@Override
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
// 获取目标对象的Class信息
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: " +
"Either an interface or a target is required for proxy creation.");
}
// 如果是一个接口
if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
// 否则使用CGLIB生成代理对象
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}
}

// java.lang.reflect.Proxy
public class Proxy implements java.io.Serializable {

public static boolean isProxyClass(Class<?> cl) {
// 是否是java.lang.reflect.Proxy的子类并且缓存中包含目标类
return Proxy.class.isAssignableFrom(cl) && proxyClassCache.containsValue(cl);
}
}

到此AopProxy已经创建成功,接下来以JdkDynamicAopProxy为例查看getProxy获取代理对象过程。

getProxy获取代理对象

JdkDynamicAopProxy是通过JDK的动态代理实现代理创建的,可以看到它实现了InvocationHandler接口,关于JDK的动态代理实现原理可参考【JAVA】动态代理,这里我们需要关注getProxy和invoke方法:

  • getProxy:创建代理对象
  • invoke:当代理对象中引用的方法执行时会进入这个方法中
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
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

@Override
public Object getProxy() {
return getProxy(ClassUtils.getDefaultClassLoader());
}

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 生成代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}

@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 先省略

}
}

getProxy创建代理对象

可以看到是通过JDK中Proxy的newProxyInstance生成代理对象的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14

final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

@Override
public Object getProxy(@Nullable ClassLoader classLoader) {
if (logger.isTraceEnabled()) {
logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource());
}
Class<?>[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true);
findDefinedEqualsAndHashCodeMethods(proxiedInterfaces);
// 生成代理对象
return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this);
}
}

invoke方法

当调用AOP中需要被通知拦截的方法时,会进入到invoke方法,比较核心的是getInterceptorsAndDynamicInterceptionAdvice获取拦截器链,如果为空直接通过反射执行目标方法即可,如果不为空,将方法包装为MethodInvocation,然后执行拦截器链:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable {

/** AdvisedSupport */
private final AdvisedSupport advised;

/**
* InvocationHandler中定义的invoke方法,当目标方法执行时会进入到这个方法中
* @param proxy 代理对象
* @param method 需要被执行的目标方法
* @param args
* @return
* @throws Throwable
*/
@Override
@Nullable
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object oldProxy = null;
boolean setProxyContext = false;

TargetSource targetSource = this.advised.targetSource;
Object target = null;

try {
// 如果是equals方法
if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) {
// The target does not implement the equals(Object) method itself.
return equals(args[0]);
}
else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) {
// 如果是hashCode方法
return hashCode();
}
else if (method.getDeclaringClass() == DecoratingProxy.class) {
// 如果方法所在类是DecoratingProxy类
return AopProxyUtils.ultimateTargetClass(this.advised);
}
else if (!this.advised.opaque && method.getDeclaringClass().isInterface() &&
method.getDeclaringClass().isAssignableFrom(Advised.class)) {
return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args);
}

Object retVal;
// 是否需要暴露代理
if (this.advised.exposeProxy) {
oldProxy = AopContext.setCurrentProxy(proxy);
setProxyContext = true;
}
target = targetSource.getTarget();
Class<?> targetClass = (target != null ? target.getClass() : null);

// 获取当前方法的拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// 校验拦截器链是否为空
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
// 如果为空直接执行目标方法即可
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
}
else {
// 创建MethodInvocation
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 执行拦截器链
retVal = invocation.proceed();
}

// 获取方法返回值
Class<?> returnType = method.getReturnType();
if (retVal != null && retVal == target &&
returnType != Object.class && returnType.isInstance(proxy) &&
!RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) {
retVal = proxy;
}
else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) {
throw new AopInvocationException(
"Null return value from advice does not match primitive return type for: " + method);
}
return retVal;
}
finally {
if (target != null && !targetSource.isStatic()) {
// Must have come from TargetSource.
targetSource.releaseTarget(target);
}
if (setProxyContext) {
// Restore old proxy.
AopContext.setCurrentProxy(oldProxy);
}
}
}
}

获取方法的拦截器链

获取目标方法的拦截器链在AdvisedSupport中实现,它主要用于将适用于当前方法的Advisor转为方法拦截器,首先它先从缓存中查询,如果未查询到,调用advisorChainFactory方法的getInterceptorsAndDynamicInterceptionAdvice进行获取,在DefaultAdvisorChainFactory中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class AdvisedSupport extends ProxyConfig implements Advised {
/** AdvisorChainFactory,默认使用DefaultAdvisorChainFactory*/
AdvisorChainFactory advisorChainFactory = new DefaultAdvisorChainFactory();
/**
* 获取方法的拦截器链
*/
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, @Nullable Class<?> targetClass) {
// 构建缓存KEY
MethodCacheKey cacheKey = new MethodCacheKey(method);
// 从缓存获取
List<Object> cached = this.methodCache.get(cacheKey);
if (cached == null) {
// 如果未从缓存中获取到,调用getInterceptorsAndDynamicInterceptionAdvice获取方法的拦截器链
cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
this, method, targetClass);
// 加入到缓存
this.methodCache.put(cacheKey, cached);
}
return cached;
}
}

DefaultAdvisorChainFactory

getInterceptorsAndDynamicInterceptionAdvice中先获取了所有的Advisor,然后遍历Advisor,对Advisor进行判断:

  1. 如果是PointcutAdvisor类型,将Advisor转为PointcutAdvisor,然后获取Pointcut切点的ClassFilter,通过matches方法判断当前方法所属的class是否匹配,如果匹配,则从切点中获取MethodMatcher方法匹配器,调用它的matches方法判断切点与当前方法是否匹配,如果也匹配,调用AdvisorAdapterRegistry的getInterceptors将Advisor转为方法拦截器

  2. 如果是引入通知IntroductionAdvisor,并且ClassFilter的matches与当前类匹配,调用AdvisorAdapterRegistry的getInterceptors将Advisor转为方法拦截器

  3. 非以上两种情况,调用AdvisorAdapterRegistry的getInterceptors将Advisor转为方法拦截器

可以看到以上三种情况,最后都是调用了AdvisorAdapterRegistry获取拦截器的,接下来就进入AdvisorAdapterRegistry中查看getInterceptors的具体实现。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
public class DefaultAdvisorChainFactory implements AdvisorChainFactory, Serializable {
/**
* 获取方法的拦截器链
*/
@Override
public List<Object> getInterceptorsAndDynamicInterceptionAdvice(
Advised config, Method method, @Nullable Class<?> targetClass) {

// 获取AdvisorAdapterRegistry
AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance();
// 获取Advisor
Advisor[] advisors = config.getAdvisors();
List<Object> interceptorList = new ArrayList<>(advisors.length);
Class<?> actualClass = (targetClass != null ? targetClass : method.getDeclaringClass());
Boolean hasIntroductions = null;
// 遍历Advisor
for (Advisor advisor : advisors) {
// 如果是PointcutAdvisor类型
if (advisor instanceof PointcutAdvisor) {
// Add it conditionally.
PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor;
// 调用matches方法判断与当前class是否匹配
if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) {
// 获取方法匹配器MethodMatcher
MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher();
boolean match;
// 如果是IntroductionAwareMethodMatcher
if (mm instanceof IntroductionAwareMethodMatcher) {
if (hasIntroductions == null) {
hasIntroductions = hasMatchingIntroductions(advisors, actualClass);
}
match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions);
}
else {
// 调用MethodMatcher的matches方法匹配
match = mm.matches(method, actualClass);
}
if (match) {
// 如果匹配,从registry中获取方法拦截器
MethodInterceptor[] interceptors = registry.getInterceptors(advisor);
if (mm.isRuntime()) {
// Creating a new object instance in the getInterceptors() method
// isn't a problem as we normally cache created chains.
for (MethodInterceptor interceptor : interceptors) {
// 将拦截器封装为InterceptorAndDynamicMethodMatcher
interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm));
}
}
else {
interceptorList.addAll(Arrays.asList(interceptors));
}
}
}
}
else if (advisor instanceof IntroductionAdvisor) {
// 如果是引入通知IntroductionAdvisor
IntroductionAdvisor ia = (IntroductionAdvisor) advisor;
if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}
else {
Interceptor[] interceptors = registry.getInterceptors(advisor);
interceptorList.addAll(Arrays.asList(interceptors));
}
}

return interceptorList;
}
}

DefaultAdvisorAdapterRegistry

在DefaultAdvisorAdapterRegistry的构造函数中注册了三种AdvisorAdapter,使用了适配器模式将Advisor转换为方法拦截器MethodInterceptor,三种AdvisorAdapter分别为:

  • MethodBeforeAdviceAdapter:支持前置通知MethodBeforeAdvice
  • AfterReturningAdviceAdapter:支持返回通知AfterReturningAdvice
  • ThrowsAdviceAdapter:支持ThrowsAdvice

AspectJAfterThrowingAdvice、AspectJAroundAdvice和AspectJAfterAdvice本身已经实现了MethodInterceptor接口,所以不需要进行转换:

在getInterceptors方法中,对Advisor进行判断,如果本身已经是MethodInterceptor直接返回即可,否则遍历所有的AdvisorAdapter,找出支持当前Advisor的Adapter,然后将Advisor转为MethodInterceptor:

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
38
public class DefaultAdvisorAdapterRegistry implements AdvisorAdapterRegistry, Serializable {

private final List<AdvisorAdapter> adapters = new ArrayList<>(3);

/**
* 注册AdvisorAdapter
*/
public DefaultAdvisorAdapterRegistry() {
// 注册MethodBeforeAdviceAdapter
registerAdvisorAdapter(new MethodBeforeAdviceAdapter());
// 注册AfterReturningAdviceAdapter
registerAdvisorAdapter(new AfterReturningAdviceAdapter());
// 注册ThrowsAdviceAdapter
registerAdvisorAdapter(new ThrowsAdviceAdapter());
}

@Override
public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException {
List<MethodInterceptor> interceptors = new ArrayList<>(3);
// 从Advisor中获取Advice
Advice advice = advisor.getAdvice();
if (advice instanceof MethodInterceptor) {
interceptors.add((MethodInterceptor) advice);
}
// 遍历Adapter
for (AdvisorAdapter adapter : this.adapters) {
// 判断是否支持当前的通知
if (adapter.supportsAdvice(advice)) {
// 通过Adapter获取拦截器
interceptors.add(adapter.getInterceptor(advisor));
}
}
if (interceptors.isEmpty()) {
throw new UnknownAdviceTypeException(advisor.getAdvice());
}
return interceptors.toArray(new MethodInterceptor[0]);
}
}

AdvisorAdapter有三个实现类,以MethodBeforeAdviceAdapter为例查看一下supportsAdvice和getInterceptor的实现:

  1. 在supportsAdvice方法中可以看到它支持的是MethodBeforeAdvice方法前置通知

  2. 在getInterceptor方法中,首先从Advisor中获取到了通知,然后将通知封装为MethodBeforeAdviceInterceptor返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable {

@Override
public boolean supportsAdvice(Advice advice) {
// 是否是MethodBeforeAdvice类型
return (advice instanceof MethodBeforeAdvice);
}

@Override
public MethodInterceptor getInterceptor(Advisor advisor) {
MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice();
// 封装为MethodBeforeAdviceInterceptor
return new MethodBeforeAdviceInterceptor(advice);
}

}

执行目标方法

回顾invoke方法中的主要逻辑,在获取到方法的拦截器之后,对拦截器是否为空进行了判断:

  • 如果为空,调用AopUtils的invokeJoinpointUsingReflection通过反射直接执行方法即可

  • 如果不为空创建MethodInvocation,具体实现类是ReflectiveMethodInvocation,然后调用proceed执行拦截器链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

// 获取当前方法的拦截器链
List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);

// 校验拦截器链是否为空
if (chain.isEmpty()) {
Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
// 如果为空直接执行目标方法即可
retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse);
} else {
// 创建MethodInvocation,使用的是ReflectiveMethodInvocation类型
MethodInvocation invocation =
new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain);
// 执行拦截器链
retVal = invocation.proceed();
}

invokeJoinpointUsingReflection

invokeJoinpointUsingReflection方法通过反射执行目标方法,在AopUtils中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public abstract class AopUtils {

@Nullable
public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args)
throws Throwable {

// 通过反射执行目标方法
try {
ReflectionUtils.makeAccessible(method);
return method.invoke(target, args);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" +
method + "] on target [" + target + "]", ex);
}
catch (IllegalAccessException ex) {
throw new AopInvocationException("Could not access method [" + method + "]", ex);
}
}

}

proceed方法执行拦截器链

因为拦截器可以有多个,所以proceed方法是一个递归调用的过程,currentInterceptorIndex记录了当前拦截器的下标:

  1. 判断currentInterceptorIndex是否与拦截器链的大小一致,如果一致说明已经走到了最后一个拦截器,调用invokeJoinpoint方法执行目标方法即可,可以看到调用了AopUtils的invokeJoinpointUsingReflection通过反射执行目标方法,如果不是最后一个拦截器进入第2步
  2. 对currentInterceptorIndex++,获取下一个拦截器,判断拦截器是否是InterceptorAndDynamicMethodMatcher类型,如果是获取methodMatcher对目标方法进行匹配:
    • 如果与目标方法匹配成功,执行拦截器的invoke方法
    • 如果与目标方法匹配不成功,递归调用proceed方法执行下一个拦截器
  3. 如果拦截器不是InterceptorAndDynamicMethodMatcher类型,直接调用方法拦截器MethodInterceptor的invoke执行拦截器即可

在执行MethodInterceptor方法拦截器的invoke方法时,传入的参数是this,指的是ReflectiveMethodInvocation对象本身,在拦截器方法执行后需要拿到这个对象调用proceed方法继续执行下一个拦截器,可以看到这里使用了责任链模式,对拦截器进行一个个的调用。

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
38
39
40
41
42
public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Cloneable {
@Override
@Nullable
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
// 执行方法
return invokeJoinpoint();
}
// 获取下一个拦截器
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// 如果是InterceptorAndDynamicMethodMatcher
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
Class<?> targetClass = (this.targetClass != null ? this.targetClass : this.method.getDeclaringClass());
// 判断方法是否匹配
if (dm.methodMatcher.matches(this.method, targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// 如果方法不匹配,跳过当前拦截器执行下一个,递归调用
return proceed();
}
}
else {
// 转为MethodInterceptor执行拦截器,注意invoke传入的参数是this,指的是ReflectiveMethodInvocation对象本身,在拦截器的invoke方法中需要拿到这个对象调用proceed方法继续执行下一个拦截器
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}

/**
* 通过反射执行目标方法
*/
@Nullable
protected Object invokeJoinpoint() throws Throwable {
return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments);
}

}

MethodBeforeAdviceInterceptor

以前置通知为例,看一下MethodBeforeAdviceInterceptor拦截器中的invoke方法的执行逻辑:

  1. 前置通知是在目标方法执行之前执行的方法,所以先调用了invokeAdviceMethod执行了前置通知方法
  2. 调用MethodInvocation的proceed执行下一个拦截器链,在上一步中可以看到调用拦截器时传入的是this,this指向ReflectiveMethodInvocation,所以会继续执行到它的proceed方法,继续下一个拦截器的执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable {

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
// 先执行通知方法,在AspectJMethodBeforeAdvice中实现
this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis());
// 执行拦截器链,这里调用的是MethodInvocation的proceed方法,会再次进入到ReflectiveMethodInvocation的proceed方法中
return mi.proceed();
}

}
// AspectJMethodBeforeAdvice
public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable {
@Override
public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
// 执行通知方法
invokeAdviceMethod(getJoinPointMatch(), null, null);
}
}

AspectJAroundAdvice

再以环绕通知为例,看一下环绕通知的执行逻辑,AspectJAroundAdvice实现了MethodInterceptor接口,所以它本身就是一个拦截器,在invoke方法中它调用了invokeAdviceMethod执行通知方法,并将ReflectiveMethodInvocation转换为ProceedingJoinPoint,在环绕通知中通过ProceedingJoinPoint调用proceed方法执行下一个拦截器:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptor, Serializable {

@Override
public Object invoke(MethodInvocation mi) throws Throwable {
if (!(mi instanceof ProxyMethodInvocation)) {
throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
}
// mi是上一步中传入的ReflectiveMethodInvocation,这里将其转换为ProxyMethodInvocation
ProxyMethodInvocation pmi = (ProxyMethodInvocation) mi;
// 将ProxyMethodInvocation转为ProceedingJoinPoint
ProceedingJoinPoint pjp = lazyGetProceedingJoinPoint(pmi);
JoinPointMatch jpm = getJoinPointMatch(pmi);
// 执行通知方法
return invokeAdviceMethod(pjp, jpm, null, null);
}
}

@SuppressWarnings("serial")
public abstract class AbstractAspectJAdvice implements Advice, AspectJPrecedenceInformation, Serializable {
/**
* 执行通知方法
*/
protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMatch,
@Nullable Object returnValue, @Nullable Throwable t) throws Throwable {
// 通过给定的参数执行通知方法
return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t));
}
/**
* 通过给定的参数执行通知方法
*/
protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable {
Object[] actualArgs = args;
if (this.aspectJAdviceMethod.getParameterCount() == 0) {
actualArgs = null;
}
try {
ReflectionUtils.makeAccessible(this.aspectJAdviceMethod);
// 这里执行了通知方法,接下来会进入到定义的环绕通知方法中
return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs);
}
catch (IllegalArgumentException ex) {
throw new AopInvocationException("Mismatch on arguments to advice method [" +
this.aspectJAdviceMethod + "]; pointcut expression [" +
this.pointcut.getPointcutExpression() + "]", ex);
}
catch (InvocationTargetException ex) {
throw ex.getTargetException();
}
}
}

假设定义了如下的环绕通知,在环绕通知方法被执行时,会进入到logAroudAdvice方法中,可以看到先打印了方法执行前的日志,然后调用了ProceedingJoinPoint的proceed方法执行拦截器链,上一步可知ProceedingJoinPoint是MethodInvocation转换而来,所以又会进入到ReflectiveMethodInvocation的proceed方法执行下一个拦截器链,待所有的拦截器执行完毕后proceed方法也就结束,然后执行了printAfterLog打印方法执行后的日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 通知Advice,这里使用了环绕通知
* @param joinPoint 连接点
* @return
* @throws Throwable
*/
@Around("logPoiontcut()") // 引用切点
public Object logAroudAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
// 方法执行前的日志打印
printBeforeLog(joinPoint);
// 执行拦截器链
Object returnValue = joinPoint.proceed();
// 方法执行后的日志打印
printAfterLog(returnValue);
return returnValue;
}

总结

方法拦截器链的执行流程图

AOP总结

参考

【猫吻鱼】Spring源码分析:全集整理

Spring版本:5.2.5.RELEASE

【Spring】AOP实现原理(二):Advisor获取(基于注解)

Posted on 2022-04-19

@EnableAspectJAutoProxy

@EnableAspectJAutoProxy注解可以用来开启AOP,那么就从@EnableAspectJAutoProxy入手学习一下Spring AOP的实现原理。

  1. @EnableAspectJAutoProxy导入了AspectJAutoProxyRegistrar。
  2. 定义了proxyTargetClass属性,表示是否使用CGLIB生成代理对象,默认返回false,默认是使用JDK动态代理创建代理对象的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)// 导入了AspectJAutoProxyRegistrar
public @interface EnableAspectJAutoProxy {

/**
* 是否使用CGLIB生成代理对象,默认为false,默认是使用JDK动态代理创建代理对象的
*/
boolean proxyTargetClass() default false;

/**
* 是否暴露代理
*/
boolean exposeProxy() default false;

}

AspectJAutoProxyRegistrar

AspectJAutoProxyRegistrar是一个代理注册器,它实现了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar可以向容器中注册bean,AspectJAutoProxyRegistrar实现了它应该是为了向容器中注册bean,那么看一下registerBeanDefinitions方法里面注册了什么。

在registerBeanDefinitions方法中它调用了AopConfigUtils的registerAspectJAnnotationAutoProxyCreatorIfNecessary方法向容器中注册了自动代理创建器,通过名称可以看出它与AspectJ注解以及代理创建有关。

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
/**
* AspectJ自动代理注册器
*/
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {

/**
* 注册BeanDefinitions
*/
@Override
public void registerBeanDefinitions(
AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 向容器中注册代理创建器
AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
AnnotationAttributes enableAspectJAutoProxy =
AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
if (enableAspectJAutoProxy != null) {
// 获取proxyTargetClass
if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
}
// 获取exposeProxy
if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
}
}
}
}

注册自动代理创建器

registerAspectJAnnotationAutoProxyCreatorIfNecessary在AopConfigUtils中实现:

  1. 定义了一个自动代理创建器集合,是一个List,List里面的存储顺序代表了优先级,一共有三种代理创建器
    • InfrastructureAdvisorAutoProxyCreator:优先级最低
    • AspectJAwareAdvisorAutoProxyCreator:优先级较InfrastructureAdvisorAutoProxyCreator高
    • AnnotationAwareAspectJAutoProxyCreator:优先级最高
  2. Spring AOP默认使用的是AnnotationAwareAspectJAutoProxyCreator类型的创建器,向容器中注册的时候会判断容器中是否已经存在代理创建器:
    • 如果已经存在,从容器中取出创建器,判断优先级是否比AnnotationAwareAspectJAutoProxyCreator高,如果低于AnnotationAwareAspectJAutoProxyCreator,则使用AnnotationAwareAspectJAutoProxyCreator进行替换。
    • 如果不存在,直接向容器中注册AnnotationAwareAspectJAutoProxyCreator类型的bean即可。
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public abstract class AopConfigUtils {

/**
* 自动代理创建器beanName
*/
public static final String AUTO_PROXY_CREATOR_BEAN_NAME =
"org.springframework.aop.config.internalAutoProxyCreator";

/**
* 自动创建器集合,是一个List,List里面的存储顺序代表了优先级
*/
private static final List<Class<?>> APC_PRIORITY_LIST = new ArrayList<>(3);

static {
// 向List中添加具体的代理创建器
APC_PRIORITY_LIST.add(InfrastructureAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AspectJAwareAdvisorAutoProxyCreator.class);
APC_PRIORITY_LIST.add(AnnotationAwareAspectJAutoProxyCreator.class);
}


@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
// 调用下面的registerAspectJAnnotationAutoProxyCreatorIfNecessary方法
return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null);
}

@Nullable
public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(
BeanDefinitionRegistry registry, @Nullable Object source) {
// 这里可以看到,注册的是AnnotationAwareAspectJAutoProxyCreator类型的
return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source);
}

@Nullable
private static BeanDefinition registerOrEscalateApcAsRequired(
Class<?> cls, BeanDefinitionRegistry registry, @Nullable Object source) {

Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
// 如果容器中已经包含名称为AUTO_PROXY_CREATOR_BEAN_NAME的bean
if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) {
// 从容器中获取
BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME);
// 如果当前要注册的bean与容器中已经存在的bean类型不一致
if (!cls.getName().equals(apcDefinition.getBeanClassName())) {
// 获取容器中存在的bean的优先级
int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName());
// 获取要注册的bean的优先级
int requiredPriority = findPriorityForClass(cls);
// 容器中存在的bean的优先级比当前要注册的低
if (currentPriority < requiredPriority) {
// 使用当的class进行注册,也就是AnnotationAwareAspectJAutoProxyCreator
apcDefinition.setBeanClassName(cls.getName());
}
}
return null;
}
// 创建BeanDefinition
RootBeanDefinition beanDefinition = new RootBeanDefinition(cls);
beanDefinition.setSource(source);
beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
// 向容器中注册
registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition);
return beanDefinition;
}
}

AbstractAutoProxyCreator

自动代理创建器AbstractAutoProxyCreator用于创建代理对象,Spring AOP使用的是AnnotationAwareAspectJAutoProxyCreator类型的创建器,它是AbstractAutoProxyCreator的子类,继承关系如下:

AnnotationAwareAspectJAutoProxyCreator的主要方法都在AbstractAutoProxyCreator中实现,AbstractAutoProxyCreator实现了SmartInstantiationAwareBeanPostProcessor,它是Spring的Bean后置处理器,后置处理器有两个比较重要的方法:

postProcessBeforeInstantiation:在bean实例化之前执行的方法,如果有自定义的TargetSource则在这个时候就创建代理对象,可以先不管这里,主要看postProcessAfterInitialization方法。

postProcessAfterInitialization:在bean初始化之后执行的方法,这时候bean已经实例化完毕但是还没有设置属性等信息,调用了wrapIfNecessary方法判断是否有必要生成代理对象,如果不需要创建代理对象直接返回即可,反之需要调用getAdvicesAndAdvisorsForBean获取Advice和Advisor,然后创建代理对象。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

/**
* 在bean实例化之前执行的方法
*/
@Override
public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
Object cacheKey = getCacheKey(beanClass, beanName);

if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}
if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

// 如果有自定义的TargetSource则创建代理对象
TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
if (targetSource != null) {
if (StringUtils.hasLength(beanName)) {
this.targetSourcedBeans.add(beanName);
}
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
// 创建代理对象
Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

return null;
}

/**
* 在bean初始化之后执行的方法
*/
@Override
public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
// 构建缓存Key
Object cacheKey = getCacheKey(bean.getClass(), beanName);
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 是否有必要创建代理
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}

/**
* 是否有必要生成代理对象
*/
protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
// 如果是基础类或者需要跳过则不创建代理对象,将bean加入到advisedBeans中即可
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 获取bean的Advices和Advisors
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
this.advisedBeans.put(cacheKey, Boolean.TRUE);
// 创建代理
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
}

跳过代理对象创建的判断

跳过代理对象的创建主要通过以下两个方法判断的:

1.调用isInfrastructureClass判断是否是基础类,如果是则跳过代理创建。

2.调用shouldSkip方法判断是否需要跳过创建代理。

isInfrastructureClass

需要注意AOP使用的是AnnotationAwareAspectJAutoProxyCreator作为代理创建器,所以需要先看AnnotationAwareAspectJAutoProxyCreator是否重写了该方法,实际上它确实重写isInfrastructureClass方法,在里面它调用了父类的AbstractAutoProxyCreator的isInfrastructureClass和aspectJAdvisorFactory的isAspect进行判断:

  • isInfrastructureClass:如果是Advice、Pointcut、Advisor、AopInfrastructureBean及其子类则跳过创建,这些是Spring的基础类,不需要进行代理。
  • isAspect:是否有Aspect注解并且不是通过Ajc编译的类则是一个切面。

总结:如果是Advice、Pointcut、Advisor、AopInfrastructureBean及其子类或者bean是一个切面并且不是通过Ajc编译的则跳过代理创建,并将当前bean的缓存key加入到advisedBeans中(wrapIfNecessary中可以看到)。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {

@Override
protected boolean isInfrastructureClass(Class<?> beanClass) {
// 调用AbstractAutoProxyCreator的isInfrastructureClass和aspectJAdvisorFactory的isAspect方法判断,在AbstractAspectJAdvisorFactory中有实现
return (super.isInfrastructureClass(beanClass) ||
(this.aspectJAdvisorFactory != null && this.aspectJAdvisorFactory.isAspect(beanClass)));
}
}

// AbstractAutoProxyCreator
public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {
/**
* 是否是基础类
*/
protected boolean isInfrastructureClass(Class<?> beanClass) {
// 如果是Advice、Pointcut、Advisor、AopInfrastructureBean类型
boolean retVal = Advice.class.isAssignableFrom(beanClass) ||
Pointcut.class.isAssignableFrom(beanClass) ||
Advisor.class.isAssignableFrom(beanClass) ||
AopInfrastructureBean.class.isAssignableFrom(beanClass);
if (retVal && logger.isTraceEnabled()) {
logger.trace("Did not attempt to auto-proxy infrastructure class [" + beanClass.getName() + "]");
}
return retVal;
}

}

// AbstractAspectJAdvisorFactory
public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory {
/**
* 是否是切面
*/
@Override
public boolean isAspect(Class<?> clazz) {
// 是否有Aspect注解并且不是通过Ajc编译的类
return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
}

// 判断是否有Aspect注解
private boolean hasAspectAnnotation(Class<?> clazz) {
// 是否有Aspect注解
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
}

/**
* 是否通过Ajc编译
*/
private boolean compiledByAjc(Class<?> clazz) {
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
}

shouldSkip

AnnotationAwareAspectJAutoProxyCreator中重写的shouldSkip方法具体实现在父类AspectJAwareAdvisorAutoProxyCreator里面,它获取所有候选的Advisor进行遍历,判断Advisor是否是AspectJPointcutAdvisor类型并且当前的bean与advisor的AspectName切面名称一致则跳过代理创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class AspectJAwareAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator {
/**
* 是否跳过代理创建
*/
@Override
protected boolean shouldSkip(Class<?> beanClass, String beanName) {
// 获取候选的Advisors
List<Advisor> candidateAdvisors = findCandidateAdvisors();
for (Advisor advisor : candidateAdvisors) {
// 如果是AspectJPointcutAdvisor类型并且当前的bean与advisor的AspectName切面名称一致,则跳过代理创建
if (advisor instanceof AspectJPointcutAdvisor &&
((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
return true;
}
}
// 调用父类的shouldSkip判断
return super.shouldSkip(beanClass, beanName);
}
}

获取所有的Advice和Advisor

getAdvicesAndAdvisorsForBean方法在AbstractAdvisorAutoProxyCreator中实现,它又调用了findEligibleAdvisors方法获取所有可应用到当前bean的Advisors:

  1. 调用findCandidateAdvisors获取所有候选的Advisor,需要注意的是虽然AbstractAdvisorAutoProxyCreator类中实现了findCandidateAdvisors,但是向容器中注册代理创建器实际的类型是AnnotationAwareAspectJAutoProxyCreator,它重写了findCandidateAdvisors方法,所以会先进入AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors方法,它里面又调用了父类的findCandidateAdvisors,这时候才会进入AbstractAdvisorAutoProxyCreator的方法。
  2. 从所有候选的Advisor中过滤出可以适用于当前bean的Advisor,findAdvisorsThatCanApply具体实现逻辑在AopUtils中。
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator {
@Override
@Nullable
protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {
// 调用findEligibleAdvisors获取Advisor
List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

/**
* 获取Advisor
*/
protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
// 1.获取所有的Advisor,需要注意实际的类型是AnnotationAwareAspectJAutoProxyCreator,所以会先进入它的findCandidateAdvisors方法中
List<Advisor> candidateAdvisors = findCandidateAdvisors();
// 2.获取可以应用到当前bean的Advisor
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

/**
* AbstractAutoProxyCreator中获取所有候选的Advisors方法,AnnotationAwareAspectJAutoProxyCreator中重写的findCandidateAdvisors会调用这个方法
*/
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
// 具体的实现逻辑在advisorRetrievalHelper的findAdvisorBeans
return this.advisorRetrievalHelper.findAdvisorBeans();
}

/**
* 获取可以应用到当前bean的Advisor
*/
protected List<Advisor> findAdvisorsThatCanApply(
List<Advisor> candidateAdvisors, Class<?> beanClass, String beanName) {

ProxyCreationContext.setCurrentProxiedBeanName(beanName);
try {
// 获取可以应用到当前bean的Advisor
return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass);
} finally {
ProxyCreationContext.setCurrentProxiedBeanName(null);
}
}
}

findCandidateAdvisors获取所有候选的Advisors

AOP使用的是的AnnotationAwareAspectJAutoProxyCreator类型的创建器,它重写了findCandidateAdvisors方法,方法的处理逻辑如下:

  • 调用了父类的findCandidateAdvisors方法获取候选的Advisors,也就是AbstractAdvisorAutoProxyCreator中实现的findCandidateAdvisors方法,这里的Advisors指的是本身是Advisor类型的bean。

  • 调用了buildAspectJAdvisors方法构建Advisors,具体的实现在BeanFactoryAspectJAdvisorsBuilder中, 这里的Advisors指的是使用了@AspectJ注解定义的切面,Spring会把它包装成Advisor。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
/**
* AnnotationAwareAspectJAutoProxyCreator
*/
public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {
// Advisors构建工厂
@Nullable
private BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder;

@Override
protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.initBeanFactory(beanFactory);
if (this.aspectJAdvisorFactory == null) {
// 如果为空使用ReflectiveAspectJAdvisorFactory
this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory);
}
// 初始化BeanFactoryAspectJAdvisorsBuilder,这里使用的BeanFactoryAspectJAdvisorsBuilderAdapter
this.aspectJAdvisorsBuilder =
new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory);
}

protected List<Advisor> findCandidateAdvisors() {
// 1.调用父类的findCandidateAdvisors获取所有的Advisors,也就是AbstractAdvisorAutoProxyCreator中实现的findCandidateAdvisors方法
List<Advisor> advisors = super.findCandidateAdvisors();
if (this.aspectJAdvisorsBuilder != null) {
// 2.构建Advisors,具体实现在BeanFactoryAspectJAdvisorsBuilder中
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}
}

/**
* AbstractAdvisorAutoProxyCreator
* 它是实现了findCandidateAdvisors
*/
public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator {
@Nullable
private BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper;

protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
// 使用的BeanFactoryAdvisorRetrievalHelperAdapter
this.advisorRetrievalHelper = new BeanFactoryAdvisorRetrievalHelperAdapter(beanFactory);
}

/**
* 获取所有的候选Advisors
*
* @return the List of candidate Advisors
*/
protected List<Advisor> findCandidateAdvisors() {
Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available");
// 获取候选的Advisor,具体的实现逻辑在advisorRetrievalHelper的findAdvisorBeans
return this.advisorRetrievalHelper.findAdvisorBeans();
}
}

1. findCandidateAdvisors获取候选的Advisors

findCandidateAdvisors方法具体的实现在BeanFactoryAdvisorRetrievalHelper中,它从beanFactory中获取了所有Advisor类型的bean:

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
38
39
public class BeanFactoryAdvisorRetrievalHelper {
/**
* 在当前的bean工厂中查找所有Advisor类型的bean
*/
public List<Advisor> findAdvisorBeans() {
// 获取缓存的AdvisorBeanName
String[] advisorNames = this.cachedAdvisorBeanNames;
if (advisorNames == null) {
advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Advisor.class, true, false);
this.cachedAdvisorBeanNames = advisorNames;
}
// 如果未获取到
if (advisorNames.length == 0) {
return new ArrayList<>();
}
List<Advisor> advisors = new ArrayList<>();
// 遍历所有的advisorNames
for (String name : advisorNames) {
if (isEligibleBean(name)) {
// 如果bean正在创建中
if (this.beanFactory.isCurrentlyInCreation(name)) {
if (logger.isTraceEnabled()) {
logger.trace("Skipping currently created advisor '" + name + "'");
}
} else {
try {
// 从容器中获取实例,可以看到这里获取的是Advisor类型的bean
advisors.add(this.beanFactory.getBean(name, Advisor.class));
} catch (BeanCreationException ex) {
// 省略了异常处理
throw ex;
}
}
}
}
return advisors;
}
}

2. buildAspectJAdvisors构建Advisor

buildAspectJAdvisors在BeanFactoryAspectJAdvisorsBuilder中实现,这一步用于查找所有使用@AspectJ注解的bean,并将其包装成Advisor返回:

(1)从容器中获取所有的bean,调用isEligibleBean方法判断bean是否符合要求,如果符合进入下一步

(2)调用isAspect方法判断bean是否是一个切面,如果是进入下一步

(3)判断是否是单例模式

  • 如果是,创建BeanFactoryAspectInstanceFactory类型的工厂,调用AspectJAdvisorFactory的getAdvisors方法获取Advisor
  • 如果不是单例模式,创建PrototypeAspectInstanceFactory类型的工厂,调用AspectJAdvisorFactory的getAdvisors方法获取Advisor
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
public class BeanFactoryAspectJAdvisorsBuilder {

private final AspectJAdvisorFactory advisorFactory;

/**
* 在当前bean工厂中查找所有使用@AspectJ注解的bean,并包装成Advisor返回
*/
public List<Advisor> buildAspectJAdvisors() {
// 获取所有的切面beanName
List<String> aspectNames = this.aspectBeanNames;
// 如果为空
if (aspectNames == null) {
synchronized (this) {
aspectNames = this.aspectBeanNames;
if (aspectNames == null) {
List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
// 从容器中获取所有的beanName
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);
for (String beanName : beanNames) {
// 判断是否符合条件,BeanFactoryAspectJAdvisorsBuilderAdapter中实现
if (!isEligibleBean(beanName)) {
continue;
}
// 根据BeanName获取class类型
Class<?> beanType = this.beanFactory.getType(beanName);
if (beanType == null) {
continue;
}
// 是否是切面
if (this.advisorFactory.isAspect(beanType)) {
// 添加到aspectNames
aspectNames.add(beanName);
// 创建Aspect元数据信息
AspectMetadata amd = new AspectMetadata(beanType, beanName);
// 单例模式
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
// 创建MetadataAwareAspectInstanceFactory
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
// 获取Advisor
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
} else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
} else {
// 原型模式
if (this.beanFactory.isSingleton(beanName)) {
throw new IllegalArgumentException("Bean with name '" + beanName +
"' is a singleton, but aspect instantiation model is not singleton");
}
MetadataAwareAspectInstanceFactory factory =
new PrototypeAspectInstanceFactory(this.beanFactory, beanName);
this.aspectFactoryCache.put(beanName, factory);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;
}
}
}

if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
} else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}
}
isEligibleBean的判断

BeanFactoryAspectJAdvisorsBuilderAdapter是AnnotationAwareAspectJAutoProxyCreator的内部类,它继承了BeanFactoryAspectJAdvisorsBuilder并重写了isEligibleBean方法:

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
public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {

private class BeanFactoryAspectJAdvisorsBuilderAdapter extends BeanFactoryAspectJAdvisorsBuilder {

@Override
protected boolean isEligibleBean(String beanName) {
// 调用isEligibleAspectBean
return AnnotationAwareAspectJAutoProxyCreator.this.isEligibleAspectBean(beanName);
}
}

/**
* 校验给定的切面是否符合要求
*/
protected boolean isEligibleAspectBean(String beanName) {
if (this.includePatterns == null) {
return true;
}
else {
for (Pattern pattern : this.includePatterns) {
// 通过includePatterns进行匹配
if (pattern.matcher(beanName).matches()) {
return true;
}
}
return false;
}
}
}
isAspect的判断

前面在讲跳过代理对象的创建时已经看到isAspect的实现逻辑,如果使用了Aspect注解并且不是通过Ajc进行编译的,则判定为切面:

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
// AbstractAspectJAdvisorFactory
public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory {

/**
* 是否是切面
*/
@Override
public boolean isAspect(Class<?> clazz) {
// 是否有Aspect注解并且不是通过Ajc编译的类
return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz));
}

// 判断是否有Aspect注解
private boolean hasAspectAnnotation(Class<?> clazz) {
// 是否有Aspect注解
return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null);
}

/**
* 是否通过Ajc编译
*/
private boolean compiledByAjc(Class<?> clazz) {
//
for (Field field : clazz.getDeclaredFields()) {
if (field.getName().startsWith(AJC_MAGIC)) {
return true;
}
}
return false;
}
}
AspectJAdvisorFactory获取Advisor

AspectJAdvisorFactory是一个Advisor工厂,它的继承关系如下:

AnnotationAwareAspectJAutoProxyCreator在初始化bean工厂的方法中,对AspectJAdvisorFactory进行了判断,则如果为空使用ReflectiveAspectJAdvisorFactory:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator {
// Advisors构建工厂
@Nullable
private BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder;

@Override
protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
super.initBeanFactory(beanFactory);
if (this.aspectJAdvisorFactory == null) {
// 如果为空使用ReflectiveAspectJAdvisorFactory
this.aspectJAdvisorFactory = new ReflectiveAspectJAdvisorFactory(beanFactory);
}
// 初始化BeanFactoryAspectJAdvisorsBuilder
this.aspectJAdvisorsBuilder =
new BeanFactoryAspectJAdvisorsBuilderAdapter(beanFactory, this.aspectJAdvisorFactory);
}
}

ReflectiveAspectJAdvisorFactory中实现了getAdvisor方法:

  1. getAdvisorMethods获取Advisor方法(也就是获取通知),具体是根据方法上是否有Pointcut注解来判断的,如果没有Pointcut注解则判定为是Advisor方法
  2. 调用getAdvisor方法将第1步中获取到的Advisor方法构建为Advisor对象
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable {
@Override
public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
// 获取Aspect类信息
Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
validate(aspectClass);

// 装饰模式
MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

List<Advisor> advisors = new ArrayList<>();
// 获取Advisor方法,也就是获取没有使用@Pointcut注解的方法
for (Method method : getAdvisorMethods(aspectClass)) {
// 将方法构建为Advisor对象
Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName);
if (advisor != null) {
advisors.add(advisor);
}
}

// If it's a per target aspect, emit the dummy instantiating aspect.
if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory);
advisors.add(0, instantiationAdvisor);
}

// Find introduction fields.
for (Field field : aspectClass.getDeclaredFields()) {
Advisor advisor = getDeclareParentsAdvisor(field);
if (advisor != null) {
advisors.add(advisor);
}
}

return advisors;
}

// 获取Advisor方法,也就是获取切面中没有被使用Pointcut注解的方法
private List<Method> getAdvisorMethods(Class<?> aspectClass) {
final List<Method> methods = new ArrayList<>();
ReflectionUtils.doWithMethods(aspectClass, method -> {
// 判断是否有Pointcut注解,如果没有则判定为是Advisor方法加入结果集中
if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) {
methods.add(method);
}
}, ReflectionUtils.USER_DECLARED_METHODS);
if (methods.size() > 1) {
methods.sort(METHOD_COMPARATOR);
}
return methods;
}
}
构建Advisor的具体实现

ReflectiveAspectJAdvisorFactory的getAdvisor方法用于将方法封装为Advisor对象,可以看到第一个参数叫candidateAdviceMethod(候选的通知方法),上一步中传入的是获取到的Advisor方法,所以Spring中Advisor方法指的就是使用了通知注解的方法,getAdvisor方法主要做了如下操作:

  1. 构建AspectJExpressionPointcut切点表达式对象
    • 从通知方法上获取切面相关注解,具体是通过判断方法上是否有Pointcut、Around、Before、After、AfterReturning、AfterThrowing注解实现的
    • 从切面注解中获取设置的切点表达式,用于之后判断方法是否匹配使用,然后创建AspectJExpressionPointcut对象并设置获取到的切点表达式
  2. 将当前的Advisor方法、AspectJExpressionPointcut对象等信息封装为InstantiationModelAwarePointcutAdvisorImpl返回,它是一个Advisor
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable {

// 将Advisor方法构建为Advisor对象
@Override
@Nullable
public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory,
int declarationOrderInAspect, String aspectName) {

validate(aspectInstanceFactory.getAspectMetadata().getAspectClass());
// 获取切点
AspectJExpressionPointcut expressionPointcut = getPointcut(
candidateAdviceMethod, aspectInstanceFactory.getAspectMetadata().getAspectClass());
if (expressionPointcut == null) {
return null;
}
// 封装为InstantiationModelAwarePointcutAdvisorImpl
return new InstantiationModelAwarePointcutAdvisorImpl(expressionPointcut, candidateAdviceMethod,
this, aspectInstanceFactory, declarationOrderInAspect, aspectName);
}

// 获取切点表达式
@Nullable
private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class<?> candidateAspectClass) {
// 获取AspectJ注解,具体是通过判断方法上是否有Pointcut、Around、Before、After、AfterReturning、AfterThrowing注解实现的
AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
if (aspectJAnnotation == null) {
return null;
}
// 创建AspectJExpressionPointcut
AspectJExpressionPointcut ajexp =
new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class<?>[0]);
// 设置切点表达式
ajexp.setExpression(aspectJAnnotation.getPointcutExpression());
if (this.beanFactory != null) {
ajexp.setBeanFactory(this.beanFactory);
}
return ajexp;
}

}

// AbstractAspectJAdvisorFactory
public abstract class AbstractAspectJAdvisorFactory implements AspectJAdvisorFactory {

private static final Class<?>[] ASPECTJ_ANNOTATION_CLASSES = new Class<?>[]{
Pointcut.class, Around.class, Before.class, After.class, AfterReturning.class, AfterThrowing.class};

/**
* 判断是否有AspectJ相关注解
*/
@SuppressWarnings("unchecked")
@Nullable
protected static AspectJAnnotation<?> findAspectJAnnotationOnMethod(Method method) {
// 判断方法上是否有Pointcut、Around、Before、After、AfterReturning、AfterThrowing注解
for (Class<?> clazz : ASPECTJ_ANNOTATION_CLASSES) {
AspectJAnnotation<?> foundAnnotation = findAnnotation(method, (Class<Annotation>) clazz);
if (foundAnnotation != null) {
return foundAnnotation;
}
}
return null;
}
}

InstantiationModelAwarePointcutAdvisorImpl封装切面

InstantiationModelAwarePointcutAdvisorImpl是一个Adviosr,它对Aspect切面进行了封装,里面引用了Advice、Pointcut等切面相关信息,在构造函数的最后,调用了instantiateAdvice对通知进行增强处理,也就是根据不同的通知类型,将其包装为不同的Advice对象,具体的实现在ReflectiveAspectJAdvisorFactory中:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
final class InstantiationModelAwarePointcutAdvisorImpl
implements InstantiationModelAwarePointcutAdvisor, AspectJPrecedenceInformation, Serializable {
// 切点表达式,用于匹配方法是否需要进行拦截
private final AspectJExpressionPointcut declaredPointcut;

// 通知
private transient Method aspectJAdviceMethod;

// AspectJAdvisorFactory
private final AspectJAdvisorFactory aspectJAdvisorFactory;

private final MetadataAwareAspectInstanceFactory aspectInstanceFactory;

private final int declarationOrder;

// 切面名称
private final String aspectName;

// 切点
private final Pointcut pointcut;

private final boolean lazy;

// 通知
@Nullable
private Advice instantiatedAdvice;

@Nullable
private Boolean isBeforeAdvice;

@Nullable
private Boolean isAfterAdvice;


public InstantiationModelAwarePointcutAdvisorImpl(AspectJExpressionPointcut declaredPointcut,
Method aspectJAdviceMethod, AspectJAdvisorFactory aspectJAdvisorFactory,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

this.declaredPointcut = declaredPointcut;
// 设置切面的类信息
this.declaringClass = aspectJAdviceMethod.getDeclaringClass();
// 设置方法名称
this.methodName = aspectJAdviceMethod.getName();
this.parameterTypes = aspectJAdviceMethod.getParameterTypes();
// 通知
this.aspectJAdviceMethod = aspectJAdviceMethod;
// AspectJAdvisorFactory
this.aspectJAdvisorFactory = aspectJAdvisorFactory;
this.aspectInstanceFactory = aspectInstanceFactory;
this.declarationOrder = declarationOrder;
// 切面名称
this.aspectName = aspectName;

if (aspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) {
Pointcut preInstantiationPointcut = Pointcuts.union(
aspectInstanceFactory.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut);
this.pointcut = new PerTargetInstantiationModelPointcut(
this.declaredPointcut, preInstantiationPointcut, aspectInstanceFactory);
this.lazy = true;
} else {
// A singleton aspect.
this.pointcut = this.declaredPointcut;
this.lazy = false;
// 增强处理
this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut);
}
}

private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) {
// 调用ReflectiveAspectJAdvisorFactory获取Advice
Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut,
this.aspectInstanceFactory, this.declarationOrder, this.aspectName);
return (advice != null ? advice : EMPTY_ADVICE);
}
}

ReflectiveAspectJAdvisorFactory

ReflectiveAspectJAdvisorFactory中的getAdvice方法中可以看到对通知进行了判断,不同的通知使用不同的包装类:

  • 环绕通知,使用AspectJAroundAdvice
  • 前置通知,使用AspectJMethodBeforeAdvice
  • 后置通知,使用AspectJAfterAdvice
  • 返回通知,使用AspectJAfterReturningAdvice
  • 异常通知,使用AspectJAfterThrowingAdvice
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80

public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFactory implements Serializable {
@Override
@Nullable
public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut,
MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) {

Class<?> candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
validate(candidateAspectClass);
// 获取AspectJAnnotation
AspectJAnnotation<?> aspectJAnnotation =
AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod);
if (aspectJAnnotation == null) {
return null;
}

// 再次校验是否是切面
if (!isAspect(candidateAspectClass)) {
throw new AopConfigException("Advice must be declared inside an aspect type: " +
"Offending method '" + candidateAdviceMethod + "' in class [" +
candidateAspectClass.getName() + "]");
}

if (logger.isDebugEnabled()) {
logger.debug("Found AspectJ method: " + candidateAdviceMethod);
}

AbstractAspectJAdvice springAdvice;
// 判断通知类型
switch (aspectJAnnotation.getAnnotationType()) {
case AtPointcut: // 如果是一个切点
if (logger.isDebugEnabled()) {
logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'");
}
return null;
case AtAround: // 环绕通知,使用AspectJAroundAdvice封装
springAdvice = new AspectJAroundAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtBefore:// 前置通知,使用AspectJMethodBeforeAdvice
springAdvice = new AspectJMethodBeforeAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtAfter:// 后置通知,使用AspectJAfterAdvice
springAdvice = new AspectJAfterAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
break;
case AtAfterReturning:// 返回通知,使用AspectJAfterReturningAdvice
springAdvice = new AspectJAfterReturningAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterReturningAnnotation.returning())) {
springAdvice.setReturningName(afterReturningAnnotation.returning());
}
break;
case AtAfterThrowing:// 异常通知,使用AspectJAfterThrowingAdvice
springAdvice = new AspectJAfterThrowingAdvice(
candidateAdviceMethod, expressionPointcut, aspectInstanceFactory);
AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation();
if (StringUtils.hasText(afterThrowingAnnotation.throwing())) {
springAdvice.setThrowingName(afterThrowingAnnotation.throwing());
}
break;
default:
throw new UnsupportedOperationException(
"Unsupported advice type on method: " + candidateAdviceMethod);
}

// Now to configure the advice...
springAdvice.setAspectName(aspectName);
springAdvice.setDeclarationOrder(declarationOrder);
String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod);
if (argNames != null) {
springAdvice.setArgumentNamesFromStringArray(argNames);
}
springAdvice.calculateArgumentBindings();

return springAdvice;
}
}

findAdvisorsThatCanApply获取可以应用到当前bean的Advisor

上一节中获取到了所有的Advisor,接下来要过滤出可以应用到当前bean的Advisor,findAdvisorsThatCanApply在AopUtils中实现:

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
public abstract class AopUtils {

public static List<Advisor> findAdvisorsThatCanApply(List<Advisor> candidateAdvisors, Class<?> clazz) {
if (candidateAdvisors.isEmpty()) {
return candidateAdvisors;
}
List<Advisor> eligibleAdvisors = new ArrayList<>();
for (Advisor candidate : candidateAdvisors) {
// 是否是IntroductionAdvisor并且canApply,加入到eligibleAdvisors中
if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) {
eligibleAdvisors.add(candidate);
}
}
boolean hasIntroductions = !eligibleAdvisors.isEmpty();
// 再次遍历候选的Advisor
for (Advisor candidate : candidateAdvisors) {
// 如果是IntroductionAdvisor,上面已经对IntroductionAdvisor进行了处理,这里continue继续下一个
if (candidate instanceof IntroductionAdvisor) {
// already processed
continue;
}
// 调用canApply方法判断
if (canApply(candidate, clazz, hasIntroductions)) {
eligibleAdvisors.add(candidate);
}
}
return eligibleAdvisors;
}
}

核心的逻辑在canApply方法中:

  • 根据切点Pointcut的getClassFilter方法对类进行匹配,判断targetClass目标类是否与切点匹配

  • 从切点获取MethodMatcher方法匹配器,通过MethodMatcher对目标类中的每一个方法进行匹配,也就是使用切点表达式对方法进行匹配,判断方法是否需要拦截。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
public abstract class AopUtils {

/**
* 是否可以应用到bean
*/
public static boolean canApply(Advisor advisor, Class<?> targetClass, boolean hasIntroductions) {
if (advisor instanceof IntroductionAdvisor) {
return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass);
}
else if (advisor instanceof PointcutAdvisor) {
PointcutAdvisor pca = (PointcutAdvisor) advisor;
// 继续调用canApply
return canApply(pca.getPointcut(), targetClass, hasIntroductions);
}
else {
// It doesn't have a pointcut so we assume it applies.
return true;
}
}
/**
* 是否可以应用到bean
*/
public static boolean canApply(Pointcut pc, Class<?> targetClass, boolean hasIntroductions) {
Assert.notNull(pc, "Pointcut must not be null");
// 通过切点进行匹配,先判断类是否匹配
if (!pc.getClassFilter().matches(targetClass)) {
return false;
}
// 从切点中获取方法匹配器
MethodMatcher methodMatcher = pc.getMethodMatcher();
if (methodMatcher == MethodMatcher.TRUE) {
// No need to iterate the methods if we're matching any method anyway...
return true;
}
IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null;
// 如果是IntroductionAwareMethodMatcher
if (methodMatcher instanceof IntroductionAwareMethodMatcher) {
introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher;
}
Set<Class<?>> classes = new LinkedHashSet<>();
if (!Proxy.isProxyClass(targetClass)) {
classes.add(ClassUtils.getUserClass(targetClass));
}
// 获取类的所有接口
classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass));

for (Class<?> clazz : classes) {
// 获取类中的所有方法
Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz);
// 遍历方法
for (Method method : methods) {
if (introductionAwareMethodMatcher != null ?
introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) :
methodMatcher.matches(method, targetClass)) { // 调用方法的matches进行匹配
return true;
}
}
}
return false;
}
}

创建代理对象

上一节中已经获取到了可以应用到当前bean的Advisor,接下来就可以创建代理对象了,由于篇幅原因,创建代理的过程将另起一篇文章。

总结

参考

【猫吻鱼】Spring源码分析:全集整理

Spring版本:5.2.5.RELEASE

12…6

shan

53 posts
12 tags
© 2022 shan
Powered by Hexo
|
Theme — NexT.Muse v5.1.4