一.背景
1.1 背景
- 业务背景:
目前梳理 SaaS 系统中存在以下几种 Id 生成的场景.
财务系统: 财务在生产财务单的时候,获取财务单 Id ,满足分布式场景下能够获取全局Id即可.
支付系统:订单系统在进行外部提单过程中,需要将生成的订单号与外部的支付平台做对接.在具体开发调试过程中出现这种情况.客户需要支持私有化交付.我们在 SaaS 的开发环境部署联调时,已经与外部的支付平台进行了对接.完成联调结束后.在客户的私有化环境中再次进行测试回归出现了,出现了因为订单号重复导致了支付失败的场景.
商品系统: 商品系统在生产具体的Id形式上有几种业务侧的诉求.
场景 | 具体逻辑 | 示例 |
---|---|---|
SPUId | SPUId | 生产支持固定位数(可配),起始值(可配)的自增流水Id |
类目Id | 类目Id .支持固定前缀(可配).流水固定位数(可配)的流水自增 | 固定前缀Ca.流水固定位数(6),Ca000001,Ca000002 |
- 技术背景:
现有 SaaS 的不同系统中存在不同的 Id 生产的逻辑和服务,大概可以分为两种方案.
- 第一种:基于中心化的 Id 服务来获得.
- 第二种:基于本地化的 Id 数据库来生产.
结合目前的 SaaS系统 会有私有化部署的场景,那就是需要去考虑部署成本的因素.且目前的中心化方案可能带来的单点故障问题.基于以上的一个背景
1.2 目标
- 支持分布式全局
- 支持私有化部署独立部署
- 支持自定义规则 Id 生产
- 性能目标:单机4c8g下 1000 qps 目标
二.技术方案
2.1 技术调研对比
常见方案 | 优势 | 对比目标后的缺点 |
---|---|---|
UUID | 使用简单,支持分布式场景,理论上存在碰撞可能(几率极低) | 分布式支持;独立部署支持;不支持自定义规则;最重要的原因是利用UUID作为分布式数据库业务Id的场景下,由于B+树作为底层的数据结构会带来大量的页分裂和反复的插入以及翻转,并以此带来的大量性能损耗 |
雪花算法 | 支持分布式;性能优秀 | 依赖物理机时钟,时钟回拨回带来碰撞问题;不支持自定义规则; |
Redis | 支持分布式;性能优秀; | 额外的Redis服务成本,不支持自定义规则 |
ZK | 支持分布式; | 额外的ZK服务成本;不支持自定义规则 |
2.2 结合现有业务的最终选择
抛开业务场景的下的技术方案设计都是刷流氓.基于目前咱们的技术目标.选用了一个基于数据库(不额外支出其他资源)的且支持自定义规则的设计方案.
- 支持分布式全局
- 支持私有化部署独立部署
- 支持自定义规则 **Id** 生产
- 性能目标:单机4c8g下 1000 **qps** 目标
三.详细设计
3.1 方案流程
3.1.1 配置初始化
3.1.2 生产 Id 流程
3.2 类图
-
IdAutoBootConfig
-
spring.factories 入口方法
-
能力1:用以保证引入的业务方能够自动扫描本身的包体,扫描对应的bean到spring container
-
能力2:用以扫描对应的 mybatis mapper 注入动态代理
-
IdGenerator 用以外漏 id 的生产接口.
方法1:生成全局id generatorGlobalId(String name) tenantId =9527
方法2:生成租户级别全局唯一generatorGlobalId(long tenantId,String name); -
IdGeneratorImpl 具体方法生成的实现类.
内部持有持有上下文中具体的Sequence name 与 SequenceValue 的映射的读写操作.
核心读写锁分离处理具体流程(方便理解可能与上图部分有重复).
- InitSequenceConfigWare& SequenceStepContextHolder
InitSequenceConfigWare 通过实现 InitializingBean扩展点 完成容器启动后(本质是利用容器启动完成,数据源完成注入)来实现数据库Sequence 读取得到 SequenceConfigModel .通过 SequenceStepContextHolder 维护 sequenceName 与 SequenceValue的映射关系.为了减少 内存占用.将SequenceConfigModel简化一部分得到 SequenceValue 进而维护到 SequenceStepContextHolder 持有的 ALL_SEQUENCE_CONTEXT 中.
3.3 DB设计
code rule 表(非必选)本期没有实现后续可以持续在这个表进行扩展得到更加灵活的SequenceId表达.
CREATE TABLE `code_rule` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键id',
`tenant_id` bigint(20) NOT NULL COMMENT '租户id',
`biz_type` tinyint(4) NOT NULL DEFAULT '1' COMMENT '业务:1-商品编码规则,2-类目编码规则,参见BizTypeEnum',
`rule_key` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1-流水自增,2-租户自定义,3-类目编号和流水自增,参见RuleKeyEnum',
`rule_value` varchar(256) NOT NULL COMMENT '编码规则描述json格式',
`create_time` datetime NOT NULL COMMENT '创建时间',
`dt` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '数据中心抽数专用字段,无业务含义',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_tid_type` (`tenant_id`,`biz_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='编码规则表';
/**
* **sequence** 表。必选表。本期先实现需要业务方自行创建**ddl**维护到对应数据源中.
*/
CREATE TABLE `sequence` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '当前序列最大值',
`tenant_id` bigint(20) NOT NULL COMMENT '租户id',
`name` varchar(200) NOT NULL COMMENT '序列名称',
`start` bigint(20) NOT NULL COMMENT '开始值',
`end` bigint(20) NOT NULL COMMENT '结束值',
`step_size` bigint(20) DEFAULT NULL COMMENT 'id号段数量',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='序列号表';
3.4 no more talking 一键开箱
详见:github https://github.com/Baixiu-code/id-generate-starter
3.5 多线程并发测试验证
本着面向TDD的原则,也开放了多线程版本的自测演示.
场景:
启动1000个线程同时并发请求获取生产Id
脚本准备:
insert into sequence (tenant_id,name,start,end,step_size,create_time,update_time) values ('9527','product','0','0','2',now(),now());
这里的 stepSize 设为2 ,尽量去触发临界值得到更新数据库的条件,来模拟现实场景的情况.实际场景中可能stepSize 可能非常大.大多数都是内存级别的读取行为,很少触动数据库的更新.
验证方案:
验证获取的Id是否重复,本方案通过 put Id 到ConcurrentHashMap , map 能够自动去重. 最后通过countDownLatch阻塞主线程,获取 ConcurrentHashMap 的 size 是否为1000,来得到是否满足并发场景下的生产Id诉求.
https://github.com/Baixiu-code/id-generate-starter-test