網(wǎng)上有很多關(guān)于手持pos機開發(fā)源碼,分布式 ID 生成系統(tǒng) Leaf 的設(shè)計思路的知識,也有很多人為大家解答關(guān)于手持pos機開發(fā)源碼的問題,今天pos機之家(m.afbey.com)為大家整理了關(guān)于這方面的知識,讓我們一起來看下吧!
本文目錄一覽:
手持pos機開發(fā)源碼
小伙伴們好呀,我是 4ye,今天來分享下最近研究的分布式 ID 生成系統(tǒng) —— Leaf ,一起來思考下這個分布式ID的設(shè)計吧
什么是分布式ID?ID 最大的特點是 唯一
而分布式 ID,就是指分布式系統(tǒng)下的 ID,它是 全局唯一 的。
為啥需要分布式ID呢?這就和 唯一 息息相關(guān)了。
比如我們用 MySQL 存儲數(shù)據(jù),一開始數(shù)據(jù)量不大,但是業(yè)務(wù)經(jīng)過一段時間的發(fā)展,單表數(shù)據(jù)每日劇增,最終突破 1000w,2000w …… 系統(tǒng)開始變慢了,此時我們已經(jīng)嘗試了 優(yōu)化索引, 讀寫分離 ,升級硬件,升級網(wǎng)絡(luò) 等操作,但是 單表瓶頸 還是來了,我們只能去 分庫分表 了。
而問題也隨著而來了,分庫分表后,如果還用 數(shù)據(jù)庫自增ID 的方式的話,那么在用戶表中,就會出現(xiàn) 兩個不同的用戶有相同的ID 的情況,這個是不能接受的。
而 分布式ID全局唯一 的特點,正是我們所需要的。
分布式ID的生成方式UUID數(shù)據(jù)庫自增ID (MySQL,Redis)雪花算法基本就上面幾種了,UUID 的最大缺點就是太長,36個字符長度,而且無序,不適合。
而其他兩種的缺點還有辦法補救,可能這也是 Leaf 提供這兩種生成 ID 方式的原因。
項目簡介Leaf ,分布式 ID 生成系統(tǒng),有兩種生成 ID 的方式:
號段模式Snowflake模式號段模式在 數(shù)據(jù)庫自增ID 的基礎(chǔ)上進行優(yōu)化
增加一個 segement ,減少訪問數(shù)據(jù)庫的次數(shù)。雙 Buffer 優(yōu)化,提前緩存下一個 Segement,降低網(wǎng)絡(luò)請求的耗時(降低系統(tǒng)的TP999指標)來自美團技術(shù)團隊
biz_tag用來區(qū)分業(yè)務(wù),max_id表示該biz_tag目前所被分配的ID號段的最大值,step表示每次分配的號段長度
沒優(yōu)化前,每次都從 db 獲取,現(xiàn)在獲取的頻率和 step 字段相關(guān)。
雙 Buffer 優(yōu)化思路
號段模式源碼解讀
segmentService 構(gòu)造方法作用
配置 dataSource設(shè)置 mybatis實例化 SegmentIDGenImpl執(zhí)行 init 方法這段代碼我也忘了 哈哈,已經(jīng)多久沒直接用 mybatis 了,還是重新去官網(wǎng)翻看的。
mybatis 官網(wǎng)例子
實例化 SegmentIDGenImpl 時,其中有兩個變量要留意下
SEGMENT_DURATION,智能調(diào)節(jié) step 的關(guān)鍵cache ,其中 SegmentBuffer 是雙 Buffer 的關(guān)鍵設(shè)計。這里先不展開,看看 init 方法先。
SegmentIDGenImpl init 方法作用
執(zhí)行 updateCacheFromDb 方法開后臺線程,每分鐘執(zhí)行一次 updateCacheFromDb() 方法顯然,核心在 updateCacheFromDb
updateCacheFromDb 方法這里就直接看源碼和我加的注釋
private void updateCacheFromDb() { logger.info("update cache from db"); StopWatch sw = new Slf4JStopWatch(); try { // 執(zhí)行 SELECT biz_tag FROM leaf_alloc 語句,獲取所有的 業(yè)務(wù)字段。 List<String> dbTags = dao.getAllTags(); if (dbTags == null || dbTags.isEmpty()) { return; } // 緩存中的 biz_tag List<String> cacheTags = new ArrayList<String>(cache.keySet()); // 要插入的 db 中的 biz_tag Set<String> insertTagsSet = new HashSet<>(dbTags); // 要移除的緩存中的 biz_tag Set<String> removeTagsSet = new HashSet<>(cacheTags); // 緩存中有的話,不用再插入,從 insertTagsSet 中移除 for (int i = 0; i < cacheTags.size(); i++) { String tmp = cacheTags.get(i); if (insertTagsSet.contains(tmp)) { insertTagsSet.remove(tmp); } } // 為新增的 biz_tag 創(chuàng)建緩存 SegmentBuffer for (String tag : insertTagsSet) { SegmentBuffer buffer = new SegmentBuffer(); buffer.setKey(tag); Segment segment = buffer.getCurrent(); segment.setValue(new AtomicLong(0)); segment.setMax(0); segment.setStep(0); cache.put(tag, buffer); logger.info("Add tag {} from db to IdCache, SegmentBuffer {}", tag, buffer); } // db中存在的,從要移除的 removeTagsSet 移除。 for (int i = 0; i < dbTags.size(); i++) { String tmp = dbTags.get(i); if (removeTagsSet.contains(tmp)) { removeTagsSet.remove(tmp); } } // 從 cache 中移除不存在的 bit_tag。 for (String tag : removeTagsSet) { cache.remove(tag); logger.info("Remove tag {} from IdCache", tag); } } catch (exception e) { logger.warn("update cache from db exception", e); } finally { sw.stop("updateCacheFromDb"); } }
執(zhí)行完后,會出現(xiàn)這樣的 log
Add tag leaf-segment-test from db to IdCache, SegmentBuffer SegmentBuffer{key='leaf-segment-test', segments=[Segment(value:0,max:0,step:0), Segment(value:0,max:0,step:0)], currentPos=0, nextReady=false, initOk=false, threadRunning=false, step=0, minStep=0, updatetimestamp=0}
最后 init 方法結(jié)束后,會將 initOk 設(shè)置為 true。
項目啟動完畢后,我們就可以調(diào)用這個 API 了。
如圖,訪問 LeafController 中的 Segment API,可以獲取到一個 id。
SegmentIDGenImpl get 方法可以看到,init 不成功會報錯。
以及會直接從 cache 中查找這個 key(biz_tag) , 沒有的話會報錯。
拿到這個 SegmentBuffer 時,還得看看它 init 了 沒有,沒有的話用雙檢查鎖的方式去更新
先來看下一眼 SegmentBuffer 的結(jié)構(gòu)
SegmentBuffer 類?updateSegmentFromDb 方法這里就是更新緩存的方法了,主要是更新 Segment 的 value , max,step 字段。
可以看到有三個 if 分支,下面展開說
分支一:初始化第一次,buffer 還沒 init,如上圖,執(zhí)行完后會更新 SegmentBuffer 的 step 和 minStep 字段。
分支二:第二次更新這里主要是更新這個 updateTimestamp ,它的作用看分支三
分支三:剩下的更新這里就比較有意思了,就是說如果這個號段在 15分鐘 內(nèi)用完了,那么它會擴大這個 step (不超過 10w),創(chuàng)建一個更大的 MaxId ,降低訪問 DB 的頻率。
那么,到這里,我們完成了 updateSegmentFromDb 方法,更新了 Segment 的 value , max,step 字段。
但是,我們不是每次 get 都走上面的流程,它還得走這個緩存方法
?getIdFromSegmentBuffer 方法顯然,這是另一個重點。
如圖,在死循環(huán)中,先獲取讀鎖,拿到當前的號段 Segment,進行判斷
使用超過 10% 就開新線程去更新下一個號段沒超過則將 value (AtomicLong 類型)+1 ,小于 maxId 則直接返回。這里要重點留意 讀寫鎖的使用 ,比如 開新線程時,使用了這個 寫鎖 ,里面的 nextReady 等變量使用了 volatile 修飾
這里的核心就是切換 Segment。
至此,號段模式結(jié)束。
優(yōu)缺點信息安全:如果ID是連續(xù)的,惡意用戶的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道我們一天的單量。所以在一些應(yīng)用場景下,會需要ID無規(guī)則、不規(guī)則。—— 《Leaf——美團點評分布式ID生成系統(tǒng)》
美團
可以看到,這個號段模式的最大弊端就是 信息不安全,所以在使用時得三思,能不能用到這些業(yè)務(wù)中去。
Snowflake模式雪花算法,核心就是將 64bit 分段,用來表示時間,機器,序列號等。
41-bit的時間可以表示(1L<<41)/(1000L360024*365)=69年的時間,10-bit機器可以分別表示1024臺機器。
12個自增序列號可以表示2^12個ID,理論上snowflake方案的QPS約為 2^12 * 1000 = 409.6w/s
這里使用 Zookeeper 持久順序節(jié)點的特性自動對 snowflake 節(jié)點配置 wokerID,不用手動配置。
時鐘回撥問題
img
Snowflake模式源碼解讀這部分源碼就不一一展開了,直接展示核心代碼
SnowflakeZookeeperHolder init 方法這里要注意調(diào)整這個 connectionTimeoutms 和 sessionTimeoutMs ,不然兩種模式都啟動的話,這個 zk 的 session 可能會超時,造成啟動失敗。
圖中流程
看看 zk 節(jié)點存不存在,不存在就創(chuàng)建同時將 worker id 保存到本地。創(chuàng)建定時任務(wù),更新 znode。znode
worker Id
定時任務(wù)
SnowflakeIDGenImpl get 方法這里直接看代碼和注釋了
@Override public synchronized Result get(String key) { long timestamp = timeGen(); // 發(fā)生了回撥,此刻時間小于上次發(fā)號時間 if (timestamp < lastTimestamp) { long offset = lastTimestamp - timestamp; if (offset <= 5) { try { //時間偏差大小小于5ms,則等待兩倍時間 wait(offset << 1); timestamp = timeGen(); //還是小于,拋異常并上報 if (timestamp < lastTimestamp) { return new Result(-1, Status.EXCEPTION); } } catch (InterruptedException e) { LOGGER.error("wait interrupted"); return new Result(-2, Status.EXCEPTION); } } else { return new Result(-3, Status.EXCEPTION); } } if (lastTimestamp == timestamp) { // sequenceMask = ~(-1L << 12 ) = 4095 二進制即 12 個1 sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { //seq 為0的時候表示是下一毫秒時間開始對seq做隨機 sequence = RANDOM.nextInt(100); timestamp = tilNextMillis(lastTimestamp); } } else { //如果是新的ms開始 sequence = RANDOM.nextInt(100); } lastTimestamp = timestamp; // timestampLeftShift = 22, workerIdShift = 12 long id = ((timestamp - twepoch) << timestampLeftShift) | (workerId << workerIdShift) | sequence; return new Result(id, Status.SUCCESS); } protected long tilNextMillis(long lastTimestamp) { long timestamp = timeGen(); while (timestamp <= lastTimestamp) { timestamp = timeGen(); } return timestamp; } protected long timeGen() { return System.currentTimeMillis(); }API 效果
生成 ID
反解 ID
至此,這個 Snowflake 模式也了解完畢了。
總結(jié)看完上面兩種模式,我覺得兩種模式都有它適用的場景,號段模式更適合對內(nèi)使用(比如 用戶ID),而如果你這個 ID 會被用戶看到,暴露出去有其他風險(比如爬蟲惡意爬取等),那就得多斟酌了,。而訂單號 就更適合用 snowflake 模式。
分布式ID 的特點
全局唯一趨勢遞增可反解(可選)信息安全(可選)參考資料Github 地址:https://github.com/Meituan-Dianping/Leaf/blob/master/README_CN.mdLeaf——美團點評分布式ID生成系統(tǒng):https://tech.meituan.com/2017/04/21/mt-leaf.html分布式id生成方案總結(jié):https://www.cnblogs.com/javaguide/p/11824105.html喜歡的小伙伴記得關(guān)注點點贊哦,全網(wǎng)同名[狗頭]
以上就是關(guān)于手持pos機開發(fā)源碼,分布式 ID 生成系統(tǒng) Leaf 的設(shè)計思路的知識,后面我們會繼續(xù)為大家整理關(guān)于手持pos機開發(fā)源碼的知識,希望能夠幫助到大家!
