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