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

时间:2024-01-20 20:33:45
  • 分布式系统下 我们每台设备(分布式系统-独立的应用空间-或者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型。 唯一的缺点是设备时间要稳定,不能出现回退. 下面就是我的代码: 没有代码不就是白瞎了?
 package test;

 /**
* @Title: SnowFlakeUtils.java
* @Package com.cn.alasga.common.core.util.wechat
* @Description: 雪花算法生成不重复的序列号 可用作订单编号
* @author LiJing
* @date 2018/12/6 13:46
* @version v.3.0
*/ import org.apache.curator.shaded.com.google.common.util.concurrent.ThreadFactoryBuilder; import java.util.concurrent.*; /**
* Twitter_Snowflake<br>
* SnowFlake的结构如下(每部分用-分开):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位标识,由于long基本类型在Java中是带符号的,最高位是符号位,正数是0,负数是1,所以id一般是正数,最高位是0<br>
* 41位时间截(毫秒级),注意,41位时间截不是存储当前时间的时间截,而是存储时间截的差值(当前时间截 - 开始时间截)
* 得到的值),这里的的开始时间截,一般是我们的id生成器开始使用的时间,由我们程序来指定的(如下下面程序IdWorker类的startTime属性)。41位的时间截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的数据机器位,可以部署在1024个节点,包括5位datacenterId和5位workerId<br>
* 12位序列,毫秒内的计数,12位的计数顺序号支持每个节点每毫秒(同一机器,同一时间截)产生4096个ID序号<br>
* 加起来刚好64位,为一个Long型。<br>
* SnowFlake的优点是,整体上按照时间自增排序,并且整个分布式系统内不会产生ID碰撞(由数据中心ID和机器ID作区分),并且效率较高,经测试,SnowFlake每秒能够产生26万ID左右。
*/
public class SnowflakeUtils { private static SnowflakeUtils idGenerater; static {
idGenerater = new SnowflakeUtils(1, 2);
} public synchronized static long getOrderNo() {
return idGenerater.nextId();
} // ==============================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 SnowflakeUtils(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 线程安全)
*
* @return SnowflakeId
*/
private 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) throws Exception {
// 线程数量
final int threadCount = 100;
// 每个线程生成的 ID 数量
final int idCountPerThread = 1000;
// 用于等待所有线程启动完成
CountDownLatch threadLatch = new CountDownLatch(threadCount); final int coreThread = 5;
final int maxThread = 50;
final long keepAliveTime = 0L;
final int queueCapacity = 1024; ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); //Common Thread Pool
ExecutorService pool = new ThreadPoolExecutor(coreThread, maxThread, keepAliveTime, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(queueCapacity), namedThreadFactory, new ThreadPoolExecutor.AbortPolicy()); ConcurrentSkipListSet<Long> ids = new ConcurrentSkipListSet<>();
for (int i = 0; i < threadCount; ++i) {
final int n = i;
pool.execute(() -> {
// 等待所有线程都运行到这里,然后都继续运行,差不多同时生成 id
final String threadNum = Thread.currentThread().getName() + "-" + n + "号线程";
try {
threadLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// System.out.println(threadNum+"继续执行");
for (int j = 0; j < idCountPerThread; ++j) {
long id = SnowflakeUtils.getOrderNo();
ids.add(id);
System.out.println(id);
}
});
threadLatch.countDown();
}
pool.shutdown();
// 等待 id 生成完成,生成不同数量的 id 时需要调整
Thread.sleep(2000);
// System.out.println(ids.size());
//System.out.println(ids);
}
}

  

现在来解释一下:
代码中测试用到的 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中,可调试发现不是这么回事:

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




 实际上Set中的添加是根据元素的compareTo()方法在比较!如果compareTo()返回0,就认为2个元素相等,跟equals()方法根本没有关系!JAVA SDK 的JavaDoc也不靠谱啊~~