分布式唯一ID

唯一id的四个要求

  • 全局唯一性:不能出现重复的ID号,既然是唯一标识,这是最基本的要求。
  • 趋势递增:在MySQL InnoDB引擎中使用的是聚集索引,由于多数RDBMS使用B+ tree的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 信息安全:如果ID是连续的,恶意用户的扒取工作,比如爬订单号,对手可以直接知道我们一天的单量。
  • 在一些应用场景下,会需要ID无规则、不规则。

UUID

  • 介绍
    • UUID(Universally Unique Identifier)的标准型式包含32个16进制数字,以连字号分为五段,形式为8-4-4-4-12的36个字符,示例:550e8400-e29b-41d4-a716-446655440000
  • 优点
    • 性能非常高:本地生成,没有网络消耗。
  • 缺点
    • 存储不方便:UUID太长了,没有合适的数据可以表示128位的数字,因此通常用长度为36的string类型表示,这会占用36 bytes
    • 信息不安全基于MAC地址生成UUID的算法可能会造成MAC地址泄露。
    • 无序:UUID并不能保证顺序

雪花算法

  • 不依赖于数据库,强依赖于时间戳
  • 只需要用一个long类型就可以表示,从现在看开始,41位时间戳可以表示 2 31 2^{31} 231秒,一天是86400秒,这样算起来,差不多就是 2 16 ∗ 1.5 2^{16}*1.5 2161.5的样子,所以可以表示大于 2 1 4 = 16384 2^14=16384 214=16384天,因此至少是60年(实际上是69年),基本上没有任何一款应用可以撑到69的
  • 1符号位+41位的毫秒时间戳+5位机房号+5位机器号+12位毫秒内自增id
  • 当机器故障,发生时间回迁的时候,很可能会出现重复id,不能完全保障可重复性
  • 具体实现的时候,一旦发生时间回迁,就会抛出异常,这会导致不可用
  • 实现
/** 
 * SnowFlake的结构如下(每部分用-分开)
 * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 
 * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0 
 * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
 * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。
 * 41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69 
 * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId 
 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号
* 加起来刚好64位,为一个Long型。 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。 */
public class SnowflakeIdWorker { // ==============================Fields=========================================== /** 开始时间截 (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 ^ (-1L << workerIdBits); /** 支持的最大数据标识id,结果是31 */ private final long maxDatacenterId = -1L ^ (-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 ^ (-1L << sequenceBits); /** 工作机器ID(0~31) */ private long workerId; /** 数据中心ID(0~31) */ private long datacenterId; /** 毫秒内序列(0~4095) */ private long sequence = 0L; /** 上次生成ID的时间截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 构造函数 * @param workerId 工作ID (0~31) * @param datacenterId 数据中心ID (0~31) */ public SnowflakeIdWorker(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 (该方法是线程安全的) * @return SnowflakeId */ public synchronized long nextId() { 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; //毫秒内序列溢出 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 当前时间戳 */ protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } /** * 返回以毫秒为单位的当前时间 * @return 当前时间(毫秒) */ protected long timeGen() { return System.currentTimeMillis(); } //==============================Test============================================= /** 测试 */ public static void main(String[] args) { SnowflakeIdWorker idWorker = new SnowflakeIdWorker(0, 0); for (int i = 0; i < 1000; i++) { long id = idWorker.nextId(); System.out.println(Long.toBinaryString(id)); System.out.println(id); } } }

数据库分段

  • 有人可能会说,既然雪花算法是通过保障全局自增来确保id不重复,那么数据库id不就可以吗
  • 这是可以的,但是会导致以下问题
    • 每次获取id号都需要访问数据库,io性能低
    • 当数据库崩溃的时候,会导致不可用,即便是主从模型的数据库,也会因为主从复制不完全的问题,导致不同步,从而产生id重复的问题
  • 解决方案,利用N个主节点的数据库集群,设置自增步长为N,每台起点设置为i,比如有两台机器
    TicketServer1:
    auto-increment-increment = 2
    auto-increment-offset = 1
    
    TicketServer2:
    auto-increment-increment = 2
    auto-increment-offset = 2
    
  • 这时候,只需要利用代理对每个用户的请求进行负载均衡,打到各个数据库上就可以,如下图所示
    分布式唯一ID_第1张图片- 这样的话,一来能保证同一个数据库上的id不同的,二来可以保证不同数据库之间的id是不同的,所以即便有宕机发生,也不会导致重复id
  • 缺点
    • 比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。
      • 可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数,然后修改第一台的步长为2
      • 如果我们线上可能从1台机器新增到100台的时候,这个时候要扩容会很困难,尤其是“将n+1台机器的初始值设置的比n台机器大很多”这个要求,会随着机器的增多,而难以控制

Leaf-初级版本

分布式唯一ID_第2张图片

  • 将每次获取id的操作,改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。
  • 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响。
  • 如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对biz_tag分库分表就行。

参考

  • leaf开源资料

你可能感兴趣的:(笔记)