• 分布式系统-主键唯一id,订单编号生成-雪花算法-SnowFlake


    • 分布式系统下 我们每台设备(分布式系统-独立的应用空间-或者docker环境)
      * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
    • 所以我们可以为分布式系统下:分库分表主键,分库,多库的情况下的订单编号使用这种方式进行唯一number操作
    • 虽然这种方法正常情况下还是可以凑合用的,但是假如设备出现时间差,在极度大的并发情况下,还是会出现问题的,设备掩码4095,
    • 为这个方案所支持的最小划分粒度是「毫秒 * 线程」,单线程(Snowflake 里对应的概念是 Worker)的每秒容量是12-bit,也就是接近4096.
    • 当时钟回拨则可能出现服务挂起,处于不可用状态,有哪些时间会发生呢,比如遇到闰秒时间等等情况.

      当然有些其他的办法:像专门使用数据库生成唯一id,加入需要多N台设备,每一台设备的起始值不同,步长则为N,例如:要部署N台机器,步长需设置为N,每台的初始值依次为0,1,2...N-1,

      但是这种有很大缺点,一开始就要整合好:缺点如下:

    • 系统水平扩展比较困难,比如定义好了步长和机器台数之后,如果要添加机器该怎么做?假设现在只有一台机器发号是1,2,3,4,5(步长是1),这个时候需要扩容机器一台。可以这样做:把第二台机器的初始值设置得比第一台超过很多,比如14(假设在扩容时间之内第一台不可能发到14),同时设置步长为2,那么这台机器下发的号码都是14以后的偶数。然后摘掉第一台,把ID值保留为奇数,比如7,然后修改第一台的步长为2。让它符合我们定义的号段标准,对于这个例子来说就是让第一台以后只能产生奇数。扩容方案看起来复杂吗?貌似还好,现在想象一下如果我们线上有100台机器,这个时候要扩容该怎么做?简直是噩梦。所以系统水平扩展方案复杂难以实现。
    • ID没有了单调递增的特性,只能趋势递增,这个缺点对于一般业务需求不是很重要,可以容忍。
    • 数据库压力还是很大,每次获取ID都得读写一次数据库,只能靠堆机器来提高性能。

      

      Leaf方案实现

      Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话:There are no two identical leaves in the world "世界上没有两片相同的树叶"

      综合对比上述几种方案,每种方案都不完全符合我们的要求。所以Leaf分别在上述第二种和第三种方案上做了相应的优化,实现了Leaf-segment和Leaf-snowflake方案。

      1.Leaf-segment数据库方案 2. Leaf-snowflake方案  具体可参考搜索. 也可去搜索一下 zookeeper的分布式唯一

    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个节点,包括5位datacenterId和5位workerId,12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号加起来刚好64位,为一个Long型。

    唯一的缺点是设备时间要稳定,不能出现回退.

    下面就是我的代码: 没有代码不就是白瞎了?

      1 package test;
      2 
      3 /**
      4  * @Title: SnowFlakeUtils.java
      5  * @Package com.cn.alasga.common.core.util.wechat
      6  * @Description: 雪花算法生成不重复的序列号 可用作订单编号
      7  * @author LiJing
      8  * @date 2018/12/6 13:46
      9  * @version v.3.0
     10  */
     11 
     12 import org.apache.curator.shaded.com.google.common.util.concurrent.ThreadFactoryBuilder;
     13 
     14 import java.util.concurrent.*;
     15 
     16 /**
     17  * Twitter_Snowflake<br>
     18  * SnowFlake的结构如下(每部分用-分开):<br>
     19  * 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
     20  * 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
     21  * 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
     22  * 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
     23  * 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
     24  * 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
     25  * 加起来刚好64位,为一个Long型。<br>
     26  * SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
     27  */
     28 public class SnowflakeUtils {
     29 
     30     private static SnowflakeUtils idGenerater;
     31 
     32     static {
     33         idGenerater = new SnowflakeUtils(1, 2);
     34     }
     35 
     36     public synchronized static long getOrderNo() {
     37         return idGenerater.nextId();
     38     }
     39 
     40     // ==============================Fields===========================================
     41     /**
     42      * 开始时间截 (2015-01-01)
     43      */
     44     private final long twepoch = 1420041600000L;
     45 
     46     /**
     47      * 机器id所占的位数
     48      */
     49     private final long workerIdBits = 5L;
     50 
     51     /**
     52      * 数据标识id所占的位数
     53      */
     54     private final long datacenterIdBits = 5L;
     55 
     56     /**
     57      * 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数)
     58      */
     59     private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
     60 
     61     /**
     62      * 支持的最大数据标识id,结果是31
     63      */
     64     private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
     65 
     66     /**
     67      * 序列在id中占的位数
     68      */
     69     private final long sequenceBits = 12L;
     70 
     71     /**
     72      * 机器ID向左移12位
     73      */
     74     private final long workerIdShift = sequenceBits;
     75 
     76     /**
     77      * 数据标识id向左移17位(12+5)
     78      */
     79     private final long datacenterIdShift = sequenceBits + workerIdBits;
     80 
     81     /**
     82      * 时间截向左移22位(5+5+12)
     83      */
     84     private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
     85 
     86     /**
     87      * 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095)
     88      */
     89     private final long sequenceMask = -1L ^ (-1L << sequenceBits);
     90 
     91     /**
     92      * 工作机器ID(0~31)
     93      */
     94     private long workerId;
     95 
     96     /**
     97      * 数据中心ID(0~31)
     98      */
     99     private long datacenterId;
    100 
    101     /**
    102      * 毫秒内序列(0~4095)
    103      */
    104     private long sequence = 0L;
    105 
    106     /**
    107      * 上次生成ID的时间截
    108      */
    109     private long lastTimestamp = -1L;
    110 
    111     //==============================Constructors=====================================
    112 
    113     /**
    114      * 构造函数
    115      *
    116      * @param workerId     工作ID (0~31)
    117      * @param datacenterId 数据中心ID (0~31)
    118      */
    119     public SnowflakeUtils(long workerId, long datacenterId) {
    120         if (workerId > maxWorkerId || workerId < 0) {
    121             throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
    122         }
    123         if (datacenterId > maxDatacenterId || datacenterId < 0) {
    124             throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
    125         }
    126         this.workerId = workerId;
    127         this.datacenterId = datacenterId;
    128     }
    129 
    130     // ==============================Methods==========================================
    131 
    132     /**
    133      * 获得下一个ID (该方法是可以加注 synchronized 线程安全)
    134      *
    135      * @return SnowflakeId
    136      */
    137     private long nextId() {
    138         long timestamp = timeGen();
    139 
    140         //如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
    141         if (timestamp < lastTimestamp) {
    142             throw new RuntimeException(
    143                     String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
    144         }
    145 
    146         //如果是同一时间生成的,则进行毫秒内序列
    147         if (lastTimestamp == timestamp) {
    148             sequence = (sequence + 1) & sequenceMask;
    149             //毫秒内序列溢出
    150             if (sequence == 0) {
    151                 //阻塞到下一个毫秒,获得新的时间戳
    152                 timestamp = tilNextMillis(lastTimestamp);
    153             }
    154         }
    155         //时间戳改变,毫秒内序列重置
    156         else {
    157             sequence = 0L;
    158         }
    159 
    160         //上次生成ID的时间截
    161         lastTimestamp = timestamp;
    162 
    163         //移位并通过或运算拼到一起组成64位的ID
    164         return ((timestamp - twepoch) << timestampLeftShift)
    165                 | (datacenterId << datacenterIdShift)
    166                 | (workerId << workerIdShift)
    167                 | sequence;
    168     }
    169 
    170     /**
    171      * 阻塞到下一个毫秒,直到获得新的时间戳
    172      *
    173      * @param lastTimestamp 上次生成ID的时间截
    174      * @return 当前时间戳
    175      */
    176     protected long tilNextMillis(long lastTimestamp) {
    177         long timestamp = timeGen();
    178         while (timestamp <= lastTimestamp) {
    179             timestamp = timeGen();
    180         }
    181         return timestamp;
    182     }
    183 
    184     /**
    185      * 返回以毫秒为单位的当前时间
    186      *
    187      * @return 当前时间(毫秒)
    188      */
    189     protected long timeGen() {
    190         return System.currentTimeMillis();
    191     }
    192 
    193     //==============================Test=============================================
    194 
    195     /**
    196      * 测试
    197      */
    198     public static void main(String[] args) throws Exception {
    199         // 线程数量
    200         final int threadCount = 100;
    201         // 每个线程生成的 ID 数量
    202         final int idCountPerThread = 1000;
    203         // 用于等待所有线程启动完成
    204         CountDownLatch threadLatch = new CountDownLatch(threadCount);
    205 
    206         final int coreThread = 5;
    207         final int maxThread = 50;
    208         final long keepAliveTime = 0L;
    209         final int queueCapacity = 1024;
    210 
    211 
    212         ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build();
    213 
    214         //Common Thread Pool
    215         ExecutorService pool = new ThreadPoolExecutor(coreThread, maxThread, keepAliveTime, TimeUnit.MILLISECONDS,
    216                 new LinkedBlockingQueue<Runnable>(queueCapacity), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy());
    217 
    218 
    219         ConcurrentSkipListSet<Long> ids = new ConcurrentSkipListSet<>();
    220         for (int i = 0; i < threadCount; ++i) {
    221             final int n = i;
    222             pool.execute(() -> {
    223                 // 等待所有线程都运行到这里,然后都继续运行,差不多同时生成 id
    224                 final String threadNum = Thread.currentThread().getName() + "-" + n + "号线程";
    225                 try {
    226                     threadLatch.await();
    227                 } catch (InterruptedException e) {
    228                     e.printStackTrace();
    229                 }
    230 //                System.out.println(threadNum+"继续执行");
    231                 for (int j = 0; j < idCountPerThread; ++j) {
    232                     long id = SnowflakeUtils.getOrderNo();
    233                     ids.add(id);
    234                     System.out.println(id);
    235                 }
    236             });
    237             threadLatch.countDown();
    238         }
    239         pool.shutdown();
    240         // 等待 id 生成完成,生成不同数量的 id 时需要调整
    241         Thread.sleep(2000);
    242 //        System.out.println(ids.size());
    243         //System.out.println(ids);
    244     }
    245 }

      

    现在来解释一下:
    代码中测试用到的 ConcurrentSkipListSet<Long> idListSet = new ConcurrentSkipListSet<>();
    ConcurrentSkipListSet是线程安全的有序的集合,适用于高并发的场景。
    ConcurrentSkipListSet和TreeSet,它们虽然都是有序的集合。
    但是,第一,它们的线程安全机制不同,TreeSet是非线程安全的,而ConcurrentSkipListSet是线程安全的。
    第二,ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的,而TreeSet是通过TreeMap实现的。
    说明:
    (01) ConcurrentSkipListSet继承于AbstractSet。因此,它本质上是一个集合。
    (02) ConcurrentSkipListSet实现了NavigableSet接口。因此,ConcurrentSkipListSet是一个有序的集合。
    (03) ConcurrentSkipListSet是通过ConcurrentSkipListMap实现的。它包含一个ConcurrentNavigableMap对象m,而m对象实际上是ConcurrentNavigableMap的实现类ConcurrentSkipListMap的实例。ConcurrentSkipListMap中的元素是key-value键值对;而ConcurrentSkipListSet是集合,它只用到了ConcurrentSkipListMap中的key!
    (04) 里面不能有重复的数据,而且是有序的,还支持并发,很强大啊!!

    但是::::

    查看add()方法的javadoc,其注释为:
    如果此 set 中不包含指定元素,则添加指定元素。更确切地讲,如果此 set 不包含满足 e.equals(e2) 的元素 
    e2,则向 set 中添加指定的元素 e。如果此 set 已经包含该元素,则调用不更改该 set 并返回 
    false。



    根据注释,只要元素的equals()方法判断不相等就能加入到Set中,可调试发现不是这么回事:

    Java代码  
    1 public boolean add(E e) {
    2     return m.putIfAbsent(e, Boolean.TRUE) == null;
    3 }

    m为 ConcurrentSkipListMap,它的putIfAbsent()方法实现是:

    
    
    Java代码  
      
     1     public V putIfAbsent(K key, V value) {
     2         if (value == null)
     3             throw new NullPointerException();
     4         return doPut(key, value, true);
     5     }
     6 
     7     private V doPut(K kkey, V value, boolean onlyIfAbsent) {
     8         Comparable super K&gt; key = comparable(kkey);
     9         for (;;) {
    10             Node b = findPredecessor(key);
    11             Node n = b.next;
    12             for (;;) {
    13                 if (n != null) {
    14                     Node f = n.next;
    15                     if (n != b.next)               // inconsistent read
    16                         break;;
    17                     Object v = n.value;
    18                     if (v == null) {               // n is deleted
    19                         n.helpDelete(b, f);
    20                         break;
    21                     }
    22                     if (v == n || b.value == null) // b is deleted
    23                         break;
    24                     int c = key.compareTo(n.key);
    25                     if (c &gt; 0) {
    26                         b = n;
    27                         n = f;
    28                         continue;
    29                     }
    30                     if (c == 0) {
    31                         if (onlyIfAbsent || n.casValue(v, value))
    32                             return (V)v;
    33                         else
    34                             break; // restart if lost race to replace value
    35                     }
    36                     // else c  z = new Node(kkey, value, n);
    37                 if (!b.casNext(n, z))
    38                     break;         // restart if lost race to append to b
    39                 int level = randomLevel();
    40                 if (level &gt; 0)
    41                     insertIndex(z, level);
    42                 return null;
    43             }
    44         }
    45     }
    
     实际上Set中的添加是根据元素的compareTo()方法在比较!如果compareTo()返回0,就认为2个元素相等,跟equals()方法根本没有关系!JAVA SDK 的JavaDoc也不靠谱啊~~

    一切没有栗子的讲解都是耍流氓

  • 相关阅读:
    使用C语言来扩展PHP,写PHP扩展dll
    用C语言写PHP扩展 linux
    mysql 建索引删除索引命令
    xhprof安装&&使用
    Burst Windows 2003 VPS安装中文语言包图文教程
    yii框架的缓存
    rsync的配置与应用
    mysql分组排序取前N条记录的最简洁的单条sql !
    linux下php以fastcgi模式运行
    JS收集<2>:图片上传_限制格式、类型、尺寸
  • 原文地址:https://www.cnblogs.com/xxmyz/p/10081989.html
Copyright © 2020-2023  润新知