业务量小于500W或数据容量小于2G的时候单独一个mysql即可提供服务,再大点的时候就进行读写分离也可以应付过来。但当主从同步也扛不住的是就需要分表分库了,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;特别一点的如订单、优惠券也都需要有唯一ID做标识。此时一个能够生成全局唯一ID的系统是非常必要的。那么这个全局唯一ID就叫分布式ID。

分布式ID需满足那些条件:

1、全局唯一:基本要求就是必须保证ID是全局性唯一的。
2、高性能:高可用低延时,ID生成响应要快。
3、高可用:无限接近于100%的可用性。
4、好接入:遵循拿来主义原则,在系统设计和实现上要尽可能的简单。
5、趋势递增:最好趋势递增。

UUID

UUID 是指Universally Unique Identifier,翻译为中文是通用唯一识别码,UUID 的目的是让分布式系统中的所有元素都能有唯一的识别信息

import java.util.UUID;
 public static void main(String[] args) {
  String uuid = UUID.randomUUID().toString().replaceAll("-","");
  System.out.println(uuid);
 }

输出结果 5477d0925b294a53b2f4db9d5a3fb798 ,但UUID却并不适用于实际的业务需求。订单号用UUID这样的字符串没有丝毫的意义,看不出和订单相关的有用信息;而对于数据库来说用作业务主键ID,它不仅是太长还是字符串,存储性能差查询也很耗时,所以不推荐用作分布式ID。

优点:生成足够简单,本地生成无网络消耗,具有唯一性。
缺点:没有具体的业务含义,查询也很耗时。

基于数据库自增ID

基于数据库的auto_increment自增ID完全可以充当分布式ID,具体实现:需要一个单独的MySQL实例用来生成ID,建表结构如下:

CREATE DATABASE `SoWhat_ID`;
CREATE TABLE SoWhat_ID.SEQUENCE_ID (
    `id` bigint(20) unsigned NOT NULL auto_increment, 
    `value` char(10) NOT NULL default '',
    `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
) ENGINE=MyISAM;
insert into SEQUENCE_ID(value) VALUES ('values');

当我们需要一个ID的时候,向表中插入一条记录返回主键ID,但这种方式有一个比较致命的缺点,访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

优点:实现简单,ID单调自增,数值类型查询速度快。
缺点:DB单点存在宕机风险,无法扛住高并发场景。

基于数据库集群模式

前边说了单点数据库方式不可取,那对上边的方式做一些高可用优化,换成主从模式集群。害怕一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。那这样还会有个问题,两个MySQL实例的自增ID都从1开始,会生成重复的ID怎么办?解决方案:设置起始值和自增步长

MySQL_1 配置:

# 起步值
set @@auto_increment_offset = 1;
# 步长
set @@auto_increment_increment = 2;

MySQL_2 配置:

# 起步值
set @@auto_increment_offset = 2;
# 步长
set @@auto_increment_increment = 2;

两个MySQL实例的自增ID分别就是:

1、3、5、7、9 
2、4、6、8、10

但是如果两个还是无法满足咋办呢?增加第三台MySQL实例需要人工修改一、二两台MySQL实例的起始值和步长,把第三台机器的ID起始生成位置设定在比现有最大自增ID的位置远一些,但必须在一、二两台MySQL实例ID还没有增长到第三台MySQL实例的起始ID值的时候,否则自增ID就要出现重复了,必要时可能还需要停机修改。

优点:解决DB单点问题。
缺点:不利于后续扩容,而且实际上单个数据库自身压力还是大,依旧无法满足高并发场景。

基于数据库的号段模式

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

CREATE TABLE id_generator (
  `id` int(10) NOT NULL,
  `max_id` bigint(20) NOT NULL COMMENT '当前最大id',
  `step` int(20) NOT NULL COMMENT '号段的步长',
  `biz_type`    int(20) NOT NULL COMMENT '业务类型',
  `version` int(20) NOT NULL COMMENT '版本号',
  PRIMARY KEY (`id`)
)

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

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

update id_generator set max_id = {max_id+step}, version = version + 1
 where version =  {version} and biz_type = XX

由于多业务端可能同时操作,所以采用版本号 version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。但是如果遇到了双十一或者秒杀类似的活动还是会对数据库有比较高的访问。

基于Redis模式

Redis 也同样可以实现,原理就是Redis 是单线程的,因此我们可以利用redis的incr命令实现ID的原子性自增。

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

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

基于雪花算法

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。使用一个 64 bit 的 long 型的数字作为全局唯一 id。

1、第一个部分是 1 个 bit:0,这个是无意义的。因为二进制里第一个 bit 位如果是 1,那么都是负数,但是我们生成的 id 都是正数,所以第一个 bit 统一都是 0。

2、第二个部分是 41 个 bit:表示的是时间戳。单位是毫秒。41 bit 可以表示的数字多达 2^41 - 1,也就是可以标识 2 ^ 41 - 1 个毫秒值,换算成年就是表示69年的时间。

3、第三个部分是 5 个 bit:表示的是机房 id 5 个 bit 代表机器 id。意思就是最多代表 2 ^ 5 个机房(32 个机房)。

4、第四个部分是 5 个 bit:表示的是机器 id。每个机房里可以代表 2 ^ 5 个机器(32 台机器),也可以根据自己公司的实际情况确定。

5、第五个部分是 12 个 bit:表示的序号,就是某个机房某台机器上这一毫秒内同时生成的 id 的序号。12 bit 可以代表的最大正整数是 2 ^ 12 - 1 = 4096,也就是说可以用这个 12 bit 代表的数字来区分同一个毫秒内的 4096 个不同的 id。

这个 SnowFlake 算法系统首先肯定是知道自己所在的机房和机器的,比如机房 id = 17,机器 id = 12。

/**
 * 雪花算法
 */
public class SnowFlake
{
 /**
  * 开始时间截 (2015-01-01)
  */
 private final long twepoch = 1420041600000L;

 /**
  * 机器id所占的位数
  */
 private final long workerIdBits = 5L;

 /**
  * 数据标识id所占的位数
  */
 private final long dataCenterIdBits = 5L;

 /**
  * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
  */
 private final long maxWorkerId = ~(-1L << workerIdBits);

 /**
  * 支持的最大机房标识id,结果是31
  */
 private final long maxDataCenterId = ~(-1L << dataCenterIdBits);

 /**
  * 序列在id中占的位数
  */
 private final long sequenceBits = 12L;

 /**
  * 机器ID向左移12位
  */
 private final long workerIdShift = sequenceBits;

 /**
  * 机房标识id向左移17位(12+5)
  */
 private final long dataCenterIdShift = sequenceBits + workerIdBits;

 /**
  * 时间截向左移22位(5+5+12)
  */
 private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;

 /**
  * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
  */
 private final long sequenceMask = ~(-1L << sequenceBits);

 /**
  * 工作机器ID(0~31)
  */
 private volatile long workerId;

 /**
  * 机房中心ID(0~31)
  */
 private volatile long dataCenterId;

 /**
  * 毫秒内序列(0~4095)
  */
 private volatile long sequence = 0L;

 /**
  * 上次生成ID的时间截
  */
 private volatile long lastTimestamp = -1L;

 //==============================Constructors=====================================

 /**
  * 构造函数
  *
  * @param workerId     工作ID (0~31)
  * @param dataCenterId 机房中心ID (0~31)
  */

 public SnowFlake(long workerId, long dataCenterId)
 {
  if (workerId > maxWorkerId || workerId < 0)
  {
   throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
  }
  if (dataCenterId > maxDataCenterId || dataCenterId < 0)
  {
   throw new IllegalArgumentException(String.format("dataCenter Id can't be greater than %d or less than 0", maxDataCenterId));
  }
  this.workerId = workerId;
  this.dataCenterId = dataCenterId;
 }

 // ==============================Methods==========================================

 /**
  * 获得下一个ID (该方法是线程安全的)
  * 如果一个线程反复获取Synchronized锁,那么synchronized锁将变成偏向锁。
  *
  * @return SnowflakeId
  */
 public synchronized long nextId() throws RuntimeException
 {
  long timestamp = timeGen();

  //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
  if (timestamp < lastTimestamp)
  {
   throw new RuntimeException((String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)));

  }

  //如果是毫秒级别内是同一时间生成的,则进行毫秒内序列生成
  if (lastTimestamp == timestamp)
  {
   sequence = (sequence + 1) & sequenceMask;
   //毫秒内序列溢出,一毫秒内超过了4095个
   if (sequence == 0)
   {
    //阻塞到下一个毫秒,获得新的时间戳
    timestamp = tilNextMillis(lastTimestamp);
   }
  }
  else
  {
   //时间戳改变,毫秒内序列重置
   sequence = 0L;
  }

  //上次生成ID的时间截
  lastTimestamp = timestamp;

  //移位并通过或运算拼到一起组成64位的ID
  return ((timestamp - twepoch) << timestampLeftShift)
    | (dataCenterId << dataCenterIdShift)
    | (workerId << workerIdShift)
    | sequence;
 }

 /**
  * 阻塞到下一个毫秒,直到获得新的时间戳
  * @param lastTimestamp 上次生成ID的时间截
  * @return 当前时间戳
  */
 private long tilNextMillis(long lastTimestamp)
 {
  long timestamp = timeGen();
  while (timestamp <= lastTimestamp)
  {
   timestamp = timeGen();
  }
  return timestamp;
 }

 /**
  * 返回以毫秒为单位的当前时间
  * @return 当前时间(毫秒)
  */
 private long timeGen()
 {
  return System.currentTimeMillis();
 }
}

SnowFlake算法的优点:

1、高性能高可用:生成时不依赖于数据库,完全在内存中生成。
2、容量大:每秒中能生成数百万的自增ID。
3、ID自增:存入数据库中,索引效率高。

SnowFlake算法的缺点:

1、依赖与系统时间的一致性,如果系统时间被回调,或者改变,可能会造成id冲突或者重复。

其他大厂分布式Id

1、百度 uid-generator

项目GitHub地址:https://github.com/baidu/uid-generator

uid-generator基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳、工作机器ID和 序列号等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。

2、 美团 Leaf

项目GitHub地址:https://github.com/Meituan-Dianping/Leaf

Leaf同时支持号段模式和snowflake算法模式,可以切换使用。

3、 滴滴 Tinyid

项目GitHub地址:https://github.com/didi/tinyid

Tinyid是一个ID生成器服务,它提供了REST API和Java客户端两种获取方式,如果使用Java客户端获取方式的话,官方宣称能单实例能达到1kw QPS