[平常]平常學習總結二

一  延時消息隊列任務

1  延時消息隊列介紹

(1)業務場景java

  例如在以下場景中可能會須要延時隊列:git

    用戶下訂單成功以後隔20分鐘給用戶發送上門服務通知短信github

    訂單完成一個小時以後通知用戶對上門服務進行評價面試

    業務執行失敗以後隔10分鐘重試一次redis

  相似的場景比較多,簡單的處理方式就是使用定時任務,假如數據比較多的時候,有的數據可能延遲比較嚴重,並且愈來愈多的定時業務致使任務調度很繁瑣很差管理。算法

(2)開發前須要考慮的問題?sql

  1.及時性:消費端能按時收到數據庫

  2.同一時間消息的消費權重apache

  3.可靠性:消息不能出現沒有被消費掉的狀況數組

  3.可恢復:假若有其餘狀況,致使消息系統不可用了,至少能保證數據能夠恢復

  4.可撤回:由於是延遲消息,沒有到執行時間的消息支持能夠取消消費

  5.高可用、多實例:這裏指HA/主備模式並非多實例同時一塊兒工做

2  基於Redis的延遲隊列

  選用redis做爲數據緩存的主要緣由是由於redis自身支持zset的數據結構(score 延遲時間毫秒) 這樣就少了排序的煩惱並且性能還很高,正好咱們的需求就是按時間維度去斷定執行的順序,同時也支持maplist數據結構。

    Zset本質就是Set結構上加了個排序的功能,除了添加數據value以外,還提供另外一屬性score,這一屬性在添加修改元素時候能夠指定,每次指定後,Zset會自動從新按新的值調整順序。

  能夠理解爲有兩列字段的數據表,一列存value,一列存順序編號。操做中key理解爲zset的名字,那麼對延時隊列又有何用呢?

  試想若是score表明的是想要執行時間的時間戳,在某個時間將它插入Zset集合中,它變會按照時間戳大小進行排序,也就是對執行時間先後進行排序,這樣的話,起一個死循環線程不斷地進行取第一個key值,若是當前時間戳大於等於該key值的socre就將它取出來進行消費刪除,就能夠達到延時執行的目的, 注意不須要遍歷整個Zset集合,以避免形成性能浪費。

/**
 * @program: test
 * @description: redis實現延時隊列
 * @author: xingcheng
 * @create: 2018-08-19
 **/
public class DelayQueue {

    private static final String ADDR = "127.0.0.1";
    private static final int PORT = 6379;
    private static JedisPool jedisPool = new JedisPool(ADDR, PORT);
    private static CountDownLatch cdl = new CountDownLatch(10);

    public static Jedis getJedis() {
        return jedisPool.getResource();
    }

    /**
     * 生產者,生成5個訂單
     */
    public void productionDelayMessage() {
        for (int i = 0; i < 5; i++) {
            Calendar instance = Calendar.getInstance();
            // 3秒後執行
            instance.add(Calendar.SECOND, 3 + i);
            DelayQueue.getJedis().zadd("orderId", (instance.getTimeInMillis()) / 1000, StringUtils.join("000000000", i + 1));
            System.out.println("生產訂單: " + StringUtils.join("000000000", i + 1) + " 當前時間:" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
            System.out.println((3 + i) + "秒後執行");
        }
    }

    //消費者,取訂單
    public static void consumerDelayMessage() {
        Jedis jedis = DelayQueue.getJedis();
        while (true) {
            Set<Tuple> order = jedis.zrangeWithScores("orderId", 0, System.currenTimeMillis(),0,1);//表示取出0-當前時間戳的數據,從第0個數據開始,取一個
            if (order == null || order.isEmpty()) {
                System.out.println("當前沒有等待的任務");
                try {
                    TimeUnit.MICROSECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                continue;
            }
            Tuple tuple = (Tuple) order.toArray()[0];
            double score = tuple.getScore();
            Calendar instance = Calendar.getInstance();
            long nowTime = instance.getTimeInMillis() / 1000;
            if (nowTime >= score) {
                String element = tuple.getElement();
                Long orderId = jedis.zrem("orderId", element);
                if (orderId > 0) {
                    System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + ":redis消費了一個任務:消費的訂單OrderId爲" + element);
                }
            }
        }
    }

    static class DelayMessage implements Runnable{
        @Override
        public void run() {
            try {
                cdl.await();
                consumerDelayMessage();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) {
        DelayQueue appTest = new DelayQueue();
        appTest.productionDelayMessage();
        for (int i = 0; i < 10; i++) {
            new Thread(new DelayMessage()).start();
            cdl.countDown();
        }
    }
}
View Code

生產環境使用注意:

  因爲這種實現方式簡單,但在生產環境下大可能是多實例部署,因此存在併發問題,即緩存的查找和刪除不具備原子性(zrangeWithScores和zrem操做不是一個命令,不具備原子性),會致使消息的屢次發送問題,這個問題的避免方法以下:

  1.能夠採用單獨一個實例部署解決(不具有高可用特性,容易單機出現故障後消息不能及時發送)

  2.採用redis的lua腳本進行原子操做,即原子操做查找和刪除(實現難度大)

 參考:https://my.oschina.net/u/3266761/blog/1930360

3  基於環形數組實現

  環形的任務隊列,由數組實現,數組中元素是Set<Task>,數組長度是3600:

  (1)環形隊列,例如能夠建立一個包含3600個slot的環形隊列(本質是個數組)

  (2)任務集合,環上每個slot是一個Set<Task>

  同時,啓動一個timer,這個timer每隔1s,在上述環形隊列中移動一格,有一個Current Index指針來標識正在檢測的slot。

    

 

  Task結構中有兩個核心屬性:

    1. Cycle-Num:當Current Index第幾圈掃描到這個Slot時,執行任務

    2. Task-Function:須要執行的任務指針

  啓動一個Timer,每一個一秒鐘在移動一個slot,那轉一圈正好須要一個小時。

  如上圖,當前Current Index指向第一格,當有延時消息到達以後,例如但願3610秒以後,觸發一個延時消息任務,只需:

    1. 計算這個Task應該放在哪個slot,如今指向1,3610秒以後,應該是第11格,因此這個Task應該放在第11個slot的Set<Task>中

    2. 計算這個Task的Cycle-Num,因爲環形隊列是3600格,這個任務是3610秒後執行,因此應該繞3610/3600=1圈以後再執行,因而Cycle-Num=1

  Current Index不停的移動,每秒移動到一個新slot,遍歷slot中對應的Set<Task>,每一個Task看Cycle-Num是否是0:

    1. 若是不是0,說明還須要多移動幾圈,將Cycle-Num減1

    2. 若是是0,說明立刻要執行這個Task了,取出Task-Funciton執行(能夠用單獨的線程來執行Task),並把這個Task從Set<Task>中刪除。

  Netty中的工具類HashedWheelTimer的原理與這種環形的延遲隊列類似。

  擴展:

  10w定時任務,如何高效觸發超時  https://mp.weixin.qq.com/s?__biz=MjM5ODYxMDA5OQ==&mid=2651959957&idx=1&sn=a82bb7e8203b20b2a0cb5fc95b7936a5&chksm=bd2d07498a5a8e5f9f8e7b5aeaa5bd8585a0ee4bf470956e7fd0a2b36d132eb46553265f4eaf&scene=21#wechat_redirect

 二  分庫分表相關問題

1  面試題

  爲何要分庫分表(設計高併發系統的時候,數據庫層面該如何設計)?用過哪些分庫分表中間件?不一樣的分庫分表中間件都有什麼優勢和缺點?大傢俱體是如何對數據庫如何進行垂直拆分或水平拆分的?

2  考察點

  其實這塊確定是扯到高併發了,由於分庫分表必定是爲了支撐高併發、數據量大兩個問題的。並且如今說實話,尤爲是互聯網類的公司面試,基本上都會來這麼一下,分庫分表如此廣泛的技術問題,不問實在是不行,而若是你不知道那也實在是說不過去!

3  爲何要分庫分表

  分庫分表是兩種不一樣的處理方式,多是光分庫不分表,也多是光分表不分庫,也可能同時分庫而且分表。

  例如以下場景:

  (1)假如咱們如今是一個小創業公司(或者是一個 BAT 公司剛興起的一個新部門),如今註冊用戶就 20 萬,天天活躍用戶就 1 萬,天天單表數據量就 1000,而後高峯期每秒鐘併發請求最多就 10。這種狀況單表數據量不大,而且併發量也很低,所以就不須要考慮分庫分表,防止帶來代碼上的複雜性。

  (2)若是業務發展迅猛,過了幾個月,註冊用戶數達到了 2000 萬!天天活躍用戶數 100 萬!天天單表數據量 10 萬條!高峯期每秒最大請求達到 1000!由於天天多 10 萬條數據,一個月就多 300 萬條數據,如今我們單表已經幾百萬數據了,立刻就破千萬了,可是勉強還能撐着。高峯期請求如今是 1000,我們線上部署了幾臺機器,負載均衡搞了一下,數據庫撐 1000QPS 也還湊合。可是這種狀況下就須要考慮下分庫分表了。

  (3)再接下來幾個月,公司用戶數已經達到 1 億,,由於此時天天活躍用戶數上千萬,天天單表新增數據多達 50 萬,目前一個表總數據量都已經達到了兩三千萬了!扛不住啊!數據庫磁盤容量不斷消耗掉!高峯期併發達到驚人的 5000~8000!此時系統就會撐不住而致使宕機。

  所以,其實是否須要分庫分表這是跟着你的公司業務發展走的,你公司業務發展越好,用戶就越多,數據量越大,請求量越大,那你單個數據庫必定扛不住。

4  分表

  好比你單表都幾千萬數據了,你肯定你能扛住麼?絕對不行,單表數據量太大,會極大影響你的 sql 執行的性能,到了後面你的 sql 可能就跑的很慢了。通常來講,就以個人經驗來看,單表到幾百萬的時候,性能就會相對差一些了,你就得分表了。

  分表是啥意思?就是把一個表的數據放到多個表中,而後查詢的時候你就查一個表。好比按照用戶 id 來分表,將一個用戶的數據就放在一個表中。而後操做的時候你對一個用戶就操做那個表就行了。這樣能夠控制每一個表的數據量在可控的範圍內,好比每一個表就固定在 200 萬之內。

5  分庫

  分庫是啥意思?就是你一個庫通常咱們經驗而言,最多支撐到併發 2000,必定要擴容了,並且一個健康的單庫併發值你最好保持在每秒 1000 左右,不要太大。那麼你能夠將一個庫的數據拆分到多個庫中,訪問的時候就訪問一個庫好了。

  這就是所謂的分庫分表,爲啥要分庫分表?你明白了吧。

6  經常使用中間件

  Sharding-jdbc

  噹噹開源的,屬於 client 層方案,目前已經改名爲 ShardingSphere(後文所提到的 Sharding-jdbc,等同於 ShardingSphere)。確實以前用的還比較多一些,由於 SQL 語法支持也比較多,沒有太多限制,並且截至 2019.4,已經推出到了 4.0.0-RC1 版本,支持分庫分表、讀寫分離、分佈式 id 生成、柔性事務(最大努力送達型事務、TCC 事務)。並且確實以前使用的公司會比較多一些(這個在官網有登記使用的公司,能夠看到從 2017 年一直到如今,是有很多公司在用的),目前社區也還一直在開發和維護,還算是比較活躍,我的認爲算是一個如今也能夠選擇的方案。

  Mycat

  基於 Cobar 改造的,屬於 proxy 層方案,支持的功能很是完善,並且目前應該是很是火的並且不斷流行的數據庫中間件,社區很活躍,也有一些公司開始在用了。可是確實相比於 Sharding jdbc 來講,年輕一些,經歷的錘鍊少一些。

  總結:

  綜上,如今其實建議考量的,就是 Sharding-jdbc 和 Mycat,這兩個均可以去考慮使用。

  Sharding-jdbc 這種 client 層方案的優勢在於不用部署,運維成本低,不須要代理層的二次轉發請求,性能很高,可是若是遇到升級啥的須要各個系統都從新升級版本再發布,各個系統都須要耦合 Sharding-jdbc 的依賴;

  Mycat 這種 proxy 層方案的缺點在於須要單獨部署,本身運維一套中間件,運維成本高,可是好處在於對於各個項目是透明的,若是遇到升級之類的都是本身中間件那裏搞就好了。

  一般來講,這兩個方案其實均可以選用,可是我我的建議中小型公司選用 Sharding-jdbc,client 層方案輕便,並且維護成本低,不須要額外增派人手,並且中小型公司系統複雜度會低一些,項目也沒那麼多;可是中大型公司最好仍是選用 Mycat 這類 proxy 層方案,由於可能大公司系統和項目很是多,團隊很大,人員充足,那麼最好是專門弄我的來研究和維護 Mycat,而後大量項目直接透明使用便可。

7  如何對數據庫進行垂直拆分或水平拆分

   水平拆分的意思,就是把一個表的數據給弄到多個庫的多個表裏去,可是每一個庫的表結構都同樣,只不過每一個庫表放的數據是不一樣的,全部庫表的數據加起來就是所有數據。水平拆分的意義,就是將數據均勻放更多的庫裏,而後用多個庫來扛更高的併發,而且能夠用多個庫的存儲容量來進行擴容

 

  垂直拆分的意思,就是把一個有不少字段的表給拆分紅多個表,或者是多個庫上去。每一個庫表的結構都不同,每一個庫表都包含部分字段。通常來講,會將較少的訪問頻率很高的字段放到一個表裏去,而後將較多的訪問頻率很低的字段放到另一個表裏去。由於數據庫是有緩存的,你訪問頻率高的行字段越少,就能夠在緩存裏緩存更多的行,性能就越好。這個通常在表層面作的較多一些。

 

  這個其實挺常見的,不必定我說,你們不少同窗可能本身都作過,把一個大表拆開,訂單表、訂單支付表、訂單商品表。

  還有表層面的拆分,就是分表,將一個表變成 N 個表,就是讓每一個表的數據量控制在必定範圍內,保證 SQL 的性能。不然單表數據量越大,SQL 性能就越差。通常是 200 萬行左右,不要太多,可是也得看具體你怎麼操做,也多是 500 萬,或者是 100 萬。你的SQL越複雜,就最好讓單錶行數越少

  好了,不管分庫仍是分表,上面說的那些數據庫中間件都是能夠支持的。就是基本上那些中間件能夠作到你分庫分表以後,中間件能夠根據你指定的某個字段值,好比說 userid,自動路由到對應的庫上去,而後再自動路由到對應的表裏去。

你就得考慮一下,你的項目裏該如何分庫分表?

  通常來講,垂直拆分,你能夠在表層面來作,對一些字段特別多的表作一下拆分;

  水平拆分,你能夠說是併發承載不了,或者是數據量太大,容量承載不了,你給拆了,按什麼字段來拆,你本身想好;

  分表,你考慮一下,你若是哪怕是拆到每一個庫裏去,併發和容量都 ok 了,可是每一個庫的表仍是太大了,那麼你就分表,將這個表分開,保證每一個表的數據量並非很大。

並且這兒還有兩種分庫分表的方式:

  (1)一種是按照 range 來分,就是每一個庫一段連續的數據,這個通常是按好比時間範圍來的,可是這種通常較少用,由於很容易產生熱點問題,大量的流量都打在最新的數據上了。

  (2)按照某個字段 hash 一下均勻分散,這個較爲經常使用。

  range 來分,好處在於說,擴容的時候很簡單,由於你只要預備好,給每月都準備一個庫就能夠了,到了一個新的月份的時候,天然而然,就會寫新的庫了;缺點,可是大部分的請求,都是訪問最新的數據。實際生產用 range,要看場景。

  hash 分發,好處在於說,能夠平均分配每一個庫的數據量和請求壓力;壞處在於說擴容起來比較麻煩,會有一個數據遷移的過程,以前的數據須要從新計算 hash 值從新分配到不一樣的庫或表。

  參考:https://github.com/yuejuntao/advanced-java/blob/master/docs/high-concurrency/database-shard.md

三  如何設計讓系統從未分庫分表動態切換到分庫分表上

1  面試題

  如今有一個未分庫分表的系統,將來要分庫分表,如何設計纔可讓系統從未分庫分表動態切換到分庫分表上?

2  考察點

  如今已經明白爲啥要分庫分表了,你也知道經常使用的分庫分表中間件了,你也設計好大家如何分庫分表的方案了(水平拆分、垂直拆分、分表),那問題來了,你接下來該怎麼把你那個單庫單表的系統給遷移到分庫分表上去?

  因此這都是一環扣一環的,就是看你有沒有全流程經歷過這個過程。

3  解析

  1  停機遷移方案

  最簡單的方案,在凌晨 12 點開始運維,網站或者 app 掛個公告,說 0 點到早上 6 點進行運維,沒法訪問。接着到 0 點停機,系統停掉。

  因爲沒有新的流量寫入了,所以此時老的單庫單表數據庫的數據不會發生變化。此時能夠經過開發的數據傳輸工具,而後將單庫單表的數據讀出來,寫到分庫分表裏面去。

  導數完了以後,修改系統的數據庫鏈接配置,包括可能代碼和 SQL 也許有修改,那你就用最新的代碼,而後直接啓動連到新的分庫分表上去。

  

  如圖所示,具體步驟以下:

  (1)系統停機,不容許外界訪問

  (2)經過後臺程序將老的單表單庫中的數據按照按照分庫分表的規則導入到新的分庫分表中。

  (3)修改系統配置及分庫分享相關SQL,使系統鏈接到新的數據庫上,而且以後新的數據都寫入到新的分庫分表中。

  (4)啓動系統,運行外界訪問。

  2  雙寫遷移方案

  在停機遷移方案中,須要使系統中止運行一段時間,這有時候對線上是沒法接受的,所以能夠採用雙寫遷移方案,不須要停機。 

  簡單來講,就是在線上系統裏面,以前全部寫庫的地方,增刪改操做,除了對老庫增刪改,都加上對新庫的增刪改,這就是所謂的雙寫,同時寫倆庫,老庫和新庫。此時查詢仍然從老的庫中查詢。

  而後系統部署以後,新庫數據差太遠,用後臺程序跑起來讀老庫數據寫入到新庫,寫的時候要根據 modified_time 這類字段判斷這條數據最後修改的時間,除非是讀出來的數據在新庫裏沒有,或者是比新庫的數據新纔會寫。簡單來講,就是不容許用老數據覆蓋新數據。

  導完一輪以後,有可能數據仍是存在不一致,那麼就程序自動作一輪校驗,比對新老庫每一個表的每條數據,接着若是有不同的,就針對那些不同的,從老庫讀數據再次寫。反覆循環,直到兩個庫每一個表的數據都徹底一致爲止。

  接着當數據徹底一致了,就切換讀數據重新庫中讀取,同時斷開老數據庫鏈接。

  

  參考:https://github.com/yuejuntao/advanced-java/blob/master/docs/high-concurrency/database-shard-method.md

四  分庫分表後,id主鍵如何處理

1  面試題

   分庫分表以後,id 主鍵如何處理?

2  考察點

  其實這是分庫分表以後你必然要面對的一個問題,就是 id 咋生成?

  由於要是分紅多個表以後,每一個表都是從 1 開始累加,那確定不對啊,須要一個全局惟一的 id 來支持。因此這都是你實際生產環境中必須考慮的問題。

3  解析

  1  基於數據庫的實現方案

  (1)數據庫自增id 

  這個就是說你的系統裏每次獲得一個 id,都是往一個庫的一個表裏插入一條沒什麼業務含義的數據,而後獲取一個數據庫自增的一個 id。拿到這個 id 以後再往對應的分庫分表裏去寫入。

  例如名爲table的表結構以下:

    id  field

    35  a

  每一次生成id的時候,都訪問數據庫,執行以下語句:

begin;
  REPLACE INTO table ( feild )  VALUES ( 'a' );
  SELECT LAST_INSERT_ID();
commit;

  REPLACE INTO 的含義是插入一條記錄,若是表中惟一索引的值遇到衝突,則替換老數據。

  這樣一來,每次均可以獲得一個遞增的ID。

  這個方案的好處就是方便簡單;缺點就是單庫生成自增 id,要是高併發的話,就會有瓶頸的

  若是改進一下,那麼就專門開一個服務出來,這個服務每次就拿到當前 id 最大值,而後本身遞增幾個 id,一次性返回一批 id,而後再把當前最大 id 值修改爲遞增幾個 id 以後的一個值;可是不管如何都是基於單個數據庫。

  適合的場景:你分庫分表無非兩個緣由:(1)單庫併發過高,(2)單庫數據量太大;除非是你併發不高,可是數據量太大致使的分庫分表擴容,你能夠用這個方案,由於可能每秒最高併發最多就幾百,那麼就走單獨的一個庫和表生成自增主鍵便可。

   (2)設置數據庫sequence或者表的自增字段步長

  因爲使用數據庫自增id只能基於單個數據庫和表,所以在併發量高的狀況下並不適用。此時能夠經過設置數據庫sequence(Oracle)或者表的自增字段來進行水平伸縮。

  例如:搭建8個數據庫節點,每一個數據庫中使用一個表來產生id。其中每一個數據庫節點使用一個sequence功能來產生id,每一個sequence的起始ID不一樣,而且依次遞增,步長都是8:

  所以此時既能將單個庫生成自增id擴展到多個庫,增長了併發量。

  適合的場景:在用戶防止產生的 ID 重複時,這種方案實現起來比較簡單,也能達到性能目標。可是服務節點固定,步長也固定,未來若是還要增長服務節點,就很差搞了。

  2  UUID

  好處就是本地生成,不要基於數據庫來了;

  很差之處就是,UUID 太長了、佔用空間大,做爲主鍵性能太差了;

  更重要的是,UUID 不具備有序性,會致使 B+ 樹索引在寫的時候有過多的隨機寫操做(連續的 ID 能夠產生部分順序寫),還有,因爲在寫的時候不能產生有順序的 append 操做,而須要進行 insert 操做,將會讀取整個 B+ 樹節點到內存,在插入這條記錄後會將整個節點寫回磁盤,這種操做在記錄佔用空間比較大的狀況下,性能降低明顯。

  例如以下B+樹索引:

  

  若是咱們的ID按遞增的順序來插入,好比陸續插入8,9,10,新的ID都只會插入到最後一個節點當中。當最後一個節點滿了,會裂變出新的節點。這樣的插入是性能比較高的插入,由於這樣節點的分裂次數最少,並且充分利用了每個節點的空間。

  可是,若是咱們的插入徹底無序,不但會致使一些中間節點產生分裂,也會白白創造出不少不飽和的節點,這樣大大下降了數據庫插入的性能。

  適合的場景:若是你是要隨機生成個什麼文件名、編號之類的,你能夠用 UUID,可是做爲主鍵是不能用 UUID 的。

  UUID.randomUUID().toString().replace(「-」, 「」) -> sfsdf23423rr234sfdaf

  3  獲取系統當前時間

  這個就是獲取當前時間便可,可是問題是,併發很高的時候,好比一秒併發幾千,會有重複的狀況,這個是確定不合適的。基本就不用考慮了。

  適合的場景:通常若是用這個方案,是將當前時間跟不少其餘的業務字段拼接起來,做爲一個 id,若是業務上你以爲能夠接受,那麼也是能夠的。你能夠將別的業務字段值跟當前時間拼接起來,組成一個全局惟一的編號。

  4  snowflake算法

  snowflake 算法是 twitter 開源的分佈式 id 生成算法,採用 Scala 語言實現,是把一個 64 位的 long 型的 id分紅四部分:1 個 bit 是不用的,用其中的 41 bit 做爲毫秒數,用 10 bit 做爲工做機器 id,12 bit 做爲序列號。

   (1)1 bit:不用,爲啥呢?由於二進制裏第一個 bit 爲若是是 1,那麼都是負數,可是咱們生成的 id 都是正數,因此第一個 bit 統一都是 0。

   (2)41 bit:表示的是時間戳,單位是毫秒。41 bit 能夠表示的數字多達 2^41 - 1,也就是能夠標識 2^41 - 1 個毫秒值,換算成年就是表示69年的時間。

   (3)10 bit:記錄工做機器 id,表明的是這個服務最多能夠部署在 2^10臺機器上哪,也就是1024臺機器。可是 10 bit 裏 5 個 bit 表明機房 id,5 個 bit 表明機器 id。意思就是最多表明 2^5個機房(32個機房),每一個機房裏能夠表明 2^5 個機器(32臺機器)。

   (4)12 bit:這個是用來記錄同一個毫秒內產生的不一樣 id,12 bit 能夠表明的最大正整數是 2^12 - 1 = 4096,也就是說能夠用這個 12 bit 表明的數字來區分同一個毫秒內的 4096 個不一樣的 id。

public class IdWorker {
    //工做節點id(0~31)
    private long workerId;
  //數據中心id(0~31)    
  private long datacenterId;
  //毫秒內序列(0~4095) 
   private long sequence;

  //構造函數
    public IdWorker(long workerId, long datacenterId, long sequence) {
        // sanity check for workerId
        // 這兒不就檢查了一下,要求就是你傳遞進來的機房id和機器id不能超過32,不能小於0
        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));
        }
        System.out.printf(
                "worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
                timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId);

        this.workerId = workerId;
        this.datacenterId = datacenterId;
        this.sequence = sequence;
    }

  //初始時間戳
    private long twepoch = 1288834974657L;
  //機器id所佔位數
    private long workerIdBits = 5L;
  //數據標識id所佔位數
    private long datacenterIdBits = 5L;

    // 這個是二進制運算,就是 5 bit最多隻能有31個數字,也就是說機器id最多隻能是32之內
    private long maxWorkerId = -1L ^ (-1L << workerIdBits);

    // 這個是一個意思,就是 5 bit最多隻能有31個數字,機房id最多隻能是32之內
    private long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
  //序列在id中所佔的位數
    private long sequenceBits = 12L;
  //機器id的偏移量(12)
    private long workerIdShift = sequenceBits;
  //機房id偏移量(12+5)
    private long datacenterIdShift = sequenceBits + workerIdBits;
  //時間戳偏移量(5+5+12)
    private long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
  //生成的序列的掩碼,治理爲4095(0b111111111111=0xfff=4095)
    private long sequenceMask = -1L ^ (-1L << sequenceBits);
  //上次生成id的時間戳
    private long lastTimestamp = -1L;

    public long getWorkerId() {
        return workerId;
    }

    public long getDatacenterId() {
        return datacenterId;
    }

    public long getTimestamp() {
        return System.currentTimeMillis();
    }

  //獲取下一個id(用同步鎖保證線程安全)
    public synchronized long nextId() {
        // 這兒就是獲取當前時間戳,單位是毫秒
        long timestamp = timeGen();
     //若是當前時間小於上一次id生成的時間戳,則說明系統時鐘回退過,這個時候拋出異常
        if (timestamp < lastTimestamp) {
            System.err.printf("clock is moving backwards.  Rejecting requests until %d.", lastTimestamp);
            throw new RuntimeException(String.format(
                    "Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            //若是是同一時間生成的,則進入毫秒內序列 
        //這個意思是說一個毫秒內最多隻能有4096個數字
            // 不管你傳遞多少進來,這個位運算保證始終就是在4096這個範圍內,避免你本身傳遞個sequence超過了4096這個範圍
            sequence = (sequence + 1) & sequenceMask;
       //sequence等於0說明毫秒內序列已經增加到最大值
            if (sequence == 0) {
         //阻塞到下一個毫秒,獲取新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0;
        }

        // 這兒記錄一下最近一次生成id的時間戳,單位是毫秒
        lastTimestamp = timestamp;

        // 這兒就是將時間戳左移,放到 41 bit那兒;
        // 將機房 id左移放到 5 bit那兒;
        // 將機器id左移放到5 bit那兒;將序號放最後12 bit;
        // 最後拼接起來成一個 64 bit的二進制數字,轉換成 10 進制就是個 long 型
        return ((timestamp - twepoch) << timestampLeftShift) | (datacenterId << datacenterIdShift)
                | (workerId << workerIdShift) | sequence;
    }

//阻塞到下一個毫秒,知道獲取到新的時間戳
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }

    // ---------------測試---------------
    public static void main(String[] args) {
        IdWorker worker = new IdWorker(1, 1, 1);
        for (int i = 0; i < 30; i++) {
            System.out.println(worker.nextId());
        }
    }

}
View Code

說明:

  1.得到單一機器的下一個序列號,使用Synchronized控制併發,而非CAS的方式,是由於CAS不適合併發量很是高的場景。

  2.若是當前毫秒在一臺機器的序列號已經增加到最大值4095,則使用while循環等待直到下一毫秒。

  3.若是當前時間小於記錄的上一個毫秒值,則說明這臺機器的時間回撥了,拋出異常。但若是這臺機器的系統時間在啓動以前回撥過,那麼有可能出現ID重複的危險。

SnowFlake算法的優勢:

  1.生成ID時不依賴於DB,徹底在內存生成,高性能高可用。

  2.ID呈趨勢遞增,後續插入索引樹的時候性能較好。

SnowFlake算法的缺點:

  依賴於系統時鐘的一致性。若是某臺機器的系統時鐘回撥,有可能形成ID衝突,或者ID亂序。

參考:https://mp.weixin.qq.com/s/JiyZbaAujBtD8F4ddc-uAw

五  count(1)比count(*)效率高嗎

  有 Where 條件的 count,會根據掃碼結果count 一下全部的行數,其性能更依賴於你的 Where 條件,因此文章咱們僅針對沒有 Where 的狀況進行說明。

  MyISAM 引擎會把一個表的總行數記錄了下來,因此在執行 count(*) 的時候會直接返回數量,執行效率很高。

  在 MySQL 5.5 之後默認引擎切換爲 InnoDB,InnoDB 由於增長了版本控制(MVCC)的緣由,同時有多個事務訪問數據而且有更新操做的時候,每一個事務須要維護本身的可見性,那麼每一個事務查詢到的行數也是不一樣的,因此不能緩存具體的行數,他每次都須要 count 一下全部的行數。那麼 count(1) 和 count(*)有區別麼?

  Returns a count of the number of non-NULL values of expr in the rows retrieved by a SELECT statement. The result is a BIGINT value.

  大體的解釋是返回 SELECT 語句檢索的行中 expr 的非 NULL 值的計數,到這裏咱們就明白了,首先它是一個聚合函數,而後對 SELECT 的結果集進行計數,可是須要參數不爲 NULL。那麼咱們繼續閱讀官網的內容:

  COUNT(*) is somewhat different in that it returns a count of the number of rows retrieved, whether or not they contain NULL values.

  大體的內容是說,count(*) 不一樣,他不關心這個返回值是否爲空都會計算他的count,由於 count(1) 中的 1 是恆真表達式,那麼 count(*) 仍是 count(1) 都是對全部的結果集進行 count,因此他們本質上沒有什麼區別。

  到這裏咱們明白了 count(*) 和 count(1) 本質上面實際上是同樣的,那麼 count(column) 又是怎麼回事呢?

  count(column) 也是會遍歷整張表,可是不一樣的是它會拿到 column 的值之後判斷是否爲空,而後再進行累加,那麼若是針對主鍵須要解析內容,若是是二級因此須要再次根據主鍵獲取內容,又是一次 IO 操做,因此 count(column) 的性能確定不如前二者嘍,若是按照效率比較的話:

  count(*)=count(1)>count(primary key)>count(column)

  既然 count(*) 在查詢上依賴於全部的數據集,是否是咱們在設計上也須要儘可能的規避全量 count 呢?一般狀況咱們針對可預見的 count 查詢會作適當的緩存,能夠是 Redis,也能夠是獨立的 MySQL count 表,固然不管是哪一種方式咱們都須要考慮一致性的問題。

相關文章
相關標籤/搜索