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

摘要:
因此,Leaf对第二个和第三个方案进行了相应的优化,并实现了Leaf片段和Leaf雪花方案。26*snowflake的优点是,总体上,它是根据时间自动排序的,在整个分布式系统中没有ID冲突,效率高。经过测试,SnowFlake每秒可以生成大约26万个ID。
  • 分布式系统下 我们每台设备(分布式系统-独立的应用空间-或者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也不靠谱啊~~

免责声明:文章转载自《分布式系统-主键唯一id,订单编号生成-雪花算法-SnowFlake》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇网络_01 基本配置Linux makefile 教程 非常详细,且易懂下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

在PHP中获取日期和时间

PHP提供了多种获取时间和日期的函数,除了通过time()函数获取当前的UNIX时间戳外,调用getdate()函数确定当前时间,通过gettimeofday()函数获取某一天中的具体时间。此外,在PHP中还可以通过date_sunrise()和date_sunset两个函数,获取某地点某天的日出和日落时间。   ①调用getdate()函数取得日期/时间...

python 时间模块小结(time and datetime)

一:经常使用的时间方法 1.得到当前时间 使用time模块,首先得到当前的时间戳 In [42]: time.time() Out[42]: 1408066927.208922 将时间戳转换为时间元组 struct_time In [43]: time.localtime(time.time()) Out[43]: time.struct_t...

python 获取当天日期

取得时间相关的信息的话,要用到python time模块,python time模块里面有很多非常好用的功能,你可以去官方文档了解下,要取的当前时间的话,要取得当前时间的时间戳,时间戳好像是1970年到现在时间相隔的时间。你可以试下下面的方式来取得当前时间的时间戳:import timeprint time.time()输出的结果是:1357723206....

HBase 伪分布式环境搭建及基础命令使用

一.前提条件: (1)文件存储在HDFS文件系统之上。因此必须启动hadoop服务。(namenode,datanode,resourcemanager,nodemanager,historyserver)(2)源文件依赖于zookeeper。因此需要启动zookeeper服务。(./zkServer ./zkCli.sh) 二,HBase的安装(版本:5...

python基础学习4-函数、内置函数、os模块、time模块

  1       函数 1.1     字符串格式化方法 Python中字符串格式化输出的几种方法: https://www.cnblogs.com/hongzejun/p/7670923.html 字符串格式化另外一种方式format方式 #字符串format()方法 #第一种import datetime msg = '欢迎光临{name},今天的日...

hive 时间戳函数之unix_timestamp,from_unixtime

一. 日期>>>>时间戳1.unix_timestamp() 获取当前时间戳 例如:select unix_timestamp() --1565858389 2.unix_timestamp(string timestame) 输入的时间戳格式必须为'yyyy-MM-dd HH:mm:ss',如不符合则返回null 例如...