引言
在掘金写了多篇文章之后,我发现了掘金生成文章ID的密码,先来看看我最近发布的几篇文章的链接:
2022-05-18 20:14 https://juejin.cn/post/7099048344558927908
2022-05-17 13:12 https://juejin.cn/post/7098568794745864222
2022-05-16 23:58 https://juejin.cn/post/7098364044238651428
2022-05-15 15:58 https://juejin.cn/post/7097869333308637191
2022-05-14 18:39 https://juejin.cn/post/7097538908270886925
2022-05-13 23:37 https://juejin.cn/post/7097245569776615454
......
首先,我们发现掘金后端的路由简洁清晰,即/post/{文章ID}
;
其次,这个文章的ID都是19个数字;
最后,ID的数字大小是随时间递增的。
有经验的掘友可能已经想到SnowFlake
了 ^-^~
如果你不清楚,下面请听我一一道来,本人的一些见解,欢迎大佬评论!
1. 主键的选择
首先,我从数据库的主键选择说起,主键,它可以唯一标识表中的某一条记录,对数据表来说非常重要。当我们需要查询和引用表中的一条记录的时候,最好的办法就是通过主键。
在数据库设计时建议尽量不要用业务字段,也就是跟业务有关的字段做主键,因为作为项目设计的技术人员,我们一般无法预测在项目的整个生命周期中,哪个业务字段会因为项目的业务需求而有重复,或者重用之类的情况出现。
所以你会给表设计一个id作为主键,并给这个字段定义自增约束,如果你是这样设计的,对于一般的小应用也不会存在大问题。
但是,对与分布式系统,数据库涉及到多个,就不能依靠数据库来生成自增的id了,因为同一时间在不同数据库上可能生成相同的id。为了保证id的唯一性,我们需要把id的生成放到后端的逻辑中。
我之前听一个老师讲,在主数据库中维护一个信息表,专门记录当前id的最大值,然后从数据库每次在新增id的时候先到主数据库获取这个最大值,在这基础上加一,这个过程需要利用事务防止误读。这个方案确实能解决冲突的问题,但是我感觉这个加锁来保持数据的一致性,可能不能满足高并发、低延时的情况。
那么在做数据库设计之前需要怎么考虑呢?下面是通用的对ID的要求:
2. 分布式系统对id的要求
全局唯一: 不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
趋势递增: 在MySQL的InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用BTree的数据结构来存储索引数据。因此在主键的选择上我们应该尽量使用有序的主键保证写入性能。
单调递增: 保证下一个ID一定大于上一个ID,例如事务版本号,IM增量消息、排序等特殊需求。
信息安全: 如果ID是连续的,恶意扒取用户工作就非常容易做了,直接按照顺序下载指定的URL即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,需要ID无规则。
含时间戳: 这样就能够在开发中快速了解分布式ID的生成时间。
高可用: 发一个获取分布式ID的请求,服务器就可以保证99.999%的情况下给我创建一个唯一的分布式ID
低延迟: 发一个获取分布式ID的请求,服务器响应速度要快
高QPS:假如并发10万个创建分布式ID请求,服务器要顶得住并能成功创建10万个唯一的分布式ID
3. 技术选型
那么业内有哪些成熟的生成id的方案来满足上述要求呢?一个是UUID
,另一个是SnowFlake
(雪花算法)。
3.1 UUID
UUID(Universally Unique Dentifer)
的标准型式包含32个16进制数字,以连接符分为五段,形式为8-4-4-4-12
的32个字符,示例:630e450a-abcb-435c-cba6-223300000000
优点:
1)简单,代码方便。
2)生成ID性能非常好,基本不会有性能问题。
3)全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。
UUID性能非常高:本地生成,没有网络消耗,如果只考虑唯一性UUID是ok的。但是
缺点:
1)没有排序,无法保证趋势递增。无法预测他的生成顺序,不能生成递增有序的数字。
2)分布式id 一般都会作为主键, UUID太长,占用存储空间比较大,如果是海量数据库,就需要考虑存储量的问题。
3)UUID往往是使用字符串存储,查询的效率比较低。
4)传输数据量大,且不可读。
5)索引, B+树索引的分裂。既然分布式id是主键,主键是包含索引的,然后mysql的索引是通过b+树来实现的, 因为UUID数据是无序的,每一次新的UUID数据的插入,为了查询的优化,都会对索引"底层的B+树进行修改,这一点很不好。插入完全无序,不但会导致一些中间节点产生分裂,也会白白创造出很多不饱和的节点,这样大大降低了数据库插入的性能。
变种的UUID
1)为了解决UUID不可读,可以使用UUID to Int64的方法。
2)为了解决UUID无序的问题,NHibernate在其主键生成方式中提供了Comb算法(combined guid/timestamp)。保留GUID的10个字节,用另6个字节表示GUID生成的时间(DateTime)
3.2 SnowFlake(雪花算法)
全局唯一,并且有序按时间递增,同时占用空间少,生成的id仅仅是19位的整形数字,正好契合mysql的bigint数据类型,简直完美。
数据结构:
1bit-符号位
因为二进制中最高位是符号位,1表示负数,0表示正数。 生成的id一般都是用整数,所以最高位固定为0。
41bit-时间戳
用来记录时间戳,毫秒级。 41位可以表示2^(41)-1个数字, 如果只用来表示正整数(计算机中正数包含0) ,可以表示的数值范围是: 0至2^(41)-1, 减1是因为可表示的数值范围是从0开始算的,而不是1。也就是说41位可以表示2^(41)-1个毫秒的值,转化成单位年则是(2"41)-1)/(1000 "60 "60 -24 "365) =69年。
10bit-工作机器id
用来记录工作机器id.可以部署在2^(10)= 1024个节点,它包括5位datacenterld和5位。 workerld 5bit可以表示的最大正整数是245-1=31,即可以用0,1,2,3…31这32个数字,来表示不同的datecenterld或workerld。
12bit-序列号
序列号,用来记录同毫秒内产生的不同id。 12bit可以表示的最大正整数是2^(12)-1=4095,即可以用0、1.2.3. 4094这4095个数字,来表示同一机器同一时间截(毫秒)内产生的4095个ID序号。
SnowFlake可以保证:所有生成的id按时间趋势递增,整个分布式系统内不会产重复id (因为有datacenterld和workerld来做区分)
优点:
(1)高性能高可用:生成时不依赖于数据库,完全在内存中生成。
(2)容量大:每秒中能生成数百万的自增ID。
(3)ID自增:存入数据库中,索引效率高。
缺点:
依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。
总结
现在,你应该知道为什么掘金生成的文章ID是19位数字了吧,尤其是正好契合mysql的bigint类型,非常友好。
如果你想知道在go语言里面如何实现雪花算法,请看我的下一篇文章:雪花算法生成主键ID |青训营笔记
原文:https://juejin.cn/post/7099366815628787748