基于UUID

  1. String uuid = UUID.randomUUID().toString().replaceAll("-","");
  2. System.out.println(uuid);

优点:

  • 生成足够简单,本地生成无网络消耗,具有唯一性

缺点:

  • 无序的字符串,不具备趋势自增特性
  • 没有具体的业务含义
  • 长度过长16 字节128位,36位长度的字符串,存储以及查询对MySQL的性能消耗较大,MySQL官方明确建议主键要尽量越短越好,作为数据库主键 UUID 的无序性会导致数据位置频繁变动,严重影响性能。

基于数据库的号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存。表结构如下:

  1. CREATE TABLE id_generator (
  2. id int(10) NOT NULL,
  3. max_id bigint(20) NOT NULL COMMENT '当前最大id',
  4. step int(20) NOT NULL COMMENT '号段的布长',
  5. biz_type int(20) NOT NULL COMMENT '业务类型',
  6. version int(20) NOT NULL COMMENT '版本号',
  7. PRIMARY KEY (`id`)
  8. )

biz_type :代表不同业务类型
max_id :当前最大的可用id
step :代表号段的长度
version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

id biz_type max_id step version
1 101 1000 2000 0

等这批号段ID用完,再次向数据库申请新号段,对max_id字段做一次update操作,update max_id= max_id + step,update成功则说明新号段获取成功,新的号段范围是(max_id ,max_id +step]

  1. update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

基于Redis模式

Redis也同样可以实现,原理就是利用redisincr命令实现ID的原子性自增。

  1. 127.0.0.1:6379> set seq_id 1 // 初始化自增ID为1
  2. OK
  3. 127.0.0.1:6379> incr seq_id // 增加1,并返回递增后的数值
  4. (integer) 2

redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDBAOF

  • RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。
image.png
Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。
Snowflake ID组成结构:正数位(占1比特)+ 时间戳(占41比特)+ 机器ID(占5比特)+ 数据中心(占5比特)+ 自增值(占12比特),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年
  • 工作机器id(10bit):也被叫做workId,这个可以灵活配置,机房或者机器号组合都可以。
  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

  1. /**
  2. * Twitter的SnowFlake算法,使用SnowFlake算法生成一个整数,然后转化为62进制变成一个短地址URL
  3. *
  4. * https://github.com/beyondfengyu/SnowFlake
  5. */
  6. public class SnowFlakeShortUrl {
  7. /**
  8. * 起始的时间戳
  9. */
  10. private final static long START_TIMESTAMP = 1480166465631L;
  11. /**
  12. * 每一部分占用的位数
  13. */
  14. private final static long SEQUENCE_BIT = 12; //序列号占用的位数
  15. private final static long MACHINE_BIT = 5; //机器标识占用的位数
  16. private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数
  17. /**
  18. * 每一部分的最大值
  19. */
  20. private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
  21. private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
  22. private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
  23. /**
  24. * 每一部分向左的位移
  25. */
  26. private final static long MACHINE_LEFT = SEQUENCE_BIT;
  27. private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
  28. private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
  29. private long dataCenterId; //数据中心
  30. private long machineId; //机器标识
  31. private long sequence = 0L; //序列号
  32. private long lastTimeStamp = -1L; //上一次时间戳
  33. private long getNextMill() {
  34. long mill = getNewTimeStamp();
  35. while (mill <= lastTimeStamp) {
  36. mill = getNewTimeStamp();
  37. }
  38. return mill;
  39. }
  40. private long getNewTimeStamp() {
  41. return System.currentTimeMillis();
  42. }
  43. /**
  44. * 根据指定的数据中心ID和机器标志ID生成指定的序列号
  45. *
  46. * @param dataCenterId 数据中心ID
  47. * @param machineId 机器标志ID
  48. */
  49. public SnowFlakeShortUrl(long dataCenterId, long machineId) {
  50. if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
  51. throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
  52. }
  53. if (machineId > MAX_MACHINE_NUM || machineId < 0) {
  54. throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
  55. }
  56. this.dataCenterId = dataCenterId;
  57. this.machineId = machineId;
  58. }
  59. /**
  60. * 产生下一个ID
  61. *
  62. * @return
  63. */
  64. public synchronized long nextId() {
  65. long currTimeStamp = getNewTimeStamp();
  66. if (currTimeStamp < lastTimeStamp) {
  67. throw new RuntimeException("Clock moved backwards. Refusing to generate id");
  68. }
  69. if (currTimeStamp == lastTimeStamp) {
  70. //相同毫秒内,序列号自增
  71. sequence = (sequence + 1) & MAX_SEQUENCE;
  72. //同一毫秒的序列数已经达到最大
  73. if (sequence == 0L) {
  74. currTimeStamp = getNextMill();
  75. }
  76. } else {
  77. //不同毫秒内,序列号置为0
  78. sequence = 0L;
  79. }
  80. lastTimeStamp = currTimeStamp;
  81. return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
  82. | dataCenterId << DATA_CENTER_LEFT //数据中心部分
  83. | machineId << MACHINE_LEFT //机器标识部分
  84. | sequence; //序列号部分
  85. }
  86. public static void main(String[] args) {
  87. SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);
  88. for (int i = 0; i < (1 << 4); i++) {
  89. //10进制
  90. System.out.println(snowFlake.nextId());
  91. }
  92. }
  93. }

百度(uid-generator)

uid-generator是由百度技术部开发,项目GitHub地址 https://github.com/baidu/uid-generator
uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳工作机器ID序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

美团(Leaf)

Leaf由美团开发,github地址:https://github.com/Meituan-Dianping/Leaf
Leaf同时支持号段模式和snowflake算法模式,可以切换使用。