分布式唯一全局id生成


一、需求及要求

1、为什么需要分布式全局唯一ID以及分布式ID的业务需求 ?

在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识

如在美团点评的金融、支付、餐饮、酒店;
猫眼电影等产品的系统中数据日渐增长,对数据分库分表后需要有一个唯一ID来标识一条数据或消息;
特别一点的如订单、骑手、优惠券也都需要有唯一ID做标识。

此时一个能够生成全局唯一ID的系统是非常必要的。

2、ID生成规则部分硬性要求

1.全局唯,不能出现重复的ID号,既然是唯一标识,这是最基本的要求

⒉.趋势递增,在MySQL的innoDB引擎中使用的是聚集索引,由于多数RDBMS使用Btree的数据,—结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能

3.单调递增, 保证下一个ID大于上一个ID,例如事务版本号、IM增量信息、排序等特殊需求

4.信息安全,如果ID是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定URL即可能众在二流用场景下需要ID无规则不规则,让竞争对手不好猜

5.含时间戳,这样就能在开发中快速了解分布式id的生成时间

提示:以下是本篇文章正文内容,下面案例可供参考

3、ID号生成系统的可用性要求

高可用,发一个获取分布式ID的请求,服务器就要保证99.999%的情况下给我创建一个唯一分布式ID
低延迟,发一个获取分布式ID的请求,服务器就要快,极速
搞QPS,假如并发一口气创建分布式ID请求同时杀过来,服务器要顶得住且一下子成功创建10万

二、一般通用方案

1、UUID

**UUID(Universally Unique ldentifier)**的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000

性能非常高:本地生成,没有网络消耗(如果只考虑唯一性,OK )

2、数据库自增主键

(1)单机模式下

在分布式里面,数据库的自增ID机制的主要原理是:数据库自增ID和mysql数据库的replace into实现的。

这里的replace into跟insert功能类似,
不同点在于:replace into首先尝试插入数据列表中,如果发现表中已经有此行数据(根据主健或唯一索引判断)则先删除,再插入。否则直接插入新数据。

缺点:
REPLACE INTO的含义是插入一条记录,如果表中唯一索引的值遇到冲突,则替换老数据。

(2)集群模式下

那数据库自增ID机制适合作分布式ID吗?答案是不太适合

1:系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,23,4.5(步长个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,貌似还好,现在想象一下如果我们线上有100台个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。

2数据库压力还是很大,每次获取ID都得读写一次数据库,非常影响性能,不符合分布式ID里面的延迟低和要高QPS的规则(在高并发下.去数据库里面获取id,那是非常影响性能的·

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

优点:实现简单,ID单调自增,数值类型查询速度快

缺点:DB单点存在宕机风险,无法扛住高并发场景

3、基于redis生成全局id策略

Redis 也同样可以实现,因为Redis是单线的天生保证原子性,可以使用原子操作INCR和INCRBY来实现,原理就是Redis 是单线程的,因此我们可以利用redis的incr命令实现ID的原子性自增。

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

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

三、基于雪花算法(Snowflake)模式

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。在分布式系统中的应用十分广泛,且ID 引入了时间戳,为什么叫雪花算法呢?私以为众所周知世界上没有一对相同的雪花。雪花算法基本上保持自增的,后面的代码中有详细的注解。这 64 个 bit 中,其中 1 个 bit 是不用的,然后用其中的 41 bit 作为毫秒数,用 10 bit 作为工作机器 id,12 bit 作为序列号。举例如上图:
在这里插入图片描述

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

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

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

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

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

总结:简单来说,你的某个服务假设要生成一个全局唯一 id,那么就可以发送一个请求给部署了 SnowFlake 算法的系统,由这个 SnowFlake 算法系统来生成唯一 id。

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

接着 SnowFlake 算法系统接收到这个请求之后,首先就会用二进制位运算的方式生成一个 64 bit 的 long 型 id,64 个 bit 中的第一个 bit 是无意义的。

接着 41 个 bit,就可以用当前时间戳(单位到毫秒),然后接着 5 个 bit 设置上这个机房 id,还有 5 个 bit 设置上机器 id。

最后再判断一下,当前这台机房的这台机器上这一毫秒内,这是第几个请求,给这次生成 id 的请求累加一个序号,作为最后的 12 个 bit。最终一个 64 个 bit 的 id 就出来了,类似于:这个算法可以保证一个机房的一台机器在同一毫秒内,生成了一个唯一的 id。可能一个毫秒内会生成多个 id,但是有最后 12 个 bit 的序号来区分开来。

总结:就是用一个 64 bit 的数字中各个 bit 位来设置不同的标志位,区分每一个 id。

SnowFlake 算法的实现代码如下:

/**
 * 雪花算法相对来说如果思绪捋顺了实现起来比较简单,前提熟悉位运算。
 */
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算法的优点

高性能高可用:生成时不依赖于数据库,完全在内存中生成。

容量大:每秒中能生成数百万的自增ID。

ID自增:存入数据库中,索引效率高。

SnowFlake算法的缺点

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

实际中我们的机房并没有那么多,我们可以改进改算法,将10bit的机器id优化成业务表或者和我们系统相关的业务。

糊涂工具包

https://github.com/twitter-archive/snowflake/releases/tag/snowflake-2010
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北街风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值