背景:
分布式需求下的ID生成方案有很多种。但满足以下条件的不多:
1、除现有的DB外,不增加运维负担和不增加基础设施的资源要求
2、ID大体趋势递增(实际情况中一般不需要严格递增,即使是用AUTO_INCREMENT字段,也因事务提交顺序和rollback问题不会严格递增)
3、ID种子状态需要持久化
词汇:
OSP:唯品会服务化基础平台
Saturn:唯品会分布式JOB平台
VMS:唯品会MQ平台
基于AUTO_INCREMENT字段的方案
基于现有应用环境,用DB实现以上需求可能是比较简单的方案。
基于mysql的持久化和协调,可以有两个方案:
1、建立带AUTO_INCREMENT字段的表,用AUTO_INCREMENT机制生成ID。
有两个子方案:
1a:获取ID时,insert into 然后 last_insert_id
1b:加入唯一索引字段,获取ID时,replace into(唯一字段名) values ('重复的唯一字段值') 然后 last_insert_id。
方案1b的好处是保证数据库中只有和行记录,不需要定期归档数据。
基于AUTO_INCREMENT字段问题是:在高并发情况下生成ID相关的并发数、TPS数要求与数据一致。在业务已经分库,但ID生成没分库的情况下,势必成为瓶颈。
解决方法是:
1、把AUTO_INCREMENT字段的表也分库,按一定策略路由ID生成请求到不同的库。库与库进行ID号的隔离,方法可以是:
a. AUTO_INCREMENT中不同的大初始号段
b. AUTO_INCREMENT相同步长(如32),不同的初始值
以上分库方法的问题是可能使ID大体趋势不是递增。
批量号段派发方案-IDSnowman
如果应用和DB的一次交互可以拿到一批连续ID,而且拿到ID有使用有效期。这样既可以减少DB TPS、并发、连接数,也可以提高ID的生成的平均时效。
DB表结构
CREATE TABLE `sequence_table` (
`biz_type` VARCHAR(50) NOT NULL COMMENT '业务类型',
`cur_id` BIGINT(20) NOT NULL COMMENT '当前值',
`step` INT(11) NOT NULL COMMENT '步长',
`time_to_live` INT(11) NOT NULL COMMENT '生存时长(秒)',
PRIMARY KEY (`biz_type`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB;
架构设计
说明:
每个应用的业务线程池中,维护一个线程本地变量,IDBuffer,其结构大体如下:
线程本地ID Buffer : [
占用相关: {batch_max_id=800, cur_id=601, birthday="20170115 03:02:01"}
调整相关:{……}]
每个业务类型记录其当前批次的最大值,当前值,批次发放时间。
具体的实时顺序图:
说明:
ID生成库使用与业务分享独立的DB连接池,DB连接的事务为autocommit=1。即不使用事务。
注意到其中的LAST_INSERT_ID(cur_id + Step) 。函数为设计DB连接的会话本地last_insert_id变量,不影响其它会话(见:http://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id)。
这样,DB的TPS、并发、连接数可以因批次步长和批次有效时间的大小而数倍下降。ID也大体趋势递增。
封装方法
可以封装为OSP服务(好处是减少DB连接数)。或一个公共JAR(好处是减少运行服务依赖),由应用控制DB连接池。
单点问题 single point of failure (SPOF)
由DB决定。为DBA保障。
封装讨论
实现上,是否真有需要跨表的唯一顺序ID生成需求。这是值得考虑的。如果没有这个需求,事情可以简单很多。
[参考文章]
http://code.flickr.net/2010/02/08/ticket-servers-distributed-unique-primary-keys-on-the-cheap/
http://dev.mysql.com/doc/refman/5.6/en/information-functions.html#function_last-insert-id
https://my.oschina.net/CandyDesire/blog/619122
http://tech.meituan.com/mtddl.html