SnowFlake 雪花ID 算法是推特公司推出的著名分佈式ID生成算法。利用預先分配好的機器ID,工做區ID,機器時間能夠生成全局惟一的隨時間趨勢遞增的Long類型ID.長度在17-19位。隨着時間的增加而遞增,在MySQL數據庫中,InnoDB存儲引擎能夠更快的插入遞增的主鍵。而不像UUID那樣由於寫入是亂序的,InnoDB不得不頻繁的作頁分裂操做,耗時且容易產生碎片。html
對於SnowFlake 的原理介紹,能夠參考該文章:理解分佈式id生成算法SnowFlakejava
理解了雪花的基本原理以後,咱們試想:在分佈式集羣或者開發環境下,不一樣服務之間/相同服務的不一樣機器之間應該如何產生差別呢?有如下幾種方案:redis
本方案結合了以上方案的優勢,按照業務的實際狀況對雪花中的數據中心和機器ID所佔的位數進行調整:數據中心佔4Bit,範圍從0-15。機器ID佔6Bit,範圍從0-63 。對不一樣的服務在yml中配置服務名稱,以服務編號做爲數據中心ID。若是按照開發+測試+生產環境區分的話,能夠部署5個不一樣的服務。application.yml 中配置以下的參數算法
# 分佈式雪花ID不一樣機器ID自動化配置 snowFlake: dataCenter: 1 # 數據中心的id appName: test # 業務類型名稱
而機器ID採用如下的策略實現:spring
package cn.keats.util; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import javax.annotation.PreDestroy; import javax.annotation.Resource; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Date; import java.util.Timer; import java.util.TimerTask; @Configuration @Slf4j public class MachineIdConfig { @Resource private JedisPool jedisPool; @Value("${snowFlake.dataCenter}") private Integer dataCenterId; @Value("${snowFlake.appName}") private String APP_NAME; /** * 機器id */ public static Integer machineId; /** * 本地ip地址 */ private static String localIp; /** * 獲取ip地址 * * @return * @throws UnknownHostException */ private String getIPAddress() throws UnknownHostException { InetAddress address = InetAddress.getLocalHost(); return address.getHostAddress(); } /** * hash機器IP初始化一個機器ID */ @Bean public SnowFlake initMachineId() throws Exception { localIp = getIPAddress(); // 192.168.0.233 Long ip_ = Long.parseLong(localIp.replaceAll("\\.", ""));// 1921680233 // machineId = ip_.hashCode() % 32;// 0-31 // 建立一個機器ID createMachineId(); log.info("初始化 machine_id :{}", machineId); return new SnowFlake(machineId, dataCenterId); } /** * 容器銷燬前清除註冊記錄 */ @PreDestroy public void destroyMachineId() { try (Jedis jedis = jedisPool.getResource()) { jedis.del(APP_NAME + dataCenterId + machineId); } } /** * 主方法:首先獲取機器 IP 並 % 32 獲得 0-31 * 使用 業務名 + 組名 + IP 做爲 Redis 的 key,機器IP做爲 value,存儲到Redis中 * * @return */ public Integer createMachineId() { try { // 向redis註冊,並設置超時時間 log.info("註冊一個機器ID到Redis " + machineId + " IP:" + localIp); Boolean flag = registerMachine(machineId, localIp); // 註冊成功 if (flag) { // 啓動一個線程更新超時時間 updateExpTimeThread(); // 返回機器Id log.info("Redis中端口沒有衝突 " + machineId + " IP:" + localIp); return machineId; } // 註冊失敗,可能緣由 Hash%32 的結果衝突 if (!checkIfCanRegister()) { // 若是 0-31 已經用完,使用 32-64之間隨機的ID getRandomMachineId(); createMachineId(); } else { // 若是存在剩餘的ID log.warn("Redis中端口衝突了,使用 0-31 之間未佔用的Id " + machineId + " IP:" + localIp); createMachineId(); } } catch (Exception e) { // 獲取 32 - 63 之間的隨機Id // 返回機器Id log.error("Redis鏈接異常,不能正確註冊雪花機器號 " + machineId + " IP:" + localIp, e); log.warn("使用臨時方案,獲取 32 - 63 之間的隨機數做爲機器號,請及時檢查Redis鏈接"); getRandomMachineId(); return machineId; } return machineId; } /** * 檢查是否被註冊滿了 * * @return */ private Boolean checkIfCanRegister() { // 判斷0~31這個區間段的機器IP是否被佔滿 try (Jedis jedis = jedisPool.getResource()) { Boolean flag = true; for (int i = 0; i < 32; i++) { flag = jedis.exists(APP_NAME + dataCenterId + i); // 若是不存在。設置機器Id爲這個不存在的數字 if (!flag) { machineId = i; break; } } return !flag; } } /** * 1.更新超時時間 * 注意,更新前檢查是否存在機器ip佔用狀況 */ private void updateExpTimeThread() { // 開啓一個線程執行定時任務: // 每23小時更新一次超時時間 new Timer(localIp).schedule(new TimerTask() { @Override public void run() { // 檢查緩存中的ip與本機ip是否一致, 一致則更新時間,不一致則從新獲取一個機器id Boolean b = checkIsLocalIp(String.valueOf(machineId)); if (b) { log.info("IP一致,更新超時時間 ip:{},machineId:{}, time:{}", localIp, machineId, new Date()); try (Jedis jedis = jedisPool.getResource()) { jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24 ); } } else { // IP衝突 log.info("從新生成機器ID ip:{},machineId:{}, time:{}", localIp, machineId, new Date()); // 從新生成機器ID,而且更改雪花中的機器ID getRandomMachineId(); // 從新生成並註冊機器id createMachineId(); // 更改雪花中的機器ID SnowFlake.setWorkerId(machineId); // 結束當前任務 log.info("Timer->thread->name:{}", Thread.currentThread().getName()); this.cancel(); } } }, 10 * 1000, 1000 * 60 * 60 * 23); } /** * 獲取32-63隨機數 */ public void getRandomMachineId() { machineId = (int) (Math.random() * 31) + 31; } /** * 檢查Redis中對應Key的Value是不是本機IP * * @param mechineId * @return */ private Boolean checkIsLocalIp(String mechineId) { try (Jedis jedis = jedisPool.getResource()) { String ip = jedis.get(APP_NAME + dataCenterId + mechineId); log.info("checkIsLocalIp->ip:{}", ip); return localIp.equals(ip); } } /** * 1.註冊機器 * 2.設置超時時間 * * @param machineId 取值爲0~31 * @return */ private Boolean registerMachine(Integer machineId, String localIp) throws Exception { // try with resources 寫法,出異常會釋放括號內的資源 Java7特性 try (Jedis jedis = jedisPool.getResource()) { // key 業務號 + 數據中心ID + 機器ID value 機器IP Long result = jedis.setnx(APP_NAME + dataCenterId + machineId, localIp); if(result == 1){ // 過時時間 1 天 jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24); return true; } else { // 若是Key存在,判斷Value和當前IP是否一致,一致則返回True String value = jedis.get(APP_NAME + dataCenterId + machineId); if(localIp.equals(value)){ // IP一致,註冊機器ID成功 jedis.expire(APP_NAME + dataCenterId + machineId, 60 * 60 * 24); return true; } return false; } } } }
import org.springframework.context.annotation.Configuration; /** * 功能:分佈式ID生成工具類 * */ @Configuration public class SnowFlake { /** * 開始時間截 (2019-09-08) 服務一旦運行過以後不能修改。會致使ID生成重複 */ private final long twepoch = 1567872000000L; /** * 機器Id所佔的位數 0 - 64 */ private final long workerIdBits = 6L; /** * 工做組Id所佔的位數 0 - 16 */ private final long dataCenterIdBits = 4L; /** * 支持的最大機器id,結果是63 (這個移位算法能夠很快的計算出幾位二進制數所能表示的最大十進制數) */ private final long maxWorkerId = -1L ^ (-1L << workerIdBits); /** * 支持的最大數據標識id,結果是15 */ 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~63) */ private static long workerId; /** * 數據中心ID(0~16) */ private long datacenterId; /** * 毫秒內序列(0~4095) */ private long sequence = 0L; /** * 上次生成ID的時間截 */ private long lastTimestamp = -1L; //==============================Constructors===================================== /** * 構造函數 * * @param workerId 工做ID (0~63) * @param datacenterId 數據中心ID (0~15) */ public SnowFlake(long workerId, long datacenterId) { if (workerId > maxWorkerId || workerId < 0) { throw new IllegalArgumentException(String.format("機器ID必須小於 %d 且大於 0", maxWorkerId)); } if (datacenterId > maxDatacenterId || datacenterId < 0) { throw new IllegalArgumentException(String.format("工做組ID必須小於 %d 且大於 0", maxDatacenterId)); } this.workerId = workerId; this.datacenterId = datacenterId; } /** * 構造函數 * */ public SnowFlake() { this.workerId = 0; this.datacenterId = 0; } /** * 得到下一個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(); } public long getWorkerId() { return workerId; } public static void setWorkerId(long workerId) { SnowFlake.workerId = workerId; } public long getDatacenterId() { return datacenterId; } public void setDatacenterId(long datacenterId) { this.datacenterId = datacenterId; } }
public class RedisConfig { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port:6379}") private Integer port; @Value("${spring.redis.password:-1}") private String password; @Bean public JedisPool jedisPool() { // 1.設置鏈接池的配置對象 JedisPoolConfig config = new JedisPoolConfig(); // 設置池中最大鏈接數 config.setMaxTotal(50); // 設置空閒時池中保有的最大鏈接數 config.setMaxIdle(10); config.setMaxWaitMillis(3000L); config.setTestOnBorrow(true); log.info(password); // 2.設置鏈接池對象 if("-1".equals(password)){ log.info("Redis不經過密碼鏈接"); return new JedisPool(config, host, port,0); } else { log.info("Redis經過密碼鏈接" + password); return new JedisPool(config, host, port,0, password); } } }
@Autowired private SnowFlake snowFlake; // 生產ID snowFlake.nextId(); 方法生產ID
原文出處:https://www.cnblogs.com/keatsCoder/p/12129279.html數據庫