思考:唯一标识id如何确定

背景

        唯一标识,有的地方叫id,也有的地方叫编码。为了方便理解,以下统一称为id。无论在生活中,还是代码程序中都是随处可见的,对于一个人来说,手机号是唯一的,身份证号是唯一的,邮编是唯一的。交通方面,车牌号是唯一的,高铁的车次在一个程度上,也可以视为唯一的。当然火车或高铁上的车厢号,座位号也是一种标识。

        对于单体系统来说,主键ID可能会常用主键自动的方式进行设置,这种ID生成方法在单体项目是可行的,但是对于分布式系统,分库分表之后,就不适应了,比如订单表数据量太大了,分成了多个库,如果还采用数据库主键自增的方式,就会出现在不同库id一致的情况,虽然是不符合业务的

现代系统对于id要基本要求如下:

  • 全局唯一性:ID是作为唯一的标识,不能出现重复

  • 趋势递增:互联网比较喜欢MySQL数据库,而MySQL数据库默认使用InnoDB存储引擎,其使用的是聚集索引,使用有序的主键ID有利于保证写入的效率

  • 单调递增:保证下一个ID大于上一个ID,这种情况可以保证事务版本号,排序等特殊需求实现

  • 信息安全:前面说了ID要递增,但是最好不要连续,如果ID是连续的,容易被恶意爬取数据,指定一系列连续的,所以ID递增但是不规则是最好的

唯一标识生成方法的分类

对于唯一标识的设定通常有以下几种方式:

  1. 简单的递增值

递增值最符合人们直接感受的一种唯一标识生成方法,从1开始递增,如火车车厢号是递增的,楼层号是递增的。对于mysql,sqlsever数据库而言,id列也可以设为自增自动生成,。也遇见过使用时间戳来做唯一标识,其本质也是一种递增值。

递增值有很多优点,简单,高效。当然也有一些缺点,有些场景需要规定唯一标识的长度,若使用简单的递增明显不合适,此外对于两个表合并一张表的场景,简单的递增值就会出现重复标识的问题。

  1. 随机值

随机值优点就在一定程度上弥补了递增值的缺陷,如使用UUID,可以得出规定长度的唯一标识,此外,对于多个表合并到一张表的场景,随机值也可以hold住。但UUID 生成的是一个无序的字符串,对于 MySQL 推荐使用增长的数值类型值作为主键来说不适合。因为MySql的主键索引B+树是基于id来建立的,若id是无序的,则对构建B+树十分不友好。

  1. 复杂的递增值:具有含义的编码+递增值

一般通过具有含义的编码+递增值构成复杂的递增值,如身份证号,前六位代表地点,第7-14位是生日,后面是该地区当年当月当日的出生人数的编码号。此外,也有表示位置的编码,如C区666。也属于这一类。前面具有含义的编码一般称为前缀。

通过对比简单的递增值和随机值的优缺点,不难得出,如果生成了一个递增的,且全局唯一,长度固定的唯一标识不就好了嘛。既能发挥mysql主键索引的优势,又能保证唯一性。于是以这个为出发点,聪明的人便制定了一些算法。如雪花算法

可以看出雪花算法是将简单递增值和具有含义的编码结合起来,这样在一定程度上保证了递增性,也保证了全局唯一性。

相关算法

以下内容是抄自:8种分布式ID生成方案汇总

UUID

UUID (Universally Unique Identifier),通用唯一识别码的缩写。UUID的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例: 863e254b-ae34-4371-87da-204b71d46a7b。UUID理论上的总数为1632=2128,约等于3.4 x 10^38。

优点

  • 性能非常高,本地生成的,不依赖于网络

缺点

  • 不易存储,16 字节128位,36位长度的字符串

  • 信息不安全,基于MAC地址生成UUID的算法可能会造成MAC地址泄露,暴露使用者的位置

  • uuid的无序性可能会引起数据位置频繁变动,影响性能

数据库自增

在分布式环境也可以使用mysql的自增实现分布式ID的生成,如果分库分表了,当然不是简单的设置好auto_increment_increment和 auto_increment_offset 即可,在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器。

设置步长step为2,Server1的初始值为1(1,3,5,7,9,11…)、Server2的初始值为2(2,4,6,8,10…)。这是Flickr团队在2010年撰文介绍的一种主键生成策略(Ticket Servers: Distributed Unique Primary Keys on the Cheap)

假设有N台机器,step就要设置为N,如图进行设置:

这种方案看起来是可行的,但是如果要扩容,步长step等要重新设置,假如只有一台机器,步长就是1,比如1,2,3,4,5,6,这时候如果要进行扩容,就要重新设置,机器2可以挑一个偶数的数字,这个数字在扩容时间内,数据库自增要达不到这个数的,然后步长就是2,机器1要重新设置step为2,然后还是以一个奇数开始进行自增。这个过程看起来不是很杂,但是,如果机器很多的话,那就要花很多时间去维护重新设置

这种实现的缺陷:

  • ID没有了单调递增的特性,只能趋势递增,有些业务场景可能不符合

  • 数据库压力还是比较大,每次获取ID都需要读取数据库,只能通过多台机器提高稳定性和性能

号段模式

这种模式也是现在生成分布式ID的一种方法,实现思路是会从数据库获取一个号段范围,比如[1,1000],生成1到1000的自增ID加载到内存中,建表结构如:

CREATE TABLE id_generator (  
id int(10) NOT NULL,  
max_id bigint(20) NOT NULL COMMENT '当前最大id',  
step int(20) NOT NULL COMMENT '号段的布长',  
biz_type int(20) NOT NULL COMMENT '业务类型',  
version int(20) NOT NULL COMMENT '版本号',  
PRIMARY KEY (`id`)
) 
  • biz_type :不同业务类型

  • max_id :当前最大的id

  • step :代表号段的步长

  • version :版本号,就像MVCC一样,可以理解为乐观锁

等ID都用了,再去数据库获取,然后更改最大值

update id_generator set max_id = #{max_id+step}, 
version = version + 1 
where 
version = # {version} 
and biz_type = XXX
  • 优点:有比较成熟的方案,

  • 缺点:依赖于数据库实现

Redis实现

Redis分布式ID实现主要是通过提供像INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性

这种实现方式,如果并发请求量上来后,就需要集群,不过集群后,又要和传统数据库一样,设置分段和步长

优缺点:

  • 优点:Redis性能相对比较好,又可以保证唯一性和有序性

  • 缺点:需要依赖Redis来实现,系统需要引进Redis组件

雪花算法(SnowFlake)

Snowflake,雪花算法是由Twitter开源的分布式ID生成算法,以划分命名空间的方式将64-bit位分割成多个部分,每个部分代表不同的含义,64位,在java中Long类型是64位的,所以java程序中一般使用Long类型存储

  • 第一部分:第一位占用1bit,始终是0,是一个符号位,不使用

  • 第二部分:第2位开始的41位是时间戳。41-bit位可表示241个数,每个数代表毫秒,那么雪花算法可用的时间年限是(241)/(1000606024365)=69 年的时间

  • 第三部分:10-bit位可表示机器数,即2^10 = 1024台机器。通常不会部署这么多台机器

  • 第四部分:12-bit位是自增序列,可表示2^12 = 4096个数。觉得一毫秒个数不够用也可以调大点

优点:雪花算法生成的ID是趋势递增,不依赖数据库等第三方系统,生成ID的效率非常高,稳定性好,可以根据自身业务特性分配bit位,比较灵活

缺点:雪花算法强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。如果恰巧回退前生成过一些ID,而时间回退后,生成的ID就有可能重复。

总结

单体应用使用mysql的自增id是完全ok的,对于分布式系统,分表分库的场景,雪花算法明显更合适,且各个大厂对于雪花算法有各自的优化,也可以根据自己系统的场景进行个性化修改。

  • 14
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值