七种企业级分布式全局唯一ID生成策略

                                 

目录

                                 

                                                                          分布式全局唯一ID生成策略

高并发情况下,生成分布式全局id策略

UUID方式

基于数据库自增方式

数据库集群如何考虑数据库自增唯一性

基于Redis生成全局id策略

Twitter的snowflake(雪花)算法

基于Zookeeper实现全局ID


                                    分布式全局唯一ID生成策略

高并发情况下,生成分布式全局id策略

  • 注意幂等性且全局唯一性
  • 注意安全性,不能被猜疑
  • 趋势递增性

订单号命名规则:比如业务编码 + 时间戳 + 机器编号[4] + 随机4位数 + 毫秒数

UUID方式

基本概念:

UUID是指在一台机器上生成的数字,它保证对在同一时空中(全世界)的所有机器都是唯一的。

UUID组成部分:

当前日期和时间+时钟序列+随机数+全局唯一的IEEE机器识别号

全局唯一的IEEE机器识别号:如果有网卡,从网卡MAC地址获得,没有网卡以其他方式获得。

优点:

  1. 简单,代码方便;
  2. 生成ID性能非常好,基本不会有性能问题;
  3. 全球唯一,在遇见数据迁移,系统数据合并,或者数据库变更等情况下,可以从容应对。

缺点:

  1. 没有排序,无法保证趋势递增;
  2. UUID往往是使用字符串存储,查询的效率比较低;
  3. 存储空间比较大,如果是海量数据库,就需要考虑存储量的问题;
  4. 传输数据量大;
  5. 不需要通讯,不占用带宽

应用场景:

一般UUID在数据库主键、生成Token领域使用比较多。

实现:

public static String produceUUID() {

    return UUID.randomUUID().toString().replace("-", "");

}

UUID唯一性问题:

       结论:可能重复,但条件很苛刻,你在Google工作也不一定能遇到这种场景

重复是肯定会重复的,32位的UUID,经过16^32+1次生成后,必然会产生至少一次重复,当然,不追求这个必然,偶然产生一次重复需要的平均次数比这个少得多。 不过……后面转折来了,16^32=……没算错的话有39位数啊,你每秒并发10000次(你在Google工作也不一定能遇到这种场景),3600秒×24小时×365天不断的并发,需要连续并发1000000……000000年(27个0,我不知道怎么读了),基本上宇宙毁灭之前,你也很难遇到重复的情况,SO,如果你认为纯粹靠运气连续100年每天中一次500W的事情是“绝对”不可能出现的话(这个概率比UUID重复来的还要容易些),UUID就是“绝对”不可能重复的。

基于数据库自增方式

实现思路:

利用数据库自增或者序列号方式实现订单号。

注意:

在数据库集群环境下,默认自增方式存在问题,因为都是从1开始自增,可能会存在重复,应该设置每台不同数据库自增的间隔方式不同。

优点:

  1. 简单,代码方便,性能可以接受。
  2. 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

 

  1. 不同数据库语法和实现不同,数据库迁移的时候或多数据库版本支持的时候需要处理。
  2. 在性能达不到要求的情况下,比较难于扩展。
  3. 在单个数据库或读写分离或一主多从的情况下,只有一个主库可以生成。有单点故障的风险。
  4. 分表分库的时候会有麻烦。

数据库集群如何考虑数据库自增唯一性

定义:

       在数据库集群环境下,默认自增方式存在问题,因为都是从1开始自增,可能会存在重复,应该设置每台节点自增步长不同。

          步长:指每一次自增的长度,默认为1,也就是每次都加1。(数据库基础知识)

实现:

查询自增的步长

SHOW VARIABLES LIKE 'auto_inc%'

修改自增的步长

SET @@auto_increment_increment=10;

修改起始值

SET @@auto_increment_offset=5;

假设有两台mysql数据库服务器

节点①自增  1 3 5 7 9 11 ….

节点②自增  2 4 6 8 10 12 ….

实际情况也可使用数据库工具进行修改

缺点:

        ① 集群无法进行横向扩展。

        ② 在最开始设置好了每台节点自增方式步长后,确定好了mysql集群数量后,无法扩展新的mysql,不然生成步长的规则可          能会发生变化。

 

基于Redis生成生成全局id策略

              因为Redis是单线程的,天生保证原子性,故可以使用Redis的原子操作 INCR和INCRBY来实现。

优点: 

 

  1. 不依赖于数据库,灵活方便,且性能优于数据库;
  2. 数字ID天然排序,对分页或者需要排序的结果很有帮助;
  3. Redis在高并发下先天具备安全性

缺点:

  1. 如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
  2. 需要编码和配置的工作量比较大。

注意:Redis集群情况下,同样和Redis一样需要设置不同的增长步长,同时key一定要设置有效期

提示:可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis。可以初始化每台Redis的值分别是1,2,3,4,5,然后设置步长都是5。各个Redis生成的ID为:

                       A:1,6,11,16,21

                       B:2,7,12,17,22

                       C:3,8,13,18,23

                       D:4,9,14,19,24

                       E:5,10,15,20,25

应用场景:

        比较适合使用Redis来生成每天从0开始的流水号。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

         如果生成的订单号超过自增增长的话,可以采用前缀+自增+并且设置有效期

Redis实现自增全局ID:

Maven依赖:

<parent>

         <groupId>org.springframework.bootgroupId>

         <artifactId>spring-boot-starter-parentartifactId>

         <version>2.0.1.RELEASEversion>

     parent>

     <dependencies>

        

         <dependency>

              <groupId>org.springframework.bootgroupId>

              <artifactId>spring-boot-starter-webartifactId>

         dependency>

        

         <dependency>

              <groupId>org.springframework.bootgroupId>

              <artifactId>spring-boot-starter-data-redisartifactId>

         dependency>

         <dependency>

              <groupId>org.springframework.bootgroupId>

              <artifactId>spring-boot-starter-testartifactId>

         dependency>

 

     dependencies>

 

 

spring:

  redis:

    database: 1

    host: 127.0.0.1

    port: 6379

    password: 123456

    jedis:

      pool:

        max-active: 8

        max-wait: -1

        max-idle: 8

        min-idle: 0

    timeout: 10000

   

 

 

 

      public static String prefix() {

           String temp_str = "";

           Date dt = new Date();

           // 最后的aa表示上午下午” HH表示24小时制 如果换成hh表示12小时制

           SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");

           temp_str = sdf.format(dt);

           return temp_str;

      }

 

      @RequestMapping("/order")

      public String order(String key) {

// 考虑key失效时间的问题,一般设置为1

           RedisAtomicLong redisAtomicLong = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());

           long andIncrement = redisAtomicLong.getAndIncrement();

           String orderId = prefix() + "-" + String.format("%1$05d", andIncrement); // 不足补0

           return orderId;

      }

 

Twittersnowflake(雪花)算法

概念:

    snowflake是Twitter开源的分布式ID生成算法,结果是一个long型的ID。其核心思想是:

               高位随机+毫秒数+机器码(数据中心+机器id+10位的流水号码

    Github地址: https://github.com/twitter-archive/snowflake

Snowflake 原理:

snowflake生产的ID是一个18位的long型数字,二进制结构表示如下(每部分用-分开):

0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000 - 00000 - 00000000 0000

第一位未使用,接下来的41位为毫秒级时间(41位的长度可以使用69年,从1970-01-01 08:00:00),然后是5位datacenterId(最大支持2^5=32个,二进制表示从00000-11111,也即是十进制0-31),和5位workerId(最大支持2^5=32个,原理同datacenterId),所以datacenterId*workerId最多支持部署1024个节点,最后12位是毫秒内的计数(12位的计数顺序号支持每个节点每毫秒产生2^12=4096个ID序号).所有位数加起来共64位,恰好是一个Long型(转换为字符串长度为18).单台机器实例,通过时间戳保证前41位是唯一的,分布式系统多台机器实例下,通过对每个机器实例分配不同的datacenterId和workerId避免中间的10位碰撞。最后12位每毫秒从0递增生产ID,再提一次:每毫秒最多生成4096个ID,每秒可达4096000个。

雪花算法实现:

/**

 * Twitter_Snowflake

 * 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个节点,包括5datacenterId5workerId

 * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096ID序号

 * 加起来刚好64位,为一个Long型。

 * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,

 * SnowFlake每秒能够产生26ID左右。

 */

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 < 100; i++) {

           long id = idWorker.nextId();

           String insertSQL = "insert into orderNumber value('" + id + "');";

           System.out.println(insertSQL);

       }

    }

}

 

基于Zookeeper实现全局ID

Session是Zookeeper中的会话实体,代表了一个客户端会话。SessionID用来唯一标识一个会话,因此Zookeeper必须保证sessionID的全局唯一性,在每次客户端向服务端发起"会话创建"请求时,服务端都会为其分配一个sessionID。那么Zookeeper是如何实现的呢?

在SessionTracker初始化的时候,会调用initializeNextSession方法来生成一个初始化的sessionID,之后在Zookeeper的正常运行过程中,会在该sessionID的基础上为每个会话进行分配。

优点:可靠,不会重复,无单点问题

缺点:只能分配32位序列号;效率不高

性能:生成1w条,用时62411毫秒,平均生成用时6.24毫秒


          com.github.adyliu
          zkclient
          2.1.1

package com.xx.xx;
 
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
 
import com.github.zkclient.ZkClient;
import org.apache.zookeeper.data.Stat;
 
/**
 * Created by xdc on 2017/2/27 15:24.
 */
public class Test {
    
    public static final String SEQ_ZNODE = "/seq";
    static long s = System.currentTimeMillis();
 
    //通过znode数据版本实现分布式seq生成
    public static class Task1 implements Runnable {
        private final String taskName;
        public Task1(String taskName) {
            this.taskName = taskName;
        }
 
        public void run() {
            ZkClient zkClient = new ZkClient("127.0.0.1:2181", 3000, 50000);
            Stat stat =zkClient.writeData(SEQ_ZNODE, new byte[0], -1);
            int versionAsSeq = stat.getVersion();
            System.out.println(taskName + " obtain seq=" +versionAsSeq );
           
            if(versionAsSeq == 9999){
                System.out.println(System.currentTimeMillis() - s);
            }
            zkClient.close();
        }
    }
 
    public static void main(String[] args) {
        
        final ExecutorService service = Executors.newFixedThreadPool(20);
 
        for (int i = 0; i < 10000; i++) {
            service.execute(new Task1("[Concurrent-" + i + "]"));
        }
    }
}

 

你可能感兴趣的:(分布式解决方案)