From 903b12bf209a2339205d828c5c835ae3a70f85cd Mon Sep 17 00:00:00 2001 From: Xargin Date: Sun, 23 Dec 2018 18:55:21 +0800 Subject: [PATCH] fix 6-1 --- ch6-cloud/ch6-01-dist-id.md | 79 +++++++++++----------------------- images/ch6-snowflake-easy.png | Bin 0 -> 5631 bytes images/ch6-snowflake.png | Bin 0 -> 13539 bytes images/ch6-snoyflake.png | Bin 0 -> 5671 bytes 4 files changed, 25 insertions(+), 54 deletions(-) create mode 100644 images/ch6-snowflake-easy.png create mode 100644 images/ch6-snowflake.png create mode 100644 images/ch6-snoyflake.png diff --git a/ch6-cloud/ch6-01-dist-id.md b/ch6-cloud/ch6-01-dist-id.md index e013d04..2451490 100644 --- a/ch6-cloud/ch6-01-dist-id.md +++ b/ch6-cloud/ch6-01-dist-id.md @@ -1,47 +1,26 @@ # 6.1 分布式 id 生成器 -有时我们需要能够生成类似 MySQL 自增 ID 这样不断增大,同时又不会重复的 id。以支持业务中的高并发场景。比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒 10w+。明星出轨时,会有大量热情的粉丝发微博以表心意,同样会在短时间内产生大量的消息。 +有时我们需要能够生成类似MySQL自增ID这样不断增大,同时又不会重复的id。以支持业务中的高并发场景。比较典型的,电商促销时,短时间内会有大量的订单涌入到系统,比如每秒10w+。明星出轨时,会有大量热情的粉丝发微博以表心意,同样会在短时间内产生大量的消息。 -在插入数据库之前,我们需要给这些消息/订单先打上一个 ID,然后再插入到我们的数据库。对这个 id 的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序。 +在插入数据库之前,我们需要给这些消息/订单先打上一个ID,然后再插入到我们的数据库。对这个id的要求是希望其中能带有一些时间信息,这样即使我们后端的系统对消息进行了分库分表,也能够以时间顺序对这些消息进行排序。 -Twitter 的 snowflake 算法是这种场景下的一个典型解法。先来看看 snowflake 是怎么一回事: +Twitter的snowflake算法是这种场景下的一个典型解法。先来看看snowflake是怎么一回事: -``` - datacenter_id sequence_id - unused - │ │ - │ │ │ - │ │ │ - │ │ │ │ │ - │ │ │ │ │ - ▼ │◀────────────────── 41 bits ────────────────────▶│ ▼ ▼ - ┌─────┼──────────────────────────────────────────────────────┼────────┬────────┬────────────────┐ - │ 0 │ 0000 0000 0000 0000 0000 0000 0000 0000 0000 0000 0 │ 00000 │ 00000 │ 0000 0000 0000 │ - └─────┴──────────────────────────────────────────────────────┴────────┴────────┴────────────────┘ - ▲ ▲ - │ │ - │ │ - │ │ - │ │ - │ │ - │ │ +![snowflake](../images/ch6-snowflake.png) - time in milliseconds worker_id -``` +首先确定我们的数值是64 位,int64类型,被划分为四部分,不含开头的第一个bit,因为这个bit是符号位。用41位来表示收到请求时的时间戳,单位为毫秒,然后五位来表示数据中心的id,然后再五位来表示机器的实例id,最后是12位的循环自增id(到达 1111 1111 1111 后会归 0)。 -首先确定我们的数值是 64 位,int64 类型,被划分为四部分,不含开头的第一个 bit,因为这个 bit 是符号位。用 41 位来表示收到请求时的时间戳,单位为毫秒,然后五位来表示数据中心的 id,然后再五位来表示机器的实例 id,最后是 12 位的循环自增 id(到达 1111 1111 1111 后会归 0)。 +这样的机制可以支持我们在同一台机器上,同一毫秒内产生`2 ^ 12 = 4096`条消息。一秒共409.6万条消息。从值域上来讲完全够用了。 -这样的机制可以支持我们在同一台机器上,同一毫秒内产生 2 ^ 12 = 4096 条消息。一秒共 409.6w 条消息。从值域上来讲完全够用了。 +数据中心 + 实例id共有10位,可以支持我们每数据中心部署32台机器,所有数据中心共1024台实例。 -数据中心 + 实例 id 共有 10 位,可以支持我们每数据中心部署 32 台机器,所有数据中心共 1024 台实例。 +表示timestamp的41位,可以支持我们使用69年。当然,我们的时间毫秒计数不会真的从1970年开始记,那样我们的系统跑到`2039/9/7 23:47:35`就不能用了,所以这里的timestamp实际上只是相对于某个时间的增量,比如我们的系统上线是2018-08-01,那么我们可以把这个timestamp当作是从`2018-08-01 00:00:00.000`的偏移量。 -表示 timestamp 的 41 位,可以支持我们使用 69 年。当然,我们的时间毫秒计数不会真的从 1970 年开始记,那样我们的系统跑到 `2039/9/7 23:47:35` 就不能用了,所以这里的 timestamp 实际上只是相对于某个时间的增量,比如我们的系统上线是 2018-08-01,那么我们可以把这个 timestamp 当作是从 `2018-08-01 00:00:00.000` 的偏移量。 +## 6.1.1 worker_id分配 -## 6.1.1 worker_id 分配 +timestamp,datacenter_id,worker_id和sequence_id这四个字段中,timestamp和 sequence_id是由程序在运行期生成的。但datacenter_id和worker_id需要我们在部署阶段就能够获取得到,并且一旦程序启动之后,就是不可更改的了(想想,如果可以随意更改,可能被不慎修改,造成最终生成的id有冲突)。 -timestamp,datacenter_id,worker_id 和 sequence_id 这四个字段中,timestamp 和 sequence_id 是由程序在运行期生成的。但 datacenter_id 和 worker_id 需要我们在部署阶段就能够获取得到,并且一旦程序启动之后,就是不可更改的了(想想,如果可以随意更改,可能被不慎修改,造成最终生成的 id 有冲突)。 - -一般不同数据中心的机器,会提供对应的获取数据中心id的API,所以 datacenter_id 我们可以在部署阶段轻松地获取到。而 worker_id 是我们逻辑上给机器分配的一个 id,这个要怎么办呢?比较简单的想法是由能够提供这种自增 id 功能的工具来支持,比如 MySQL: +一般不同数据中心的机器,会提供对应的获取数据中心id的API,所以datacenter_id我们可以在部署阶段轻松地获取到。而worker_id是我们逻辑上给机器分配的一个id,这个要怎么办呢?比较简单的想法是由能够提供这种自增id功能的工具来支持,比如MySQL: ```shell mysql> insert into a (ip) values("10.1.2.101"); @@ -56,25 +35,21 @@ mysql> select last_insert_id(); 1 row in set (0.00 sec) ``` -从 MySQL 中获取到 worker_id 之后,就把这个 worker_id 直接持久化到本地,以避免每次上线时都需要获取新的 worker_id。让单实例的 worker_id 可以始终保持不变。 +从MySQL中获取到worker_id之后,就把这个worker_id直接持久化到本地,以避免每次上线时都需要获取新的worker_id。让单实例的worker_id可以始终保持不变。 -当然,使用 MySQL 相当于给我们简单的 id 生成服务增加了一个外部依赖。依赖越多,我们的服务的可运维性就越差。 +当然,使用MySQL相当于给我们简单的id生成服务增加了一个外部依赖。依赖越多,我们的服务的可运维性就越差。 -考虑到集群中即使有单个 id 生成服务的实例挂了,也就是损失一段时间的一部分 id,所以我们也可以更简单暴力一些,把 worker_id 直接写在 worker 的配置中,上线时,由部署脚本完成 worker_id 字段替换。 +考虑到集群中即使有单个id生成服务的实例挂了,也就是损失一段时间的一部分id,所以我们也可以更简单暴力一些,把worker_id直接写在worker的配置中,上线时,由部署脚本完成worker_id字段替换。 ## 6.1.2 开源实例 ### 6.1.2.1 标准 snowflake 实现 -`github.com/bwmarrin/snowflake` 是一个相当轻量化的 snowflake 的 Go 实现。其文档指出: +`github.com/bwmarrin/snowflake` 是一个相当轻量化的snowflake的Go实现。其文档指出: -``` -+--------------------------------------------------------------------------+ -| 1 Bit Unused | 41 Bit Timestamp | 10 Bit NodeID | 12 Bit Sequence ID | -+--------------------------------------------------------------------------+ -``` +![ch6-snowflake-easy](../images/ch6-snowflake-easy.png) -和标准的 snowflake 完全一致。使用上比较简单: +和标准的snowflake完全一致。使用上比较简单: ```go package main @@ -122,21 +97,17 @@ func main() { StepBits uint8 = 12 ``` -Epoch 就是本节开头讲的起始时间,NodeBits 指的是机器编号的位长,StepBits 指的是自增序列的位长。 +Epoch 就是本节开头讲的起始时间,NodeBits指的是机器编号的位长,StepBits指的是自增序列的位长。 ### 6.1.2.2 sonyflake -sonyflake 是 Sony 公司的一个开源项目,基本思路和 snowflake 差不多,不过位分配上稍有不同: +sonyflake是Sony公司的一个开源项目,基本思路和snowflake差不多,不过位分配上稍有不同: -``` -+-----------------------------------------------------------------------------+ -| 1 Bit Unused | 39 Bit Timestamp | 8 Bit Sequence ID | 16 Bit Machine ID | -+-----------------------------------------------------------------------------+ -``` +![sonyflake](../images/ch6-snoyflake.png) -这里的时间只用了 39 个 bit,但时间的单位变成了 10ms,所以理论上比 41 位表示的时间还要久(174 years)。 +这里的时间只用了39个bit,但时间的单位变成了10ms,所以理论上比41位表示的时间还要久(174 years)。 -Sequence ID 和之前的定义一致,Machine ID 其实就是节点 id。sonyflake 与众不同的地方在于其在启动阶段的 setting 配置: +`Sequence ID`和之前的定义一致,`Machine ID`其实就是节点id。`sonyflake`与众不同的地方在于其在启动阶段的配置参数: ```go func NewSonyflake(st Settings) *Sonyflake @@ -152,11 +123,11 @@ type Settings struct { } ``` -StartTime 选项和我们之前的 Epoch 差不多,如果不设置的话,默认是从 `2014-09-01 00:00:00 +0000 UTC` 开始。 +StartTime选项和我们之前的Epoch差不多,如果不设置的话,默认是从`2014-09-01 00:00:00 +0000 UTC`开始。 -MachineID 可以由用户自定义的函数,如果用户不定义的话,会默认将本机 ip 的低 16 位作为 machine id。 +MachineID可以由用户自定义的函数,如果用户不定义的话,会默认将本机IP的低16位作为`machine id`。 -CheckMachineID 是由用户提供的检查 MachineID 是否冲突的函数。这里的设计还是比较巧妙的,如果有另外的中心化存储并支持检查重复的存储,那我们就可以按照自己的想法随意定制这个检查 MachineID 是否冲突的逻辑。如果公司有现成的 Redis 集群,那么我们可以很轻松地用 Redis 的 set 来检查冲突。 +CheckMachineID是由用户提供的检查MachineID是否冲突的函数。这里的设计还是比较巧妙的,如果有另外的中心化存储并支持检查重复的存储,那我们就可以按照自己的想法随意定制这个检查MachineID是否冲突的逻辑。如果公司有现成的Redis集群,那么我们可以很轻松地用Redis的set来检查冲突。 ```shell redis 127.0.0.1:6379> SADD base64_encoding_of_last16bits MzI0Mgo= diff --git a/images/ch6-snowflake-easy.png b/images/ch6-snowflake-easy.png new file mode 100644 index 0000000000000000000000000000000000000000..c2fa038deaaac6e9198da7c7755bcb962ac84e9c GIT binary patch literal 5631 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 literal 0 HcmV?d00001 diff --git a/images/ch6-snoyflake.png b/images/ch6-snoyflake.png new file mode 100644 index 0000000000000000000000000000000000000000..60de060011c9c6eb9cd0f23b618757855836dc82 GIT binary patch literal 5671 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