背景介绍

在一般的业务场景中, 初始的时候简单的自增数(比如MySQL 自增键)就可以很好的满足需求, 不过随着业务的发展和驱动, 尤其是在分布式的场景中, 如何生成全局的唯一 id 便成了需要慎重考虑的事情. 业务之间如何协调, 生成的序列是否还有其它需求等都需要重新设计, 下文则介绍生成唯一 id 的不同方式以及各自适用的场景.

1. twitter Snowflake 介绍

原文见: announcing-snowflake twitter 碰到的问题 twitter 使用 MySQL 存储线上的数据, 不过随着业务的发展, 现在已经成为了很大的数据库集群. 由于种种原因, 在一些细节方面, twitter 使用分布式数据库 Cassandra 或水平拆分 MySQL 来更好的服务全局的博文及帖子. Cassandra 并没有内置类似 MySQL 自增主键的功能, 这也意味着随着业务的扩张, 使用 Cassandra 很难在序列 id 方面提供一个通用的解决方案(one-size-fits-all solution), 这个问题在水平拆分 MySQL 的架构中也同样存在. 基于这些问题, twitter 提出了以下需求:

  1. . 每秒生成上万的 id 号, 并且能以高可用方式提供服务;
  2. . 由于业务的关系只能选择非协调(业务无关)的方式生成 id 号;
  3. . id 号大致上要能排序, 这意味着同时发表 A B 两篇文章, 他们的 id 号应该是相近的.
  4. . id 号应该是 位大小.

可选的解决方案 twitter 也考虑了几种方式来满足上述的需求:

  1. . 基于 MySQL 的服务;
  2. . UUID 方式;
  3. . zookeeper sequential nodes;

基于 MySQL-based ticket servers 本质上通过自增 id 来实现, 不过这种方式在程序不重构的情况下很难保证 id 号按顺序生成, 也不能按照时间排序; 而 UUID 则是 128 位的, 也有概率发生冲突, 同样也没有时间戳; 而 zookeeper 的时序节点则难以满足上万每秒的性能. twitter 的解决方案 为了生成能够大致上可以排序的 64 位 id 号, twitter 提出以三个字段组合生成 id 号: 时间戳(timestamp), worker(工作号), 序列数(sequence number). 序列数和工作号是在每个线程连接 zookeeper 后就确定的, 详细的代码见: snowflake 这种方式有几点好处, 首先, 开始部分都是时间戳, 可以很方便的建立索引; 其次, 同一个线程下发表的文章或帖子可以进行排序, 而且 id 号临近; 另外, 整体上看 id 号是近似排序的. id 号实现 twitter 的 id 号以如下部分组合实现, 构成63位的整数, 最高位为0:

  1. id is composed of:
  2. time - 41 bits (millisecond precision w/ a custom epoch gives us 69 years)
  3. configured machine id - 10 bits - gives us up to 1024 machines
  4. sequence number - 12 bits - rolls over every 4096 per machine (with protection to avoid rollover in the same ms)

机器 id 共占 10 bit(5 bit 数据中心id, 5 bit 工作id), 最大即为 1024; 时间戳精确到毫秒, 占 41 bit(比如1490842567501 精确到了毫秒), 每次生成新的 id 的时候需要获取当前的系统时间, 再分两种情况生成 sequence number:

  1. 如果当前的时间和前一个已生成的时间相同(同一毫秒), 就用前一个 id `sequence number + ` 作为新的 sequence number; 如果本毫秒的 id 用完就等到下一毫秒继续(等待过长中不能分配新的id);
  2.  
  3. 如果当前的时间比前一个 id 的时间大, 随机生成一个初始的 sequence number 作为本毫秒内的第一个 sequence number;

整个过程中, 只在 worker 启动的时候会对外部有依赖(从 zookeeper 获取 worker 号), 以后就可以独立工作, 做到了去中心化; 另外如果是异常情况下:

  1. 获取的当前时间小于上一个 id 的时间, twitter 的做法则是继续获取当前机器的时间直到获取到更大的时间才能继续工作(等待的过程中不能分配新的 id);

从这点看如果机器的时钟偏差较大, 整个系统则不能正常工作, snowflake 文档中也做了相应的提示, 使用 ntp 同步系统时钟, 同时将 ntp 配置成不会向后调整的模式, 详见: Time_synchronization

  1. System Clock Dependency
  2. You should use NTP to keep your system clock accurate. Snowflake protects from non-monotonic clocks, i.e. clocks that run
  3. backwards. If your clock is running fast and NTP tells it to repeat a few milliseconds, snowflake will refuse to generate ids
  4. until a time that is after the last time we generated an id. Even better, run in a mode where ntp won't move the clock
  5. backwards. See http://wiki.dovecot.org/TimeMovedBackwards#Time_synchronization for tips on how to do this.

参见: Unique-ID

2. last_insert_id 方式

详见: flickr 如果使用 MySQL 作为序列号的服务, 就不能使用 uuid, 这个问题同 snowflake 中介绍的, 也不能使用 md5, guid 等, 这些太散列, 不利于索引的创建和查找; flickr 的文章的介绍了使用 MySQL 自增id 的方式实现序列号的生成. 这种方式也是很多中小业务使用的方式, 不过很多都使用了 InnoDB 引擎: 创建 ticket 相关表:

  

  1. CREATE TABLE `Tickets64` (
  2. `id` bigint(20) unsigned NOT NULL auto_increment,
  3. `stub` char(1) NOT NULL default '',
  4. PRIMARY KEY (`id`),
  5. UNIQUE KEY `stub` (`stub`)
  6. ) ENGINE=MyISAM
  7.  
  8. REPLACE INTO Tickets64 (stub) VALUES ('a');
  9. SELECT LAST_INSERT_ID();

replace 语句在存在唯一键或主键冲突的时候, 会加一个互斥的 next-key 锁, 以免在查询或索引扫描的时候出现幻读的现象, 详见: innodb-locks-set 但是这也会引来一个问题, 多个线程并发更新的时候容易产生死锁, MyISAM 引擎的效果较好, 但不利于 innobackupex 在线备份, 记录很少的情况下可以改为 MyISAM 引擎. 单一业务使用这种方式是个很好的解决方案. 如果需要更好的性能可以采用双主的架构, 不过需要设置好各自的自增键的偏移值和步长.

3. MariaDB Sequence 介绍

MariaDB 10.0.3 版本引入了新的引擎: Sequence , 不同于 postgresql, MariaDB 的 sequence 比较特殊, 它是一个虚拟的, 临时的自增序列, 会话结束后序列便消失, 没有持久化功能, 也不能被其它表像自增主键那样引用. sequence 根据表的名字确定边界和自增值. 如何使用

  1. 边界和自增值由表名决定, 生成 1 ~ 5 的序列
  1. SELECT * FROM seq_1_to_5;
  2. +-----+
  3. | seq |
  4. +-----+
  5. | 1 |
  6. | 2 |
  7. | 3 |
  8. | 4 |
  9. | 5 |
  10. +-----+
  1. 以 3 为步长生成 1 ~ 15 的序列

  

  1. SELECT * FROM seq_1_to_15_step_3;
  2. +-----+
  3. | seq |
  4. +-----+
  5. | 1 |
  6. | 4 |
  7. | 7 |
  8. | 10 |
  9. | 13 |
  10. +-----+
  1. 递减生成序列
  1. SELECT * FROM seq_5_to_1_step_2;
  2. +-----+
  3. | seq |
  4. +-----+
  5. | 5 |
  6. | 3 |
  7. | 1 |
  8. +-----+

note: 如果启用了 sequence 引擎, 新建的表名不能和序列的表名冲突, 临时表可以和序列表名一样 MariaDB sequence 误区 MariaDB sequence 引擎不像 PostgreSQL 和 FirebirdSQL 的序列生成器, 生存期仅为当前语句的执行时间, 没有持久化功能, 也没有 nextval 相关的功能. sequence 也不能生成负数序列, 在达到最大/最小边界的时候不能轮询(类似 PostgreSQL 序列生成器的 CYCLE 选项). MariaDB sequence 使用场景 详细使用参见 mariadbs-sequence

  1. . 找出列中的空洞行
  2. . 生成组合数
  3. . 生成两个数的公约数
  4. . 生成排序的字符
  5. . 生成排序的日期时间等

4. postgresql 序列生成器

postgresql 自带的序列生成器能够很好的实现序列数的需求, 类似 MySQL 的 last_insert_id 方式. 不过 postgresql 的序列包含以下特性:

  1. . 序列可以用于表中的多个字段;
  2. . 序列可以被多个表共用;

创建序列见: sql-createsequence 语法较丰富, 支持很多参数, 可以设置序列的起始值, 上限值, cache 和是否循环等. 序列函数见: functions-sequence 操作序列的函数包括

  1. currval(regclass) bigint 返回最近一次用 nextval 获取的指定序列的数值
  2. lastval() bigint 返回最近一次用 nextval 获取的任何序列的数值
  3. nextval(regclass) bigint 递增序列并返回新值
  4. setval(regclass, bigint) bigint 设置序列的当前数值
  5. setval(regclass, bigint, boolean) bigint 设置序列的当前数值及 is_called 标志

程序调用 currval 函数之前, 都需要执行过 nextval 函数.如果 setval 的 is_called 为 false, 则下次调用 nextval 函数将范围其声明的值, 再次调用 nextval 才会开始递增序列. regclass 类型为相关函数的参数, 这里即序列的名称. 如下所示:

  1. cztest=# create sequence seq1;
  2. CREATE SEQUENCE
  3. cztest=# select nextval('seq1');
  4. nextval
  5. ---------
  6. 1
  7. (1 row)
  8.  
  9. cztest=# select nextval('seq1');
  10. nextval
  11. ---------
  12. 2
  13. (1 row)
  14.  
  15. cztest=# select currval('seq1');
  16. currval
  17. ---------
  18. 2
  19. (1 row)
  20.  
  21. cztest=# select setval('seq1', 1, false);
  22. setval
  23. --------
  24. 1
  25. (1 row)
  26.  
  27. cztest=# select nextval('seq1');
  28. nextval
  29. ---------
  30. 1
  31. (1 row)
  32.  
  33. cztest=# select nextval('seq1');
  34. nextval
  35. ---------
  36. 2
  37. (1 row)

  

使用序列生成器经常碰到的问题

  1. 事务回滚后, 序列不会回滚
  2. 序列的范围基于 bigint 运算, 其范围不超过 8 字节的整数范围.一些老的系统不支持 8 字节的编译器则采用普通的 4 字节 int 运算.
  3. 序列达到上限后, 默认不加 CYCLE 选项, 则会报错, 不允许生成序列, 如果加了 CYCLE 选择, 则从开始值重新生成.
  4. 如果 cache 大于 1, 意味着该会话一次取多个序列, 每次访问序列对象的过程中都将分配并缓存随后的序列值, 并且相应的增加序列对象的 last_value. 从这点看 cache 越大意味着序列的性能越高. 不过同一个事务中随后的 cache - 1 次 nextval 将只返回预先分配的值, 在会话结束前没有使用剩下的值, 会导致序列里出现空洞(不连续). 另外如果有多个会话并发操作同一个序列生成器, 在业务层面来看可能会产生无序的问题, 在 cache 大于 1 的时候, 只能保证 nextval 值唯一, 不能保证顺序生成; 最后, 如果在序列上执行 setval, 则其它会话不会发觉, 直到用光缓存的数为止.

总结

在上述介绍的四种 id 生成方式中, MariaDB 的 sequence 不适合序列生成器的需求. 很多中小业务使用的都是基于 MySQL 的 last_insert_id 方式. 这种方式在单一业务中使用方便, 有多少业务就创建多少对应的表, 不太使用具有分布式特性的业务. 另外很多开源的工具, 如 idgo 就是基于该方式, 只是提供了 redis 协议兼容的接口, 创建多个序列及意味着映射了多个 MySQL 表, 在并发较大的场景下不能避免死锁的发生. 而 PostgreSQL 的序列生成器则是内置的功能, 有很丰富的操作函数, 并发方面比起 MySQL 方式有较好的性能, 比较流行的开源工具 postgrestprest 都提供了 http 接口, 已有的程序改造起来也比较轻松方便. snowflake 方式则比较适合分布式场景的业务, 对时间依赖较强的业务也可以使用该方式, 另外这种方式在性能方面应该是最好的. 已有的开源工具如 sonygoSnowFlake 都做了比较好的实现, 以 http 接口对外服务, 程序改造起来也比较方便. 不过与上述的两种方式相比, 开源的工具并未实现持久化和高可用的功能, 在服务中断的情况下难以继续生成相应的序列, 需要我们做相应的二次开发.

id 生成器介绍的更多相关文章

  1. CosId 通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...

  2. CosId 1.0.0 发布,通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...

  3. CosId 1.0.3 发布,通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...

  4. CosId 1.1.0 发布,通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式系统 ID 生成器. 目前提供了俩大类 ID 生成器:SnowflakeId (单机 TPS ...

  5. CosId 1.1.8 发布,通用、灵活、高性能的分布式 ID 生成器

    CosId 通用.灵活.高性能的分布式 ID 生成器 介绍 CosId 旨在提供通用.灵活.高性能的分布式 ID 生成器. 目前提供了三类 ID 生成器: SnowflakeId : 单机 TPS 性 ...

  6. Java版分布式ID生成器技术介绍

    分布式全局ID生成器作为分布式架构中重要的组成部分,在高并发场景下承载着分担数据库写瓶颈的压力. 之前实现过PHP+Swoole版,性能和稳定性在生产环境下运行良好.这次使用Java进行重写,目前测试 ...

  7. 分布式ID生成器 zz

    简介 这个是根据twitter的snowflake来写的.这里有中文的介绍. 如上图所示,一个64位ID,除了最左边的符号位不用(固定为0,以保证生成的ID都是正数),还剩余63位可用. 下面的代码与 ...

  8. 游戏服务器ID生成器组件

    游戏服务器程序中,经常需要生成全局的唯一ID号,这个功能很常用,本文将介绍一种通用ID生成组件.游戏服务器程序中使用此组件的场景有: 创建角色时,为其分配唯一ID 创建物品时,每个物品需要唯一ID 创 ...

  9. ID生成器的一种可扩展实现方案

    ID生成器主要为了解决业务程序生成记录ID的场景,而一个好的ID生成器肯定要满足扩展性好.并发性好的特点,本文下面介绍一种满足上述特点的实现方案. 此方案的核心思想是:每次需要扩容机器时,将每个节点维 ...

随机推荐

  1. SqlParameter参数类型为int32时候的传值陷阱

    前2天在使用SqlParameter传递参数的时候遇到一个小坑,这里分享一下. SqlParameter para=new SqlParameter("@IsDeleted",0) ...

  2. 2017-4-18 ADO.NET

    1.什么是ADO.NET?     (是一种数据库访问技术) ADO.NET的名称起源于ADO(ActiveX Data Objects),是一个COM组件库,用于在以往的Microsoft技术中访问 ...

  3. 有关 json对象 取出其中数据问题

    这几天,在做一个ajax异步提交的小功能,发现从ashx中传递过来的string 类型的数据,一直拿不到(当时是指的是json点不出来),傻傻的自己,一直在找其他的方法,看看其他那里出了错误,最后,那 ...

  4. 一个全局变量引起的DLL崩溃

    参考我发的帖子: http://bbs.csdn.net/topics/390737064?page=1#post-397000946 现象是exe程序在加载dll的时候崩溃了,莫名其妙的崩溃了.换其 ...

  5. javascript中类式继承和原型式继承的实现方法和区别

    在所有面向对象的编程中,继承是一个重要的话题.一般说来,在设计类的时候,我们希望能减少重复性的代码,并且尽量弱化对象间的耦合(让一个类继承另一个类可能会导致二者产生强耦合).关于“解耦”是程序设计中另 ...

  6. javascript执行原理

    执行环境 当执行流执行到函数时会创建一个执行环境,这个执行环境包含了函数内部 语句可以访问的所有变量和函数,当代码执行完时,销毁执行环境.所以一般情 况下,局部变量在函数执行完时会被销毁. 作用域.调 ...

  7. poj1751最小生成树

    The island nation of Flatopia is perfectly flat. Unfortunately, Flatopia has a very poor system of p ...

  8. Android系统--输入系统(十一)Reader线程_简单处理

    Android系统--输入系统(十一)Reader线程_简单处理 1. 引入 Reader线程主要负责三件事情 获得输入事件 简单处理 上传给Dispatch线程 InputReader.cpp vo ...

  9. Kafka官方文档

    Apache Kafka是 一个分布式消息发布订阅系统.它最初由LinkedIn公司基于独特的设计实现为一个分布式的提交日志系统( a distributed commit log),,之后成为Apa ...

  10. 走进javascript——类型

    ECMAScript语言类型对应于使用ECMAScript语言的ECMAScript程序员直接操作的值.ECMAScript语言类型有以下几种Undefined,Null,Boolean,String ...