一. 概述
分布式系统中很多场景都需要唯一标识,比如消息、订单号、券号等。服务化、分布式已成为当下系统开发的首选,高并发操作在数据存储时,需要一套id生成器服务,来保证分布式情况下全局唯一性,以确保系统的订单创建、交易支付等场景下数据的唯一性,否则将造成不可估量的损失.
二. 需求
-
全局唯一
-
高性能
-
高可用
-
趋势递增
-
全局递增
三.常见方案&思路
1.UUID(Universally Unique Identifier)
UUID 经由一定的算法机器生成,为了保证 UUID 的唯一性,规范定义了包括网卡 MAC 地址、时间戳、名字空间 (Namespace)、随机或伪随机数、时序等元素,以及从这些元素生成 UUID 的算法(uuid实现也有多种版本).
特点:
-
本地生成 ID,不需要进行远程调用,时延低,性能高
-
长度固定128位,字段较长、无序、可读性差(不适合做db索引)
-
并非绝对唯一,参考:https://www.zhihu.com/question/34876910#answer-31004674
场景:
-
日志跟踪
-
普通的唯一标识(message id)
2.基于数据库自增主键
数据库递增主键是天然的递增“发号器”,每次插入数据即可获得递增id.
特点:
-
实现简单,使用数据库已有的功能
-
能够保证唯一性,能够保证递增性
-
性能一般(每次访问DB)、可用性较差(主库挂了,数据一致性能以保证)
-
单点、扩展性有待提升
3.基于数据库自增升级版V1
为了解决单点问题和性能问题,可以采用多个数据库实例,每台机器设置不同的初始值,且步长和机器数相等.比如有两台机器。设置步长step为3,server1的初始值为1(1,4,7…)、server2的初始值为2(5,8,11…),server3则为(3,6,9…) 这样错开就可以得到唯一id.
特点:
-
能够保证唯一性,能够保证趋势递增
-
分布式,高可用
-
性能一般(每次都写DB)、扩容麻烦(水平扩容后,步长和初始值都需要重新设置)
4.基于数据库自增升级版V2
核心思想是批量获取id(segment),减少DB写次数,实操可有多种方式.参考美团公司级发号器leaf做法:
首先设置字段maxId和step,每次批量取一定数量的可用ID在内存中,使用完后,再请求数据库重新获取下一批可用ID,每次获取的可用ID数量由step控制,实际业务中可根据使用速度进行配置step大小,同时增加一个业务标识字段,隔离各个业务系统的ID.
特点:
-
大大降低数据库写压力,数据库不再是性能瓶颈,生成ID性能大幅度提高,因为获取一个可用号段后在内存中直接分配,相对于每次读取数据库性能提高了几个量级
-
强依赖数据库,当数据库异常时整个系统不可用(DB短时间内挂了可以接受)
-
单服务内id递增,多节点整体趋势递增,服务重启会导致id空洞
5. 基于缓存实现
类似方案2,利用redis原子递增操作(incr)获取id.(17年酒店订单发号器:两套缓存+db兜底)
特点:
-
全局递增
-
高性能
-
强依赖缓存、可靠性较低,需要兜底方案
6. 类snowflake算法
snowflake核心思想是:一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号,借鉴snowflake的思想,结合各业务逻辑和并发量,可以增加一些trick实现自己的分布式ID生成算法.
特点:
-
41位的时间戳能够用到约69年,241 毫秒转换成时间为2039-9-7 23:47:35,如果机器数量较少机器标识可以人工处理, 12bit序列号表示单机每毫秒最大发号4096个
-
性能高、扩展性强
-
多台服务时钟同步可能会时钟回拨,这是snowflake算法要解决的问题( linux软件时间会随着服务器的长时间运行会出现漂移,最终会越来越不准确)
-
闰秒问题(https://coolshell.cn/articles/7804.html) ,简单说几年会出现一次00:59秒持续2秒的情况
四.业界参考
1.美团leaf
leaf实现了两套方案:基于数据库自增升级版V2和snowflake算法,目前代码没有开源,技术细节可以参考美团点评技术团队博文,讲的非常清楚,之前找leaf团队的同学要repository的时候,他们也说后续会开源.
2.支付宝Vesta
Vesta基于snowflake算法做了改造,详情参考官方文档.