在单库单表时,业务 ID 可以依赖数据库的自增主键实现。现在把存储拆分到了多处,如果还是用数据库的自增主键,势必会导致主键重复。
生成主键有哪些方案
1、一个最直接的方案是使用单独的自增数据表,存储拆分以后,创建一张单点的数据表。
CREATE TABLE IF NOT EXISTS `order_sequence` (
`order_id` INT UNSIGNED AUTO_INCREMENT, PRIMARY KEY (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这个方案存在的问题
1、性能无法保证,在并发比较高的情况下,如果通过这样的 数据表来创建自增ID生成的主键很容易成为性能瓶颈。
2、存在单点故障,如果生成自增ID的数据库挂掉,那么会直接影响创建功能。
2. 使用 UUID 实现
public String getUUID() {
UUID uuid = UUID.randomUUID();
return uuid.toString();
}
比如:135c8321-bf10-45d3-9980-19ba588554e8
首先 uuid 作为数据库主键太长,会导致较大的存储开销。
另外 uuid 是无序的,如果使用 uuid 作为主键,会降低数据库写入性能。
以MySQL 为例, MySQL 建议使用自增 ID 作为主键
MySQL InnoDB 引擎支持索引,底层数据结构是B+树
- 如果主键为自增 ID ,MySQL 可以按照磁盘的顺序去写入
- 如果主键是非自增 ID,在写入时需要增加很多额外的数据移动。将每次插入的数据放到合适的位置上,会导致出现页分裂,降低数据写入的性能。
3. 基于 Snowflake 算法
Snowflake 是 Tiwtter 开源的分布式 ID 生成算法,由 64 位的二进制数字组成
20230708171753.jpg
- 第1位默认不使用,作为符号位,总是0。保证数值是正数。
- 41位时间戳,表示毫秒数,41位数字可以表示 2^41毫秒,换算成年,结果是69年多。
- 10位工作机器ID ,支持 2^10 = 1024 个节点
- 12位序列号,作为当前时间戳和机器下的流水号,每个节点每毫秒支持2^12 的区间,也就是 4096个 ID,换算成秒,相当于可以允许 409万的QPS 。如果在这个区间超出了 4096,则等待至下一毫秒计算。
Snowflake 算法可以作为一个单独的服务,部署到多台机器上,产生的 ID 是趋势递增的,不需要依赖数据库等第三方系统,并且性能非常高。理论上409 万的 QPS 是一个非常可观的数字,可以满足大部分业务场景,其中的机器 ID 部分,可以根据业务特点来分配。
缺点:
Snowflake 算法存在时钟回拨问题。
在一些业务场景中,比如在电商等整点抢购中,为了防止不同用户访问的服务器时间不同,则需要保持服务器时间的同步,为了确保时间准确,会通过 NTP 的机制来进行校对。
NTP(Network Time Protocl)指的是网络时间协议,用来同步网络中各个计算机的时间。
如果服务器在同步 NTP 时出现不一致,会出现时钟回拨,那么 SnowFlake 在计算中可能出现重复 ID。
润秒也会导致服务器出现时钟回拨。
如何解决时钟回拨,可以使用延迟等待,等待时间追上来。
4. 数据库维护区间分配
一种基于数据库维护自增 ID 区间,结合内存分配的策略。这也是淘宝的 TDDL 等数据库中间件使用的主键生成策略。
在数据库中创建 sequence 表,其中的每一行,用于记录某个业务主键当前已经被占用的 ID 区间的最大值
CREATE TABLE `sequence` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(64) NOT NULL COMMENT 'sequence name',
`value` bigint(32) NOT NULL COMMENT `sequence current value`,
PRIMARY KEY(`id`),
UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入一条行记录,当需要获取主键时,每台服务器主机从数据表中取出对应的 ID 区间缓存在本地,同时更新 sequence 表中的 value 最大值记录
INSERT INTO sequence (name, value) values ('order_sequence', 10000);
当服务器在获取主键增长区间段时,首先访问对应数据库的 sequence 表,更新对应的记录,占用一个对应的区段。比如设置步长为200,原先的 value 值为 1000,更新后为 1200。取到对应的 ID 区间后,在服务器内部进行分配,涉及的并发问题可以依赖乐观锁等机制解决。
有了对应的 ID 增长区间,本地就可以使用 AtomicInteger 等方式进行 ID 分配。
这种方式生成的唯一 ID,可以保证整体的趋势递增
为了防止单点故障,sequence 表所在的数据库,通常会配置多个从库,实现高可用。
除了上面几种方案,实际开发中还可以应用 Redis 作为解决方案。即通过 Redis Incr 命令来实现。
总结
主要分享了实现唯一主键的几种方案,也就是我们通常说的 分布式发号器
主要使用 UUID,使用 Snowflake 算法,以及数据库存储区间结合分配的方式
问题:一个生产环境中可用的主键生成器应该具备哪些特性呢?
- 生成的主键必须全局唯一,不能出现重复 ID
- 需要满足有序性,也就是单调递增,或者也可以满足一段时间内的递增。
- 在写入数据库时,有序的主键可以保证写入性能
- 很多时候都会使用主键来进行一些业务处理,比如通过主键排序等
- 性能要求,要求尽可能快的生成主键,同时满足高可用。因为存储拆分后,业务写入强依赖主键生成服务,假设生成主键的服务不可用,订单新增,商品创建等都会阻塞。
网友评论