互联网业务用户、商家、订单号等 id 如何生成

2019 Java 开发者跳槽指南.pdf (吐血整理)….>>>

互联网业务用户、商家、订单号等 id 如何生成


id 在互联网企业的业务中无处不在,用户、商家、订单号、商品等都是用 id 来唯一标识的。


最常见的 id 生成策略便是数据库的自增主键,但用自增主键有一个致命的缺点:会暴露业务数据


竞争对手只要每天开始和结束分别调下你的接口,就知道你当天的业务数据了。


于是,有些人用 UUID,UUID 无顺序,生成也是完全分布式的,没有单点问题,但它也有它的问题。


一是太长,会浪费空间,可能会影响到联合索引的建立,联合索引有长度限制,id 所占位数越多,留给其它字段的位数就越少。


二是它几乎是随机的,在插入数据库的时候会损失性能。


拿 MySQL 来说,索引的组织形式是 B+ 树,如果是完全顺序的插入,基本在后面追加新页就行了,反应到磁盘上就是大量的顺序写,速度很快。而如果是完全随机的插入,那么位于 B+ 树前面的那些页存不下的时候,就需要对页做分裂操作,这就会需要大量修改之前的页,反应到磁盘上就是大量的随机写,速度可以比顺序写慢一个甚至几个数量级。


当然我们平时一张表上会有多个索引,不是所有的索引都能按照一致的顺序来建立的,因此,大部分情况下实际性能损失并没有上面描述的那么夸张。


如果你不在乎这点性能损失,也不在乎这点空间浪费,用 UUID 就行了。


但你还是在乎的嘛。


整型 id

所以我们来探究下更紧凑的整型 id,我们先列一下 id 需要满足的基本要求:

1. 全局唯一

2. 不会泄露业务数据(不可预测)


说到不可预测,首先想到随机数。但它会受到第一条要求的约束。我拿到一个随机生成的 id,怎么知道这个 id 是唯一的呢?


除了反查数据库,似乎别无他法。而且,如果拿到的是个已经存在的 id,那就得继续随机,继续反查。当然这个策略可以做一些优化,比如每天预先跑一批不重复的 id 存起来,用的时候直接取。但这对于日单量过千万的系统来说并不是一个好的选择,一是仍然避免不了反查数据库,二是还得专门存这批预先跑出来的 id。


如果生成的 id 不需要反查数据库就可确定唯一性,无疑是更好的选择。


Snowflake

要满足以上这些要求,Snowflake 是一个非常好的选择。


互联网业务用户、商家、订单号等 id 如何生成


Snowflake 原理很简单,如果 worker 的时间不出现回退,理论上就不会出现重复的 id,但我们没办法保证 worker 的时间不出现回退。


出现重复 id 问题严重吗

其实对于大部分系统而言,短暂出现的重复 id 并不可怕,因为反正入库的时候会因为唯一冲突而报错,只是造成系统不可用而已,而不会造成脏数据,对于绝大多数系统来说,出现脏数据才是真正的灾难。


workerId 的分配

workerId 应该如何分配?显然 workerId 最好不应该被交叉分配,比如原来分配给 A 的 workerId,之后某个时间点又被分配给 B,因为各个机器的本地时间不可能完全同步,这台机器已经发完号的时间,其他机器也许并没有发完,所以如果 workerId 出现交叉分配的情况,就可能出现 id 重复的情况。


因此 workerId 一旦被分配,就最好是固定的。


那么 workerId 应该按机器分配,还是按进程分配呢?实际上,开源实现里,两种方式都有,各有优缺点。


workerId 按机器分配

按机器分配即每台机器分配一个固定的 workerId,一般用机器的 IP 地址来标识机器,在运维实践中,机器分配的内网 IP 一般不会变。


由于机器数量一般情况下变化不大,因此可以用手动来配置,但手动配置有很大的缺点。比如,对于高峰时段非常集中的应用,为了提高资源的利用率,弹性扩缩容就非常必要,因此集群机器数量会频繁变化,这样一来,需要频繁的人肉配置,麻烦不说,还容易出错。


因此,大型应用一般会采用程序分配的方式。其实是手动分配还是程序分配对 id 的正确发放并没有多大影响,这里我想讨论的是 workerId 按机器分配时可能出现的问题。来看一个 case。


机器 8.8.8.8 分配的 workerId 为 1,发号时突然宕机了,最后发号的时间是 t1。机器重启后时间出现了回退,之后进程被重新启动,此时时间为 t2,但由于机器时间出现了回退,结果 t2 比 t1 还早,可想而知,这么继续发号极有可能出现重复。


美团的开源实现 Leaf 采用的正是按机器分配的方案,那么它是如何处理上面这种情况的呢?


Leaf 在程序正常运行过程中,每 3 秒上报一次本机器的系统时间,如果程序意外重启,它首先会去取最后一次上报的时间,如果系统时间比最后一次上报的时间还早,那显然是出问题了,启动失败报警,由人工介入处理。


前一步正常的话往下继续,通过 RPC 获取集群中所有机器的系统时间,判断这些机器的平均系统时间与本机器的系统时间的差距,如果差距较大,则说明本机器的系统时间有较大误差,启动失败报警,否则正常启动。


可以看到,上报过程如果正常的话,最多出现 3 秒的重复发号时段。上报如果不正常,情况就比较糟糕了,但上报不正常,同时还出现进程重启并且时间回退的概率就比较小了。所以美团实践中运行多年还未出现重复发号的情况。


workerId 按进程分配

按进程分配即每个进程分配一个固定的 workerId,但进程用什么来做唯一标识呢?进程号也是会被操作系统复用的,可以说没有这样的唯一标识,因为进程是朝生夕死的,它的生命只有一次,跟人一样。所以一般只要进程启动就分配一个之前从未用过的 workerId,也就是一次性的 workerId。


百度的开源实现 UidGenerator 就是这么做的。这么做的好处显而易见,像按机器分配时出现的进程重启前后出现时间回退的情况,就不会影响到这种方案了,只需要管进程运行中出现的时间回退就行了。


而进程运行中的时间回退我们完全可以在程序中检测到,出现这种情况或者等待时间纠正,或者直接报错就行,只要不发出重复的 id 就是可以接受的。


这种方案缺点也很明显,每次进程重启就要消耗一个 workerId,workerId 位数有限,总有消耗完的时候。而且集群机器越多,进程重启次数越多,消耗就越快。


Snowflake 方案总结

如前所述,workerId 按机器分配这种方案,缺点是有出现重复 id 的概率(概率可以做到非常小),优点是 workerId 占用位数少,生成的 id 可以更短。workerId 按进程分配的方案,缺点是 workerId 消耗多,占用位数相对多,优点是不会出现重复发号的情况。


订单号的其它生成方案

以上是用 Snowflake 的方案,比较适合需要高并发,又不能泄露业务数据的 id,比如订单号。


下面介绍另一种订单号的生成方案。


假如我是一个电商网站,用户多,商家相对少,商家 id 是一个 32 位整型,那么我可以这样来生成订单号:


维护商家每天的当日订单量,生成订单号时,高 32 位为商家 id,低 32 位用日期的数字组合当日该商家订单量,高 32 位与低 32 位拼起来做订单号。


互联网业务用户、商家、订单号等 id 如何生成


由于日期 yyMMdd 占据了一定位数,32 位整型只剩下 10000 个数字的空间可使用,因此,该方案适合商家日单量小于 10000 的业务,比如外卖。


自增 id 就完全没用吗

自增 id 生成方便,又不用引入额外的依赖,插入性能又好,弃之实在是可惜,难道就真的不能用吗?


非也,要用还是可以用的,我们只需要在把自增 id 给出去时做个加密就行了。


最简单的加密方法,异或加密。


加密

cipherId = autoIncrementId ^ password;


解密

autoIncrementId = cipherId ^ password;


so easy!当然,你可能会嫌这种加密方式太弱,其实还有许多其它的加密方式,64 位数据的加密算法有:Blowfish、DES、XTEA、SkipJack等,32 位数据的加密算法有:Skip32(话说 32 位加密就有点弱了,人家把整个 32 位空间遍历一遍也花不了多久)。


这种方案比较糟糕的地方在于,密钥是死的,被员工拿到,人家跳槽去竞对就暴露了。


也许有人想到可以拼一个版本号在 id 里面,不同的版本号对应不同的密钥。但这样的话,一旦你升级版本号,那对用户来说,订单号就会变(同一个订单,去年看和今年看订单号居然不一样),这也是不能接受的。


64 位 id 与 JavaScript 交互问题

我们的 id 总是要给到前端的,这些年大前端概念流行,跨平台开发框架大行其道,JavaScript 还有很强的生命力。不同于其它语言,JavaScript 不区分整数值和浮点数值,所有数字均用 64 位浮点数值表示,因此会出现某些 64 位整型在 JavaScript 中无法表示的问题。因此,64 位 id 最好转成字符串形式再给到前端。


关于百度 UidGenerator 的一点想法

百度开源 UidGenerator 按进程分配 workerId 的方案,其实 workerId 可以循环使用,什么意思呢?就是像 TCP 的序列号一样,用完后归零继续用,反正上次序列号 0 被使用已经过了很久了。如果发号时机器时间不出现非常大的误差(几个月甚至几年),那么完全可以采用这种 workerId 循环使用的方案。这样的话,每次启动需要校准一下时间(可以结合美团 Leaf 的方法),不允许在时间出现非常大误差的情况下发号。在程序运行过程中也需要校验,不允许本次发号距离上次发号超过太长时间,以防程序运行中时间突然出现较大偏差。


总结

互联网业务中的用户、商家、订单号等 id,为了兼顾性能及安全性,建议使用 Snowflake 的方案生成,该方案适合中大型企业。对于创业公司等小企业来说,自增 id 加密的方案也不失为一种好的实用方案,相较于 Snowflake 的方案,其性能更好,但安全性较弱。

原文始发于微信公众号(野马的架构之路):互联网业务用户、商家、订单号等 id 如何生成