mirror of
https://github.com/chai2010/advanced-go-programming-book.git
synced 2025-05-24 04:22:22 +00:00
ch6 图片增加编号
This commit is contained in:
parent
2dd023101c
commit
217820254c
@ -4,10 +4,12 @@
|
||||
|
||||
在插入数据库之前,我们需要给这些消息/订单先打上一个ID,然后再插入到我们的数据库。对这个id的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序。
|
||||
|
||||
Twitter的snowflake算法是这种场景下的一个典型解法。先来看看snowflake是怎么一回事:
|
||||
Twitter的snowflake算法是这种场景下的一个典型解法。先来看看snowflake是怎么一回事,见*图 6-1*:
|
||||
|
||||

|
||||
|
||||
*图 6-1 snowflake中的比特位分布*
|
||||
|
||||
首先确定我们的数值是64 位,int64类型,被划分为四部分,不含开头的第一个bit,因为这个bit是符号位。用41位来表示收到请求时的时间戳,单位为毫秒,然后五位来表示数据中心的id,然后再五位来表示机器的实例id,最后是12位的循环自增id(到达 1111 1111 1111 后会归 0)。
|
||||
|
||||
这样的机制可以支持我们在同一台机器上,同一毫秒内产生`2 ^ 12 = 4096`条消息。一秒共409.6万条消息。从值域上来讲完全够用了。
|
||||
@ -49,6 +51,8 @@ mysql> select last_insert_id();
|
||||
|
||||

|
||||
|
||||
*图 6-2 snowflake库*
|
||||
|
||||
和标准的snowflake完全一致。使用上比较简单:
|
||||
|
||||
```go
|
||||
@ -101,10 +105,12 @@ Epoch 就是本节开头讲的起始时间,NodeBits指的是机器编号的位
|
||||
|
||||
### 6.1.2.2 sonyflake
|
||||
|
||||
sonyflake是Sony公司的一个开源项目,基本思路和snowflake差不多,不过位分配上稍有不同:
|
||||
sonyflake是Sony公司的一个开源项目,基本思路和snowflake差不多,不过位分配上稍有不同,见*图 6-2*:
|
||||
|
||||

|
||||
|
||||
*图 6-3 sonyflake*
|
||||
|
||||
这里的时间只用了39个bit,但时间的单位变成了10ms,所以理论上比41位表示的时间还要久(174 years)。
|
||||
|
||||
`Sequence ID`和之前的定义一致,`Machine ID`其实就是节点id。`sonyflake`与众不同的地方在于其在启动阶段的配置参数:
|
||||
|
@ -17,10 +17,12 @@ timer的实现在工业界已经是有解的问题了。常见的就是时间堆
|
||||
|
||||
### 6.3.1.1 时间堆
|
||||
|
||||
最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树:
|
||||
最常见的时间堆一般用小顶堆实现,小顶堆其实就是一种特殊的二叉树,见*图6-4*
|
||||
|
||||

|
||||
|
||||
*图 6-4 二叉堆结构*
|
||||
|
||||
小顶堆的好处是什么呢?实际上对于定时器来说,如果堆顶元素比当前的时间还要大,那么说明堆内所有元素都比当前时间大。进而说明这个时刻我们还没有必要对时间堆进行任何处理。所以对于定时check来说,时间复杂度是O(1)的。
|
||||
|
||||
当我们发现堆顶的元素小于当前时间时,那么说明可能已经有一批事件已经开始过期了,这时进行正常的弹出和堆调整操作就好。每一次堆调整的时间复杂度都是O(LgN)。
|
||||
@ -29,6 +31,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆
|
||||
|
||||

|
||||
|
||||
*图 6-5 四叉堆结构*
|
||||
|
||||
小顶堆的性质,父节点比其4个子节点都小,子节点之间没有特别的大小关系要求。
|
||||
|
||||
四叉堆中元素超时和堆调整与二叉堆没有什么本质区别。
|
||||
@ -37,6 +41,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆
|
||||
|
||||

|
||||
|
||||
*图 6-6 时间轮*
|
||||
|
||||
用时间轮来实现timer时,我们需要定义每一个格子的“刻度”,可以将时间轮想像成一个时钟,中心有秒针顺时针转动。每次转动到一个刻度时,我们就需要去查看该刻度挂载的tasklist是否有已经到期的任务。
|
||||
|
||||
从结构上来讲,时间轮和哈希表很相似,如果我们把哈希算法定义为:触发时间%时间轮元素大小。那么这就是一个简单的哈希表。在哈希冲突时,采用链表挂载哈希冲突的定时器。
|
||||
@ -51,6 +57,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆
|
||||
|
||||

|
||||
|
||||
*图 6-7 分布式任务分发*
|
||||
|
||||
每一个实例每隔一小时,会去数据库里把下一个小时需要处理的定时任务捞出来,捞取的时候只要取那些task_id % shard_count = shard_id的那些task即可。
|
||||
|
||||
当这些定时任务被触发之后需要通知用户侧,有两种思路:
|
||||
@ -70,6 +78,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆
|
||||
|
||||

|
||||
|
||||
*图 6-8 任务数据分布*
|
||||
|
||||
一份数据虽然有两个持有者,但持有者持有的副本会进行区分,比如持有的是主副本还是非主副本,主副本在图中为摸黑部分,非主副本为正常线条。
|
||||
|
||||
一个任务只会在持有主副本的节点上被执行。
|
||||
@ -78,6 +88,8 @@ Go自身的timer就是用时间堆来实现的,不过并没有使用二叉堆
|
||||
|
||||

|
||||
|
||||
*图 6-9 故障时数据分布*
|
||||
|
||||
节点1的数据会被迁移到节点2和节点3上。
|
||||
|
||||
当然,也可以用稍微复杂一些的思路,比如对集群中的节点进行角色划分,由协调节点来做这种故障时的任务重新分配工作,考虑到高可用,协调节点可能也需要有1 ~ 2个备用节点以防不测。
|
||||
|
@ -28,12 +28,16 @@ Elasticsearch是开源分布式搜索引擎的霸主,其依赖于Lucene实现
|
||||
|
||||

|
||||
|
||||
*图 6-10 倒排列表*
|
||||
|
||||
对Elasticsearch中的数据进行查询时,本质就是求多个排好序的序列求交集。非数值类型字段涉及到分词问题,大多数内部使用场景下,我们可以直接使用默认的bi-gram分词。什么是bi-gram分词呢:
|
||||
|
||||
即将所有Ti和T(i+1)组成一个词(在es中叫term),然后再编排其倒排列表,这样我们的倒排列表大概就是这样的:
|
||||
即将所有Ti和T(i+1)组成一个词(在Elasticsearch中叫term),然后再编排其倒排列表,这样我们的倒排列表大概就是这样的:
|
||||
|
||||

|
||||
|
||||
*图 6-11 “今天天气很好”的分词结果*
|
||||
|
||||
当用户搜索'天气很好'时,其实就是求:天气、气很、很好三组倒排列表的交集,但这里的相等判断逻辑有些特殊,用伪代码表示一下:
|
||||
|
||||
```go
|
||||
@ -345,6 +349,8 @@ SQL的where部分就是boolean expression。我们之前提到过,这种bool
|
||||
|
||||

|
||||
|
||||
*图 6-12 AST和DSL之间的对应关系*
|
||||
|
||||
既然结构上完全一致,逻辑上我们就可以相互转换。我们以广度优先对AST树进行遍历,然后将二元表达式转换成json字符串,再拼装起来就可以了,限于篇幅,本文中就不给出示例了,读者朋友可以查看:
|
||||
|
||||
> github.com/cch123/elasticsql
|
||||
@ -361,6 +367,8 @@ SQL的where部分就是boolean expression。我们之前提到过,这种bool
|
||||
|
||||

|
||||
|
||||
*图 6-13 基于时间戳的数据同步*
|
||||
|
||||
这种同步方式与业务强绑定,例如wms系统中的出库单,我们并不需要非常实时,稍微有延迟也可以接受,那么我们可以每分钟从MySQL的出库单表中,把最近十分钟创建的所有出库单取出,批量存入es中,具体的逻辑实际上就是一条SQL:
|
||||
|
||||
```sql
|
||||
@ -381,6 +389,8 @@ select * from wms_orders where update_time >= date_sub(
|
||||
|
||||

|
||||
|
||||
*图 6-13 基于binlog的数据同步*
|
||||
|
||||
业界使用较多的是阿里开源的Canal,来进行binlog解析与同步。canal会伪装成MySQL的从库,然后解析好行格式的binlog,再以更容易解析的格式(例如json)发送到消息队列。
|
||||
|
||||
由下游的Kafka消费者负责把上游数据表的自增主键作为es的document的id进行写入,这样可以保证每次接收到binlog时,对应id的数据都被覆盖更新为最新。MySQL的row格式的binlog会将每条记录的所有字段都提供给下游,所以实际上在向异构数据目标同步数据时,不需要考虑数据是插入还是更新,只要一律按id进行覆盖即可。
|
||||
|
@ -78,6 +78,8 @@ func main() {
|
||||
|
||||

|
||||
|
||||
*图 6-14 爬虫工作流程*
|
||||
|
||||
上游的主要工作是根据预先配置好的起点来爬取所有的目标“列表页”,列表页的html内容中会包含有所有详情页的链接。详情页的数量一般是列表页的10~100倍,所以我们将这些详情页链接作为“任务”内容,通过消息队列分发出去。
|
||||
|
||||
针对页面爬取来说,在执行时是否偶尔会有重复其实不太重要,因为任务结果是幂等的(这里我们只爬页面内容,不考虑评论部分)。
|
||||
@ -94,10 +96,14 @@ nats的服务端项目是gnatsd,客户端与gnatsd的通信方式为基于tcp
|
||||
|
||||

|
||||
|
||||
*图 6-15 nats协议中的pub*
|
||||
|
||||
以workers的queue从tasks subject订阅消息:
|
||||
|
||||

|
||||
|
||||
*图 6-16 nats协议中的sub*
|
||||
|
||||
其中的queue参数是可选的,如果希望在分布式的消费端进行任务的负载均衡,而不是所有人都收到同样的消息,那么就要给消费端指定相同的queue名字。
|
||||
|
||||
#### 基本消息生产
|
||||
|
Loading…
x
Reference in New Issue
Block a user