背景:
日常开发中,我们需要对系统中的各种数据使用ID唯一表示,
比如商户ID对应且仅对应一个商户,权益ID对应且仅对应一种权益,订单ID对应且仅对应一个订单。
简单来说,ID 就是数据的唯一标识
一般情况下,会使用数据库的自增主键作为数据ID,但是在大数量的情况下,我们往往会引入分布式、分库分表等手段来应对,
很明显对数据分库分表后我们依然需要有一个唯一ID来标识一条数据或消息,数据库的自增ID已经无法满足需求。
特点:
全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求
趋势递增/单调递增:在mysql的InnoDB引擎中使用的是聚簇索引,由于多数RDBMS使用B-TREE的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写性能,保证下一个ID大于上一个ID
为什么往数据库中插入数据时候要递增?效率高,为什么?不管mysql还是oracle,都是B-tree,一个是B+tree,一个是B*tree,
但是在数据存储时候,遵循一个原则:有序,叶子节点从左到右是由小到大,如果插入的数据不是递增的,例如刚才新增的是10,现在新增是60,等会插入的是20,70,30等
这样的数据,要维护树的平衡,以及'页的分裂与合并',因为需要很大成本,影响性能;
信息安全:如果ID是连续+1的,恶意用户的扒取工作就非常容易做了,例如今天中午下一单,记录下单号,明天中午再下一单,多次操作竞对可以直接知道我们一天的单量。所以在一些应用场景下需要ID无规则
方式:
1.UUID:
常见的生成id方式,利用程序生成。
UUID的标准形式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:d6874b49-d4f5-43e3-8fa8-fffbbb9e31ef。
优点:
非常简单,本地生成,代码方便,API调用方便。
性能非高。生成的id性能非常好,没有网络消耗,基本不会有性能问题。
全球唯一。在数据库迁移、系统数据合并、或者数据库变更的情况下,可以 从容应对
缺点:
存储成本高:UUID太长,16字节128位,通常以36长度的字符串表示,很多场景不适用。如果是海量数据库,就需要考虑存储量的问题。
不适用作为主键:ID作为主键时在特定的环境会存在一些问题,比如做DB主键的场景下,UUID就非常不适用。UUID往往是使用字符串存储,查询的效率比较低。
UUID是无序的:如果作为数据库主键,在InnoDB引擎下,UUID的无序性可能回引发数据位置频繁变动,严重影响性能。
2.雪花算法
这种方案大致来说是一种划分命名空间来生成ID的一种算法,Snowflake是Twitter开源的分布式ID生成算法, 这种方案把64-bit分别划分成多段,分开来标示机器、时间等。
在snowflake中64bit分别表示以下含义:
第 0 位: 符号位(标识正负),始终为 0,没有用,不用管。
第 1~41 位 :一共 41 位,用来表示时间戳,单位是毫秒,可以支撑 2 ^41
毫秒(约 69 年)
第 42~52 位 :一共 10 位,一般来说,前 5 位表示机房 ID,后 5 位表
示机器 ID(实际项目中可以根据实际情况调整),这样就可以区分不同集群/机
房的节点,这样就可以表示 32 个 IDC,每个 IDC 下可以有 32 台机器。
第 53~64 位 :一共 12 位,用来表示序列号。 序列号为自增值,代表单
台机器每毫秒能够产生的最大 ID 数(2^12 = 4096),也就是说单台机器每毫秒最
多可以生成 4096 个 唯一 ID。
优点:
单机上ID单调自增,毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
稳定性高,不依赖于数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
灵活方便,可以根据自身业务特性分配bit位。
缺点:
强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。
还可以利用外部中间件,比如 Mongdb objectID,它也可以算作是和 snowflake 类似方法,通过“时间+机器码+pid+inc”共 12 个字节,
通过 4+3+2+3 的方式最终标识成一个 24 长度的十六进制字符。
3.数据库
以 MySQL 举例,
1.创建一个数据库表。
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;stub 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性。
2.通过 replace into 来插入数据。
BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;
插入数据这里,我们没有使用 insert into 而是使用 replace into 来插入数据。replace是insert的增强版,replace into首先尝试插入数据到表中,
1. 如果发现表中已经有此行数据(根据主键或者唯一索引判断)则先删除此行数据,然后插
入新的数据。 2. 否则,直接插入新数据。
优点:
非常简单,利用现有数据库系统的功能实现,成本小,有 DBA 专业维护。
ID号单调自增,存储消耗空间小。
缺点:
支持的并发量不大、存在数据库单点问题(可以使用数据库集群解决,不过增加了复杂度)、
ID没有具体业务含义、
安全问题比如上面提到的订单问题、
每次获取 ID 都要访问一次数据库(增加了对数据库的压力,获取速度也慢)
4.leaf实现分布式唯一ID
Leaf-segment 数据库方案
在使用数据库的方案上,做了如下改变:
原MySQL方案每次获取ID都得读写一次数据库> 改为批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大减轻db的压力。
各个业务不同的发号需求用biz_tag字段来区分,每个 biz-tag的ID获取相互隔离,互不影响。
表结构字段:biz_tag,max_id,step,description,update_time;
可能出现'尖刺'现象,系统最大好事取决于更新DB号段的时间
更新DB号段时,若DB宕机或发生主从切换,会导致一段时间的服务不可用
解决:Leaf双Buffer优化
Leaf动态调整step优化
优点:
Leaf服务可以很方便的线性扩展,性能完全能够支撑大多数业务场景。
容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服务。
缺点:
ID 号码不够随机,能够泄露发号数量的信息,不太安全。
DB 宕机会造成整个系统不可用
Leaf-snowflake方案
Leaf-snowflake不同于原始snowflake算法地方,主要是在workId的生成上,Leaf-snowflake以靠Zookeeper生成的workId,也就是上边的机器ID(占5bit)+机房ID(占5bit)
Leaf中workId是基于Zookeeper的顺序Id来生成的,每个应用在使用Leaf-snowflake时,启动时都会在Zookeeper中生成一个顺序id,相当于一台机器对应一个顺序节点,也就是一个workId
美团怎么解决时钟回拨问题?
新增或者宕机的机器重新回到服务中时候会和其他几台机器的时间进行比较,看是否有回拨现象,决定能否重启成功;
优点:
ID号码是趋势递增的8 byte的64位数字,满足上述数据库存储的主键要求。
缺点:
依赖ZooKeeper,存在服务不可用风险
参考文章:
https://blog.csdn.net/jiaomubai/article/details/124385324
https://blog.csdn.net/zhiyikeji/article/details/124026952
https://blog.csdn.net/sayoko06/article/details/122664856