雪花算法
為什麼需要分佈式全局唯一ID 以及分佈式ID的業務需求?
- 在複雜分佈式系統中,往往需要對大量對數據和消息進行標識
- 如在美團、支付、餐飲 中 系統的數據日漸增長,對數據分庫分表需要有一個唯一來標識一條數據或消息
- 此時一個能夠生成全局唯一ID的系統是非常有必要的
ID生成規則部分硬性要求
- 全局唯一 :不能出現重複的ID,要 唯一標識
- 趨勢遞增 :在Mysql 的InnoDB引擎使用的是聚集索引,由於多數RDBMS 使用的是Btree數據結構來存儲數據,在主鍵的選擇上面我們應該儘量使用有序的主鍵保證數據寫入
- 單調遞增 :保證下一個ID一定大於上一個ID,例如事物版本號,增量消息
- 信息安全 :如果ID是連續的,惡意用戶的扒取數據就非常容易來,直接按照順序下載指定的URL,如果是訂單號就更危險來,競爭對手可以知道我們一天的單量,所以在一些應用場景下,需要ID不規則
- 含時間戳 :這樣就能夠在開發中快速瞭解這個分佈式id的生成時間
ID生成系統的可用性要求
- 高可用 :發一個獲取分佈式ID的請求,服務器就要保證99.99%的情況下給我創建一個唯一分佈式ID
- 低延遲 :發一個獲取分佈式ID的請求,服務器就是要快,極速
- 高QPS :假如併發一口氣10萬個創建分佈式ID請求同時殺過來,服務器要頂的住一下子成功創建10w個分佈式ID
我們平時的方案
UUID 、 數據庫自增主鍵 、基於Redis 生成全局ID策略
弊端
UUID 不能生成順序,遞增的數據,並且長,不是很推薦
數據庫自增,集群多的情況下,擴容簡直就是噩夢
Redis 使用Redis INCR 和 INCRBY 實現
snowflake(雪花算法)
Twitter的分佈式自增ID算法:snowflake(雪花算法)
概述
最初 Twitter把存儲系統從Mysql 遷移到 Cassandra (由Facebook 開發一套開源分佈式Nosql系統) 因為Cassandra沒有順序ID生成機制,所以開發成了這樣一套全局唯一 ID生成服務
Twitter 的分佈式雪花算法SnowFlake , 經測試 snowflake 每秒能產出26 萬個自增可排序的ID
- twitter的SnowFlake生成ID能夠按照時間有序生成
- SnowFlake 算法生成id 的結果是一個64 bit 大小的整數,為一個Long 型(轉換成字符後長度19位)
- 分佈式系統不會產生ID碰撞(由datacenter 和 workerld 區分)並且效率較高
結構
號段解析:
1bit ,
- 不用,因為二進制中最高位是符號位,毫秒級,生成的id一般用整數,所以最高位 0
41bit - 時間戳,用來記錄時間戳,毫秒級,
- 41位可以表示 2 ^ {41}-1個數字
- 如果只用來表示正整數(計算機中正整數包含0)。可以表示數值範圍:0 至 2^{41}-1 , 減1 是因為表示的數值是從0開始算的 ,而不是1.
- 也就是說 41 位可以表示 2 ^ {41}-1 個毫秒的值,裝換成單位年則 (2^{41}-1)/ (1000 60 60 24 365)=69年
10bit- 工作機器ID,用來記錄工作機器ID
- 可以部署在 2^{10} = 1024 個節點,包括5位 datacenterId 和 5位的 workeId
- 5位(bit)可以表示的最大正整數是 2 ^ {5}-1 =31 , 即可以用0、1、2、3....31這32個數字,來表示不同的datacenterId 或者 workId
12 bit -序列號,序列號,用來記錄同毫秒內產生的不同的id。
- 12位可以表示的最大正整數是2^{12}-1 = 4095 即可以用0、1、2、34094 這 4095個數字來表示同一機器同一時間(毫秒)產生的4095個ID序號
SnowFlake可以保證
- 所有生成的id 按時間趨勢遞增
- 整個分佈式系統內不會產生重複的id
源碼
twitter的雪花算法:https://github.com/twitter-archive/snowflake
GitHub上java版的雪花算法:
https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java
https://github.com/souyunku/SnowFlake/blob/master/SnowFlake.java
java版❄️雪花算法
public class SnowflakeIdWorker {
// ==============================Fields==================
/** 開始時間截 (2019-08-06) */
private final long twepoch = 1565020800000L;
/** 機器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 < 10; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
springboot整合雪花算法
- 新建項目snowflake
- pom
<dependencies>
<!--hutool 引入糊塗工具包,測試雪花算法-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.3.8</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- yml
server:
port: 7777
- 新建 utils包 IdGeneratorSnowflake 類
@Slf4j
@Component
public class IdGeneratorSnowflake {
private long workerId = 0; //第幾號機房
private long datacenterId = 1; //第幾號機器
private Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
@PostConstruct //構造後開始執行,加載初始化工作
public void init(){
try{
//獲取本機的ip地址編碼
workerId = NetUtil.ipv4ToLong(NetUtil.getLocalhostStr());
log.info("當前機器的workerId: " + workerId);
}catch (Exception e){
e.printStackTrace();
log.warn("當前機器的workerId獲取失敗 ----> " + e);
workerId = NetUtil.getLocalhostStr().hashCode();
}
}
public synchronized long snowflakeId(){
return snowflake.nextId();
}
public synchronized long snowflakeId(long workerId, long datacenterId){
Snowflake snowflake = IdUtil.createSnowflake(workerId, datacenterId);
return snowflake.nextId();
}
//測試
public static void main(String[] args) {
System.out.println(new IdGeneratorSnowflake().snowflakeId()); //1277896081711169536
}
}
- 新建service包 OrderService 接口 與 service.impl包 OrderServiceImpl 實現OrderService的接口
public interface OrderService {
String getIDBySnowFlake();
}
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private IdGeneratorSnowflake idGenerator;
public String getIDBySnowFlake() {
//新建線程池(5個線程)
ExecutorService threadPool = Executors.newFixedThreadPool(5);
for (int i = 1; i <= 20; i++) {
threadPool.submit(() -> {
System.out.println(idGenerator.snowflakeId());
});
}
threadPool.shutdown();
return "hello snowflake";
}
}
- 新建 controller 包 OrderController
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@RequestMapping("/snowflake")
public String index(){
return orderService.getIDBySnowFlake();
}
}
- 主啟動類
@SpringBootApplication
public class MainApp {
public static void main(String[] args) {
SpringApplication.run(MainApp.class, args);
}
}
啟動項目 瀏覽器輸入http://localhost:7777/snowflake
優缺點
解決時鐘回撥問題
- 百度開源的分佈式唯一ID生成器 UidGenerator
- 美團開源的分佈式ID生成系統 Leaf