《Reids 设计与实现》第十五章 集群(中)

文章目录

  • 《Reids 设计与实现》第十五章 集群(中)
  • 四、在集群中执行命令
    • 1.计算键属于哪个槽
    • 2.判断槽是否由当前节点负责
    • 3.MOVED 错误
    • 4.节点数据库的实现
  • 五、重新分片
  • 六、ASK 错误
    • 1.CLUSTER SETSLOT IMPORTING 命令的实现
    • 2.CLUSTER SETSLOT MIGRATING 命令的实现
    • 3.ASK 错误
    • 4.ASKING 命令
    • 5.ASK 错误和 MOVED 错误的区别

四、在集群中执行命令

在对数据库中的 16384 个槽都进行了指派之后,集群就会进入上线状态,这时客户端就可以向集群中的节点发送数据命令了

当客户端向节点发送与数据库键有关的命令时,接受命令的节点会计算出命令要处理的数据库键属于哪个槽,并检查这个槽是否指派给了自己:

  • 如果键所在的槽正好就指派给了当前节点,那么节点直接执行这个命令
  • 如果键所在的槽并没有指派给当前节点,那么节点会向客户端返回一个 MOVED 错误,指引客户端转向(redirect)至正确的节点,并再次发送之前想要执行的命令

图 17-18 展示了这两种情况的判断流程

举个例子,如果我们在之前提到的,由 7000、7001、7002 三个节点组成的集群中,用客户端连上节点 7000,并发送以下命令,那么命令会直接被节点 7000 执行:

127.0.0.1:7000>SET data "2013-12-31"
OK

因为键 date 所在的槽 2022 正是由节点 7000 负责处理的

但是,如果我们执行以下命令,那么客户端会先被转向至节点 7001,然后再执行命令:

127.0.0.1:7000>SET msg "happy new year!"
-> Redirected to slot [6257] located at 127.0.0.1:7001
OK127.0.0.1:7001>GET msg
"happy new year!"

这是因为键 msg 所在的槽 6257 是由节点 7001 负责处理的,而不是由最初接受命令的节点 7000 负责处理:

  • 当客户端第一次向节点 7000 发送 SET 命令的时候,节点 7000 回向客户端返回 MOVED 错误,指引客户端转向至节点 7001
  • 当客户端转向到节点 7001 之后,客户端重新向节点 7001 发送 SET 命令,这个命令会被节点 7001 成功执行

1.计算键属于哪个槽

节点使用以下算法来计算给定键 key 属于哪个槽:

def slot_number(key):return CRC16(key) & 16383

其中 CRC16(key) 语句用于计算 key 的 CRC-16 校验和,而 & 16383 语句则用于计算出一个介于 0 至 16383 之间的整数作为键 key 的槽号

使用 CLUSTER KEYSLOT <key> 命令可以查看一个给定键属于哪个槽:

127.0.0.1:7000>CLUSTER KEYSLOT "date"
(integer) 2022127.0.0.1:7000>CLUSTER KEYSLOT "msg"
(integer) 6257127.0.0.1:7000>CLUSTER KEYSLOT "name"
(integer) 5798127.0.0.1:7000>CLUSTER KEYSLOT "fruits"
(integer) 14943

CLUSTER KEYSLOT 命令就是通过调用上面调用上面给出的槽分配算法来实现的,以下是该命令的伪代码实现:

def CLUSTER_KEYSLOT(key):#计算槽号slot = slot_number(key)#将槽号返回给客户端reply_client(slot)

2.判断槽是否由当前节点负责

当节点计算出键所属的槽 i 之后,节点就会检查自己在 clusterState.slots 数组中的项 i,判断键所在的槽是否由自己负责:

  1. 如果 clusterState.slots[i] 等于 clusterState.myself,那么说明槽 i 由当前节点负责,节点可以执行客户端发送的命令
  2. 如果 clusterState.slots[i] 不等于 clusterState.myself,那么说明槽 i 并非由当前节点负责,节点会根据 clusterState.slots[i] 指向的 clusterNode 结构所记录的节点 IP 和端口号,向客户端返回 MOVED 错误,指引客户端转向至正在处理槽 i 的节点

举个例子,假设图 17-19 为节点 7000 的 clusterState 结构:

  • 当客户端向节点 7000 发送命令 SET date “2013-12-31” 的时候,节点首先计算出键 date 属于槽 2022,然后检查得出 clusterState.slots[2022] 等于 clusterState.myself,这说明槽 2022 正是由节点 7000 负责,于是节点 7000 直接执行这个 SET 命令,并将结果返回给发送命令的客户端
  • 当客户端向节点 7000 发送命令 SET msg “happy new year!” 的时候,节点首先计算出键 msg 属于槽 6257,然后检查 clusterState.slots[6257] 是否等于 clusterState.myself,结果发现两者并不相等:这说明槽 6257 并非由节点 7000 负责处理,于是节点 7000 访问 clusterState.slots[6257] 所指向的 clusterNode 结构,并根据结构中记录的 IP 地址 127.0.0.1 和端口号 7001,向客户端返回错误 MOVED 6257 127.0.0.1:7001,指引节点转向至正在负责处理槽 6257 的节点 7001

3.MOVED 错误

当节点发现键所在的槽并非由自己负责处理的时候,节点就会向客户端返回一个 MOVED 错误,指引客户端转向正在负责槽的节点

MOVED 错误的格式为:

MOVED <slot> <ip>:<port>

其中 slot 为键所在的槽,而 ip 和 port 则是负责处理槽 slot 的节点的 IP 地址和端口号。例如错误:

MOVED 10086 127.0.0.1:7002

表示槽 10086 正由 IP 地址为 127.0.0.1,端口号为 7002 的节点负责

当客户端接收到节点返回的 MOVED 错误时,客户端会根据 MOVED 错误中提供的 IP 地址和端口号,转向至负责处理槽 slot 的节点,并向该节点重新发送之前想要执行的命令。以前面的客户端从节点 7000 转向至 7001 的情况作为例子:

127.0.0.1:7000>SET msg "happy new year!"
-> Redirected to slot [6527] located at 127.0.0.1:7001
OK127.0.0.1:7001>

图 17-20 展示了客户端向节点 7000 发送 SET 命令,并获得 MOVED 错误的过程

而图 17-21 则展示了客户端根据 MOVED 错误,转向至节点 7001,并重新发送 SET 命令的过程

一个集群客户端通常会与集群中的多个节点创建套接字连接,而所谓的节点转向实际上就是换一个套接字来发送命令

如果客户端尚未与想要转向的节点创建套接字连接,那么客户端会先根据 MOVED 错误提供的 IP 地址和端口号来连接节点,然后再进行转向

被隐藏的 MOVED 错误

集群模式的 redis-cli 客户端再接收到 MOVED 错误时,并不会打印出 MOVED 错误,而是根据 MOVED 错误自动进行节点转向,并打印出转向信息,所以我们是看不见节点返回的 MOVED 错误的:

$ redis-cli -c -p 7000       #集群模式127.0.0.1:7000>SET msg "happy new year!"
-> Redirected to slot [6257] located at 127.0.0.1:7001
OK127.0.0.1:7001>

但是,如果我们使用单机(stand alone)模式的 redis-cli 客户端,再次向节点 7000 发送相同的命令,那么 MOVED 错误就会被客户端打印出来:

$ redis-cli -p 7000      #单机模式127.0.0.1:7000>SET msg "happy new year!"
(error) MOVED 6257 127.0.0.1:7001127.0.0.1:7000>

这是因为单机模式的 redis-cli 客户端不清楚 MOVED 错误的作用,所以它只会直接将 MOVED 错误打印出来,而不会进行自动转向

4.节点数据库的实现

集群节点保存键值对以及键值对过期时间的方式,与单击 Redis 服务器保存键值对过期时间的方式完全相同

节点和单击服务器在数据库方面的一个区别是,节点只能使用 0 号数据库,而单机 Redis 服务器则没有这一限制

举个例子,图 17-22 展示了节点 7000 的数据库状态,数据库中包含列表键 “lst”,哈希键 “book”,以及字符串键 “date”,其中键 “lst” 和键 “book” 带有过期时间

另外,除了将键值对保存在数据库里面之外,节点还会用 clusterState 结构中的 slots_to_keys 跳跃表来保存槽和键之间的关系:

typedef struct clusterState{//...zskiplist *slots_to_keys;//...
}clusterState;

slots_to_keys 跳跃表每个节点的分值(score)都是一个槽号,而每个节点的成员(member)都是一个数据库键:

  • 每当节点往数据库中添加一个新的键值对时,节点就会将这个键以及键的槽号关联到 slots_to_keys 跳跃表
  • 当节点删除数据库中的某个键值对时,节点就会在 slots_to_keys 跳跃表解除被删除键与槽号的关联

举个例子, 对于图 17-22 所示的数据库,节点 7000 将创建类似图 17-233 所示的 slots_to_keys 跳跃表:

  • 键 “book” 所在跳跃表节点的分值为 1337.0,这表示键 “book” 所在的槽为 1337
  • 键 “date” 所在跳跃表节点的分值为 2022.0,这表示键 “date” 所在的槽为 2022
  • 键 “lst” 所在跳跃表节点的分值为 3347.0,这表示键 “lst” 所在的槽为 3347

通过在 slots_to_keys 跳跃表中记录各个数据库键所属的槽,节点可以很方便地对属于某个或某些槽的所有数据库键进行批量操作,例如命令 CLUSTER GETKEYSINSLOT <slot> <count> 命令可以返回最多 count 个属于槽 slot 的数据库键,而这个命令就是通过遍历 slots_to_keys 跳跃表来实现的

五、重新分片

Redis 集群的重新分片操作可以将任意数量已经指派给某个节点(源节点)的槽改为指派给另一个节点(目标节点),并且相关槽所属的键值对也会从源节点被移动到目标节点

重新分片操作可以在线进行,在重新分片的过程中,集群不需要下线,并且源节点和目标节点都可以继续 处理命令请求

举个例子,对于之前提到的,包含 7000、7001、7002 三个节点的集群来说,我们可以向这个集群添加一个 IP 为 127.0.0.1,端口号为 7003 的节点(后面简称节点 7003):

$ redis-cli -c -p 7000
127.0.0.1:7000>CLUSTER MEET 127.0.0.1 7003
OK127.0.0.1:7000>cluster nodes
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master -0 0 0 connected 0-5000
68eef66df23420a5862208ef6b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388635782831 0 connected 5001-10000
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388635782831 0 connected 10001-16383
04579925484ce537d3410d7ce97bd2e260c459a2 127.0.0.1:7002 master - 0 1388635782330 0 connected

然后通过重新分片操作,将原本指派给节点 7002 的槽 15001 至 16383 改为指派给节点 7003

以下是重新分片操作执行之后,节点的槽分配状态:

127.0.0.1:7000>cluster nodes
51549e625cfda318ad27423a31e7476fe3cd2939 :0 myself,master -0 0 0 connected 0-5000
68eef66df23420a5862208ef6b1a7005b806f2ff 127.0.0.1:7001 master - 0 1388635782831 0 connected 5001-10000
9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26 127.0.0.1:7002 master - 0 1388635782831 0 connected 10001-15000
04579925484ce537d3410d7ce97bd2e260c459a2 127.0.0.1:7002 master - 0 1388635782330 0 connected    15001-16383

重新分片的实现原理

Redis 集群的重新分片操作是由 Redis 集群管理软件 redis-cli 负责执行的,Redis 提供了进行重新分片所需的所有命令,而 redis-trib 则通过向源节点和目标节点发送命令来进行重新分片操作

redis-trib 对集群的单个槽slot 进行重新分片的步骤如下:

  1. redis-trib 对目标节点发送 CLUSTER SETSLOT <slot> IMPORTING <source_id> 命令,让目标节点准备好从源节点导入(import)属于槽 slot 的键值对
  2. redis-trib 对源节点发送 CLUSTER SETSLOT <slot> MIGRATING <target_id> 命令,让源节点准备好将属于槽 slot 的键值对迁移(migrate)至目标节点
  3. redis_trib 向源节点发送 CLUSTER GETKEYSINSLOT <slot> <count> 命令,获得最多 count 个属于槽 slot 的键值对的键名(key name)
  4. 对于步骤 3 获得的每个键名,redis-trib 都向源节点发送一个 MIGRATE <target_ip> <target_port> <key_name> 0 <timeout> 命令,将被选中的键原子地从源节点迁移至目标节点
  5. 重复执行步骤 3 和步骤 4,直到源节点保存的所有属于槽 slot 的键值对都被迁移至目标节点为止。每次迁移键的过程如图 17-24 所示
  6. redis-trib 向集群中的任意一个节点发送 CLUSTER SETSLOT <slot> NODE <target_id> 命令,将槽 slot 指派给目标节点,这一指派信息会通过消息发送至整个集群,最终集群中的所有节点都会知道槽 slot 已经指派给了目标节点

图 17-25 展示了对槽 slot 进行重新分片的整个过程

如果重新分片涉及多个槽,那么 redis-trib 将对每个给定的槽分别执行上面给出的步骤

六、ASK 错误

在进行重新分片期间,源节点向目标节点迁移一个槽的过程中,可能会出现这样一种情况:属于被迁移槽的一部分键值对保存在源节点里面,而另一部分键值对则保存在目标节点里面

当客户端向源节点发送一个与数据库键有关的命令,并且命令要处理的数据库键恰好就属于真在被迁移的槽时:

  • 源节点会先在自己的数据库里面查找指定的键,如果找到的话,就直接执行客户端发送的命令
  • 相反地,如果源节点没能在自己的数据库里面找到指定的键,那么这个键有可能已经被迁移到了目标节点,源节点将向客户端返回一个 ASK 错误,指引客户端转向正在导入槽的目标节点,并再次发送之前想要执行的命令

图 17-26 展示了源节点判断是否需要向客户端发送 ASK 错误的整个过程

举个例子,假设节点 7002 正在向节点 7003 迁移槽 16198,这个槽包含 “is” 和 “love” 两个键,其中键 “is” 还留在节点 7002,而键 “love” 已经被迁移到了节点 7003

如果我们向节点 7002 发送关于键盘 “is” 的命令,那么这个命令会直接被节点 7002 执行;

127.0.0.1:7002>GET "is"
"you get the key 'si'"

而如果我们向节点 7002 发送关于键盘 “love” 的命令,那么客户端会先被转向至节点 7003,然后再次执行命令:

127.0.0.1:7002>GET "love"
-> Redirected to slot [16198] located at 127.0.0.1:7003
"you get the key 'love'"127.0.0.1:7003>

被隐藏的 ASK 错误

和接到 MOVED 错误时的情况类似,集群模式的 redis-cli 在接到 ASK 错误时也不会打印错误,而是自动根据错误提供的 IP 地址和端口进行转向动作。如果想看到节点发送的 ASK 错误的话,可以使用单机模式的 redis-cli 客户端;

$ redis-cli -p 7002
127.0.0.1:7002>GET "love"
(error) ASK 16198 127.0.0.1:7003

1.CLUSTER SETSLOT IMPORTING 命令的实现

clusterState 结构的 importing_slots_from 数组记录了当前节点正在从其他节点导入的槽:

typedef struct clusterState{//...clusterNode *importing_slots_from[16384];//...
}clusterState;

如果 importing_slots_from[i] 的值不为 NULL,而是指向一个 clusterNode 结构,那么表示当前节点正在从 clusterNode 所代表的节点导入槽 i

在对集群进行重新分片的时候,向目标节点发送命令:

CLUSTER SETSLOT <i> IMPORTING <source_id>

可以将目标节点 clusterState.importing_slots_from[i] 的值设置为 source_id 所代表节点的 clusterNode 结构

举个例子,如果客户端向节点 7003 发送以下命令:

#9df... 是节点 7002 的 ID
127.0.0.1:7003>CLUSTER SETSLOT 16198 IMPORTING 9dfb4c4e016e627d9769e4c9bb0d4fa208e65c26
OK

那么节点 7003 的 clusterState.importing_slots_from 数组将变成图 17-27 所示的样子

2.CLUSTER SETSLOT MIGRATING 命令的实现

clusterState 结构的 migrating_slots_to 数组记录了当前节点正在迁移至其他节点的槽:

typedef struct clusterState{//...clusterNode *migrating_slots_to[16384];//...
}clusterState;

如果 migrating_slots_to[i] 的值不为 NULL,而是指向一个 clusterNode 结构,那么表示当前节点正在将槽 i 迁移至 clusterNode 所代表的节点

在对集群进行重新分片的时候,向源节点发送命令:

CLUSTER SETSLOT <i> MIGRATING <target_id>

可以将源节点 clusterState.migrating_slots_to[i] 的值设置为 target_id 所代表的节点的 clusterNode 结构

举个例子,如果客户端向节点 7002 发送以下命令:

#0457... 是节点 7003 的 ID
127.0.0.1:7002>CLUSTER SETSLOT 16198 IMPORTING 04579925484ce537d3410d7ce97bd2e260c459a2
OK

那么节点 7002 的 clusterState.migrating_slots_to 数组将变成图 17-28 所示的样子

3.ASK 错误

如果节点收到一个关于键 key 的命令请求,并且键 key 所属的槽 i 正好就指派给了这个节点,那么节点会尝试在自己的数据库里查找键 key,如果找到了的话,节点就直接执行客户端发送的命令

与此相反,如果节点没有在自己的数据库里面找到键 key,那么节点就会检查自己的 clusterState.migrating_slots_to[i],看键 key 所属的槽 i 是否正在进行迁移,如果槽 i 的确在进行迁移的话,那么节点会向客户端发送一个 ASK 错误,引导客户端到正在导入槽 i 的节点去查找键 key

举个例子,假设在节点 7002 向节点 7003 迁移槽 16198 期间,有一个客户端向节点 7002发送命令:

GET "love"

因为键 “love” 正好属于槽 16198,所以节点 7002 会首先在自己的数据库中查找键 “love”,但并没有找到,通过检查自己的 clusterState.migrating_slots_to[16198],节点 7002 发现自己正在将槽 16198 迁移至节点 7003,于是它向客户端返回错误:

ASK 16198 127.0.0.1:7003

这个错误表示客户端可以尝试到 IP 为 127.0.0.1,端口号为 7003 的节点去执行和槽 16198 有关的操作,如图 17-29 所示

接到 ASK 错误的客户端会根据错误提供的 IP 地址和端口号,转向至正在导入槽的目标节点,然后首先向目标节点发送一个 ASKING 命令,之后再重新发送原本想要执行的命令

以前面的例子来说,当客户端收到节点 7002 返回的以下错误时:

ASK 16198 127.0.0.1:7003

客户端会转向至节点 7003,首先发送命令:

ASKING

然后再次发送命令:

GET "love"

并获得回复:

"you get the key 'lvoe'"

整个过程如图 17-30 所示

4.ASKING 命令

ASKING 命令唯一要做的就是打开发送该命令的客户端的 REDIS_ASKING 标识,以下是该命令的伪代码实现:

def ASKING():#打开标识client.flags |= REDIS_ASKING#向客户端返回 OK 回复reply("OK")

在一般情况下,如果客户端向节点发送一个关于槽 i 的命令,而槽 i 又没有指派给这个节点的话,那么节点将向客户端返回一个 MOVED 错误;但是,如果节点的 clusterState.importing_slots_from[i] 显示节点正在导入槽 i,并且发送命令的客户端带有 REDIS_ASKING 标识,那么节点将破例执行这个关于槽 i 的命令以此,图 17-31 展示了这个判断过程

当客户端接收到 ASK 错误并转向至正在导入槽的节点时,客户端会先向节点发送一个 ASKING 命令,然后才重新发送想要执行的命令的话,那么客户端发送的命令将被节点拒绝执行,并返回 MOVED 错误

举个例子,我们可以使用普通模式的 redis-cli 客户端,向正在导入槽 16198 的节点 7003 发送以下命令

$ ./redis-cli -p 7003127.0.0.1:7003>GET "love"
(error) MOVED 16198 127.0.0.1:7002

虽然节点 7003 正在导入 16198,但槽 16198 目前仍然是指派给了节点 7002,所以节点 7003 会向客户端返回 MOVED 错误,指引客户端转向至节点 7002

但是,如果我们在发送 GET 命令之前,先向节点发送一个 ASKING 命令,那么这个 GET 命令就会被节点 7003 执行

127.0.0.1:7003>ASKING
OK127.0.0.1:7003>GET "love"
"you get the key 'love'"

另外要注意的是,客户端的 REDIS_ASKING 标识是一个一次性标识,当节点执行了一个带有 REDIS_ASKING 标识的客户端发送的命令之后,客户端的 REDIS_ASKING 表示就会被移除

举个例子,如果我们在成功执行 GET 命令之后,再次向节点 7003 发送 GET 命令,那么第二次发送的 GET 命令将执行失败,因为这时客户端的 REDIS_ASKING 标识已经被移除

127.0.0.1:7003>ASKING         #打开 REDIS_ASKING 标识
OK127.0.0.1:7003>GET "love"        #移除 REDIS_ASKING 标识
"you get the key 'love'"127.0.0.1:7003>GET "love"      #REDIS_ASKING 标识未打开,执行失败
(error) MOVED 16198 127.0.0.1:7002

5.ASK 错误和 MOVED 错误的区别

ASK 错误和 MOVED 错误都会导致客户端转向,它们的区别在于:

  • MOVED 错误代表槽的负责权已经从一个节点转移到了另一个节点:在客户端收到关于槽 i 的 MOVED 错误之后,客户端每次遇到关于槽 i 的命令请求时,都可以直接将命令请求发送至 MOVED 错误所指向的节点,因为该节点就是目前负责槽 i 的节点
  • 与此相反,ASK 错误只是两个节点在迁移槽的过程中使用的一种临时措施:在客户端收到关于槽 i 的 ASK 错误之后,客户端只会将关于

《Reids 设计与实现》第十五章 集群(中)相关推荐

  1. 《Reids 设计与实现》第十六章 集群(下)

    <Reids 设计与实现>第十六章 集群(下) 文章目录 <Reids 设计与实现>第十六章 集群(下) 七.复制与故障转移 1.设置从节点 2.故障检测 3.故障转移 4.选 ...

  2. 《Reids 设计与实现》第五章 对象

    <Reids 设计与实现>第五章 对象 文章目录 <Reids 设计与实现>第五章 对象 一.简介 二.对象的类型与编码 1.类型 2.编码和底层实现 三.字符串对象 1.编码 ...

  3. 《Reids 设计与实现》第十四章 集群(上)

    <Reids 设计与实现>第十四章 集群(上) 文章目录 <Reids 设计与实现>第十四章 集群(上) 一.简介 二.节点 1.启动节点 2.集群数据结构 3.CLUSTER ...

  4. Visual C++ 2008入门经典 第十五章 在窗口中绘图

    /*第十五章 在窗口中绘图 主要内容: 1 Windows为窗口绘图提供的坐标系统 2 设置环境及其必要性 3 程序如何以及在窗口中绘图 4 如何定义鼠标消息的处理程序 5 如何定义自己的形状类 6 ...

  5. Redis设计与实现 笔记 第十七章 集群 cluster

    集群 Redis 集群是 Redis 提供的分布式数据库方案,集群通过分片来进行数据共享,并提供复制和故障转移功能 17.1 节点 一个 Redis 集群通常由多个节点组成,在刚开始的时候,每个节点都 ...

  6. dubbo源码解析(三十五)集群——cluster

    集群--cluster 目标:介绍dubbo中集群容错的几种模式,介绍dubbo-cluster下support包的源码. 前言 集群容错还是很好理解的,就是当你调用失败的时候所作出的措施.先来看看有 ...

  7. Hazelcast IMDG参考中文版手册-第五章-集群设置

    本章介绍Hazelcast集群以及集群成员和本机客户端用于构成Hazelcast集群的方法. 5.1.发现机制 Hazelcast集群是运行Hazelcast的集群成员网络.集群成员(也称为节点)自动 ...

  8. 第十五章 五虎上将中谁最冷血

    五虎上将中谁最冷血 笔者有一个发现:名字为"平"或"萍"的人中,搞艺术的数量相当可观,例子多得顺手就可拈来,如:作曲家王立平.赵季平,漫画家张乐平,导演何平,相 ...

  9. 奋斗吧,程序员——第十五章 去年今日此门中,人面桃花相映红

    元宵节从豫园飞出来的孔明灯,使得摩肩接踵的人群里时不时有人抬头发出这样的感叹. 那时候的蜀汉丞相诸葛亮,在他统一大业注定无法实现的北伐途中,在他"二将军三将军,望二位将军在天之灵,暗助亮一臂 ...

最新文章

  1. 如何起一个好的学术期刊论文题目?
  2. OpenCV 3最新模块介绍
  3. 【机器学习基础】理解关联规则算法
  4. selenium 页面经常改变元素_selenium 总结篇,常见方法和页面元素的操作
  5. Android添加垂直滚动scrollview
  6. eclipse 全屏插件
  7. Ubuntu16版本安装截图软件Flameshot
  8. android oat如何提取dex文件字节码,Android: 使用oatdump反编译oat文件
  9. 作者:张国惠(1978-),男,美国新墨西哥大学土木工程系助理教授、博士生导师。...
  10. AI一周热闻:北大建立人工智能新校区;英国首例机器人心脏手术致死
  11. HTTP协议详细介绍~超详细
  12. windows清除记住的密码
  13. python如何运行_家长看的懂的Python编程---电脑要如何运行Python?
  14. ArcFace算法笔记
  15. 小米怎么快速回到顶部_拆解报告:小米小爱鼠标采用炬芯ATB110X蓝牙物联网方案 -...
  16. MySQL之filed函数
  17. Docker 容器安装监控软件 cAdvisor
  18. VMware扩展Ubuntu分区容量大小
  19. OpenCV每日函数 图像过滤模块 (6) erode腐蚀函数
  20. 【信息安全】RSA非对称加密算法原理(详解和C++代码实现)

热门文章

  1. 反射parameter field_Java反射有多强?这5大神奇功能,你需要了解!
  2. 简述python常用的函数模块_Python学习笔记(十三)—函数常用模块
  3. c++ 测试串口速率_山西充放电测试设备实现多台仪器准确通
  4. 《C++ Primer Plus》16.2 智能指针模板类
  5. Asp.net MVC 4 Attributes特性
  6. php微信段子,年度挤进前十名的微信段子,笑死了
  7. Spark源码分析之TaskSetManager分析
  8. java ajax简单实例_JAVA编写的AJAX例子,很简单,但是很容易理解详解
  9. 怎么更改wifi频段_手机wifi延迟高怎么办
  10. (18)ISE14.7调试核名称与顶层名称不一致导致生成bit报error(FPGA不积跬步101)