本项目地址:LMQ
LMQ 是一个仿照 NSQ 的基于 TCP 协议的分布式消息队列,本系列说明 LMQ 的实现方式。
在本节说明 TCP 服务器的实现,TCP 服务器的实现地址:Hamble TCP Server
TCP 服务器架构
TCP 服务器的架构如下,总体上采用 1+M+N 的服务器编程模型:
- 一个协程(Server goroutine)用于建立连接(Accept)。
- M 个协程用于读写取连接请求信息。
- N 个协程用于处理请求内容,并生成响应信息。
本项目地址:LMQ
LMQ 是一个仿照 NSQ 的基于 TCP 协议的分布式消息队列,本系列说明 LMQ 的实现方式。
在本节说明 TCP 服务器的实现,TCP 服务器的实现地址:Hamble TCP Server
TCP 服务器的架构如下,总体上采用 1+M+N 的服务器编程模型:
本项目完整地址 simple-redis
Simple-Redis 是一个 golang 编写的实现了 RESP(REdis Serialization Protocol)协议的简易 Redis,实现的功能包括:
相比于 Redis,Simple-Redis 的优势在于:
1 | zhaohan08@MacBook-Pro redis % redis-benchmark -t set,get,incr,lpush,rpush,lpop,rpop,sadd,hset,spop,zadd,lrange -n 10000 -q |
在单机时,可以得出结论,Simple-Redis 和 Redis 是有性能差距的(达到 Redis 的 60-70%),具体数据如下:
本项目完整地址 simple-redis
Simple-Redis 是支持分布式事务的,采用 TCC(Try-Commit-Cancel)实现分布式事务,以当前节点为事务协调者,控制分布在其他节点的本地事务。所以 Simple-Redis 的分布式事务实现包括两个部分:
TCC 事务协调者定义如下:
1 | // Coordinator TCC事务协调者 |
一次分布式事务的流程如下:
1 | // ExecTx 执行TCC分布式事务 |
try 命令比较复杂,分为几个子命令:
1 | func (coordinator *Coordinator) sendTry(peer string, cmdLines [][][]byte) redis.Reply { |
本地事务就是分布式事务在节点上的实际执行者,下面是本地事务的定义:
1 | // Transaction TCC本地事务 |
在收到 try end 命令后,表明 try 阶段已经结束,此时本地事务:
1 | // Try TCC事务的结束try阶段,锁定key |
在本地事务收到 commit 命令后,会执行命令,(如果开启原子事务)并记录 undolog。
1 | // Commit TCC事务的commit阶段 |
在收到 cancel 命令后,(如果开启原子性事务)会执行 undo log 进行回滚。
1 | // Cancel TCC事务的cancel阶段 |
本项目完整地址 simple-redis
Redis 不支持原子事务,在 multi 排队执行时,如果在入队时发生错误则放弃执行,如果在 exec 执行时错误则跳过这条命令。
Simple-Redis 支持原子事务,在 multi 排队执行时,如果在入队时发生错误则放弃执行,在 exec 执行时错误则整条命令回滚。
在数据库存储引擎层,使用了并发的 dict 记录 key 和版本号的映射关系,当写命令执行成功时版本号加一。
在 Redis 中 watch 命令用于记录一个 key 当前的版本号,将版本号记录在客户端连接的上下文中。
原子事务的实现是在数据库引擎层实现的,在 multi 后客户端发出的每一条命令,数据库只会检查语法错误并将命令记录在客户端连接的上下文中。
在 exec 时,才会从客户端连接的上下文中读取所有需要执行的命令队列。exec 命令的实现方式如下:
本项目完整地址 simple-redis
Simple-Redis 的集群本质上就是一个分片集群,使用一致性哈希环实现。
Cluster 结构体如下,其中:
以下三个属性与分布式事务相关:
1 | // Cluster 用于和集群中的主机进行交互 |
在和远程节点通信时,本节点就作为一个 Client。而一个 getter 维护一个远程节点的连接池(实际上一个 getter 维护多个连接池,一个数据库对应一个连接池。
集群中节点之间的通信采用连接池,是为了:
- 复用连接(复用空闲连接)。
- 增加并发性能(节点之间有多个连接)。
- 控制节点之间的连接数,不会因为节点之间的通信而影响客户端与节点之间的通信(有最大空闲连接数和最大活跃连接数的限制)。
本项目完整地址 simple-redis
使用的机器为 MacBook Pro 13,处理器 Intel i5 四核,内存 8GB。
在同样的测试环境上,使用 redis-benchmark分别对 Redis 和 Simple-Redis 进行性能测试。
发送一万条请求,对 set,get,incr,lpush,rpush,lpop,rpop,sadd,hset,spop,zadd,lrange
命令进行测试。
Redis 测试结果如下:
1 | zhaohan08@MacBook-Pro redis % redis-benchmark -t set,get,incr,lpush,rpush,lpop,rpop,sadd,hset,spop,zadd,lrange -n 10000 -q |
Simple-Redis 测试结果如下:
1 | zhaohan08@MacBook-Pro redis % redis-benchmark -t set,get,incr,lpush,rpush,lpop,rpop,sadd,hset,spop,zadd,lrange -n 10000 -q |
在单机时,可以得出结论,Simple-Redis 和 Redis 是有性能差距的,具体数据如下:
本项目完整地址 simple-redis
本节说明 simple-redis 中的 AOF 持久化功能。AOF(append only file)是一种 Redis 持久化方式。,其优缺点总结如下:
Persister 是 AOF 持久化中的核心数据结构,它从 channel 中接收消息并且将消息写入到 AOF 文件中。其中重要的字段如下:
1 | const ( |
payload 包含命令、数据库编号两个字段,它表示发送给 aofChan 的数据。
1 | type payload struct { |
本项目完整地址 simple-redis
在上一节中,我们简述了 simple-redis 的工作方式,需要注意的是如 GET、SET 这样需要在某个具体的数据库中执行的命令,单机模式下 Server 会调用 Server.db.Exec 去执行这类命令。
本节我们就聊一聊 simple-redis 的底层数据库,simple-redis 的底层数据库定义在 database/engine 文件夹中。
本项目完整地址 simple-redis
在之前已经介绍了 TCP 服务器,本节介绍 Simple-Redis 服务器,这是一个应用层服务器。在 Handler 的 Handle 方法中,有这样一条命令 result := h.db.Exec(client, r.Args)
,它将收到的命令交给 simple-redis 服务器去执行。
simple-redis 服务器的被定义在 database/server.go 文件中,simple-redis 服务器的相关代码在 database 文件夹下。
simple-redis 服务器的数据结构如下,需要说明的是:
1 | type Server struct { |
AOF 持久化、集群、发布订阅会在后面的章节中说明。
本项目完整地址 simple-redis
Godis 中定义了 List 接口,以定义 List 的各种操作。
1 | // Expected check whether given item is equals to expected value |
在 godis 中,采用快速链表作为 List 的数据结构。
快速链表实际上就是一个双向链表,但是双向链表中的每一个节点不是存储一个数据,而是将数据连续存放形成一段连续的存储空间作为链表的节点。
这一段连续的存储空间在 godis 中被称为 page(每一个 page 的大小为 1024),page 的类型为空接口切片。
1 | // pageSize must be even |