使用bitset實現毫秒級查詢

来源:http://www.cnblogs.com/ncbest/archive/2017/10/23/7703615.html
-Advertisement-
Play Games

前言 因為業務要求api的一次請求響應時間在10ms以內,所以傳統的資料庫查詢操作直接被排除(網路io和磁碟io)。通過調研,最終使用了bieset,目前已經正常運行了很久 bitset介紹 看JDK中的解釋簡直一頭霧水,用我自己的理解概括一下 1. bitset的內部實現是long數組 2. se ...


前言

因為業務要求api的一次請求響應時間在10ms以內,所以傳統的資料庫查詢操作直接被排除(網路io和磁碟io)。通過調研,最終使用了bieset,目前已經正常運行了很久
***

bitset介紹

看JDK中的解釋簡直一頭霧水,用我自己的理解概括一下

  1. bitset的內部實現是long數組
  2. set中每一個位的預設值為false(0)
  3. bitset長度按需增長
  4. bitset非線程安全
    ***

    bitset關鍵方法分析
    /**
     * Sets the bit at the specified index to {@code true}.
     *
     * @param  bitIndex a bit index
     * @throws IndexOutOfBoundsException if the specified index is negative
     * @since  JDK1.0
     */
    public void set(int bitIndex) {
        if (bitIndex < 0)
            throw new IndexOutOfBoundsException("bitIndex < 0: " + bitIndex);
    
        int wordIndex = wordIndex(bitIndex);
        expandTo(wordIndex);
    
        words[wordIndex] |= (1L << bitIndex); // Restores invariants
    
        checkInvariants();
    }

    設置指定“位”為true,bitIndex參數為非負整數。假設我們執行以下代碼,觀察上面代碼中worIndex,words[wordIndex]值的變化

    BitSet bs = new BitSet()
    bs.set(0);
    bs.set(1);
    bs.set(2);
    bs.set(3);
    bs.set(4);
    bitIndex wordIndex words[wordIndex] words[wordIndex]二進位表示
    0 0 1 0001
    1 0 3 0011
    2 0 7 0111
    3 0 15 1111
    4 0 31 0001 1111

    通過上表,我們可以很清晰的根據bitIndex和words[wordIndex]二進位值的對應關係,得到一個結論,即:bitset中每一個long可以表示64個非負整數在bitSet中存在與否。例如:0001可以表示整數0在bitset中存在,1111可以表示整數3,2,1,0在bitset中存在。
    ***

進入正題,實現bitset毫秒級查詢

想象一個場景,我們有一張user表,name唯一。

CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) NOT NULL,
  `address` varchar(255) NOT NULL comment '地址',
  `gender` varchar(10) NOT NULL comment '性別',
  `age` varchar(10) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=utf8;

假設我們要查詢“北京市18歲的女生”,那麼對應的sql如下:

select * from `user` where address='北京' and age='18' and gender='girl';

如何使用bitset實現同樣的查詢呢?

  1. 將user表數據載入進記憶體中
  2. 為user表建立address,age,gender維度的bitset索引
  3. 根據索引查詢數據
1.將user表數據載入進記憶體中

將user表從資料庫讀取出來存入List

User實體類:

public class User implements Cloneable {
    private String name;
    private String address;
    private String gender;
    private String age;

    @Override
    public String toString() {
        return "User [name=" + name + ", address=" + address + ", gender=" + gender + ", age=" + age + "]";
    }

    @Override
    public User clone() {
        User user = null;
        try {
            user = (User) super.clone();
        } catch (CloneNotSupportedException e) {
            e.printStackTrace();
        }
        return user;
    }
    //省略get set 方法。。。

2.建立索引

創建bitset索引模型類

public class BitSetIndexModel {
    private String type;//索引類型:address,age,gender
    private ConcurrentHashMap<String, Integer> map;//索引類型和bitSet在bsList中下標的映射關係
    private List<String> list;//索引類型的值集合,例如gender:girl,boy
    private List<BitSet> bsList;//bitset集合

    public BitSetIndex() {
        
    }

    public BitSetIndexModel(String type) {
        this.type = type;
        map = new ConcurrentHashMap<String, Integer>();
        list = new ArrayList<String>();
        bsList = new ArrayList<BitSet>();
    }
    
    public String getType() {
        return type;
    }

    public void setType(String type) {
        this.type = type;
    }

    public Map<String, Integer> getMap() {
        return map;
    }

    public void setMap(ConcurrentHashMap<String, Integer> map) {
        this.map = map;
    }

    public List<String> getList() {
        return list;
    }

    public void setList(List<String> list) {
        this.list = list;
    }

    public List<BitSet> getBsList() {
        return bsList;
    }

    public void setBsList(List<BitSet> bsList) {
        this.bsList = bsList;
    }

    /**
     *
     * @param str
     * @param i
     */
    public void createIndex(String str, int i) {
        BitSet bitset = null;
        //獲取‘str’對應的bitset在bsList中的下標
        Integer index = this.getMap().get(str);
        if (index != null) {
            bitset = this.getBsList().get(index);
            if (bitset == null) {
                bitset = new BitSet();
                this.getBsList().add(index, bitset);
            }
            bitset.set(i, true);// 將str對應的位置為true,true可省略
        } else {
            bitset = new BitSet();
            List<String> list = this.getList();
            list.add(str);
            index = list.size() - 1;
            bitset.set(i, true);
            this.getBsList().add(bitset);
            this.getMap().put(str, index);
        }
    }
    
    /**
     * 從entity里拿出符合條件的bitset
     *
     * @param str
     * @return
     */
    public BitSet get(String str) {
        BitSet bitset = null;
        str = str.toLowerCase();
        Integer index = this.getMap().get(str);

        if (index != null) {
            bitset = this.getBsList().get(index);
        } else {
            bitset = new BitSet();
        }
        return bitset;
    }

    /**
     * bitset的與運算
     *
     * @param str
     * @param bitset
     * @return
     */
    public BitSet and(String str, BitSet bitset) {
        if (str != null) {
            str = str.toLowerCase();
            if (bitset != null) {
                bitset.and(get(str));
            } else {
                bitset = new BitSet();
                bitset.or(get(str));
            }
        }
        return bitset;
    }

    /**
     * bitset的或運算
     *
     * @param str
     * @param bitset
     * @return
     */
    public BitSet or(String str, BitSet bitset) {
        if (str != null) {
            str = str.toLowerCase();
            if (bitset != null) {
                bitset.or(get(str));
            } else {
                bitset = new BitSet();
                bitset.or(get(str));
            }
        }
        return bitset;
    }

    /**
     * 獲取bitset值為true的 即 把 bitset翻譯為list的索引
     *
     * @param bitset
     * @return
     */
    public static List<Integer> getRealIndexs(BitSet bitset) {
        List<Integer> indexs = new ArrayList<Integer>();
        if (bitset != null) {
            int i = bitset.nextSetBit(0);
            if (i != -1) {
                indexs.add(i);
                for (i = bitset.nextSetBit(i + 1); i >= 0; i = bitset.nextSetBit(i + 1)) {
                    int endOfRun = bitset.nextClearBit(i);
                    do {
                        indexs.add(i);
                    } while (++i < endOfRun);
                }
            }
        }

        return indexs;
    }
    
}

為每一個user對象創建address,gender,age維度索引

public class UserIndexStore {

    private static final String ADDRESS = "address";
    private static final String GENDER = "gender";
    private static final String AGE = "age";
    private BitSetIndexModel address;
    private BitSetIndexModel gender;
    private BitSetIndexModel age;
    private ConcurrentHashMap<Integer, User> userMap;//存儲所有的user數據
        private ConcurrentHashMap<String, Integer> nameIndexMap;//name和index映射

    public static final UserIndexStore INSTANCE = getInstance();

    private UserIndexStore() {
        init();
    }

    public static UserIndexStore getInstance() {
        return UserIndexStoreHolder.instance;
    }

    private static class UserIndexStoreHolder {
        private static UserIndexStore instance = new UserIndexStore();
    }

    private void init() {
        this.address = new BitSetIndexModel(ADDRESS);
        this.gender = new BitSetIndexModel(GENDER);
        this.age = new BitSetIndexModel(AGE);
        userMap = new ConcurrentHashMap<Integer, User>();
                nameIndexMap =  new ConcurrentHashMap<String, Integer>();
    }

    /**
     * 構建索引
     * @param users 
     */
    public void createIndex(List<User> users) {
        if (users != null && users.size() > 0) {
            for (int index = 0; index < users.size(); index++) {
                User user = users.get(index);
                createIndex(user, index);
            }
        }
    }

    private void createIndex(User user, int index) {
        getAddress().update(user.getAddress(), index);
        getGender().update(user.getGender(), index);
        getAge().update(user.getAge(), index);
        this.userMap.put(index, user);
        this.nameIndexMap.put(user.getName(), index);
    }

    public BitSet query(String address, String gender, String age) {
        BitSet bitset = null;
        bitset = getAddress().and(address, bitset);
        bitset = getGender().and(gender, bitset);
        bitset = getAge().and(age, bitset);
        return bitset;
    }

    public User findUser(Integer index) {
        User user = this.userMap.get(index);
        if (user != null) {
            return user.clone();//可能會對user做修改操作,要保證記憶體原始數據不變
        }
        return null;
    }

    public BitSetIndexModel getAddress() {
        return address;
    }

    public void setAddress(BitSetIndexModel address) {
        this.address = address;
    }

    public BitSetIndexModel getGender() {
        return gender;
    }

    public void setGender(BitSetIndexModel gender) {
        this.gender = gender;
    }

    public BitSetIndexModel getAge() {
        return age;
    }

    public void setAge(BitSetIndexModel age) {
        this.age = age;
    }

}
3.測試bitset
public class BitSetTest {

    public static void main(String[] args) {
        List<User> users = buildData();
        UserIndexStore.getInstance().createIndex(users);
        ExecutorService executorService = Executors.newFixedThreadPool(20);
        int num = 2000;
        long begin1 = System.currentTimeMillis();
        for (int i = 0; i < num; i++) {
            Runnable syncRunnable = new Runnable() {
                @Override
                public void run() {
                    List<Integer> indexs = BitSetIndexModel.getRealIndexs(UserIndexStore.getInstance().query("北京", "girl", "18"));
                    for (Integer index : indexs) {
                        UserIndexStore.getInstance().findUser(index);
                    }
                }
            };
            executorService.execute(syncRunnable);
        }
        executorService.shutdown();
        while (true) {
            try {
                if (executorService.awaitTermination(1, TimeUnit.SECONDS)) {
                    System.out.println("單次查詢時間為:" + (System.currentTimeMillis() - begin1) / num + "ms");
                    break;
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private static List<User> buildData() {
        String[] addresss = { "北京", "上海", "深圳" };
        String[] ages = { "16", "17", "18" };
        List<User> users = new ArrayList<>();
        for (int i = 0; i < 200000; i++) {
            User user = new User();
            user.setName("name" + i);
            int rand = ThreadLocalRandom.current().nextInt(3);
            user.setAddress(addresss[ThreadLocalRandom.current().nextInt(3)]);
            user.setGender((rand & 1) == 0 ? "girl" : "boy");
            user.setAge(ages[ThreadLocalRandom.current().nextInt(3)]);
            users.add(user);
        }
        return users;
    }

}

測試結果(查詢2w次):
數據量(users.size()) | 併發數 | 平均查詢時間
---|---|---
20w | 10 | 1ms
50w | 20 | 3ms
100w| 50 | 9ms

測試機為thinkpad x240 i5 8g記憶體

4.總結

==優點==:
通過測試發現隨著數據量的增大和併發數的提高,平均耗時並沒有明顯升高,並且響應時間都在10ms以內

==缺點==:

  1. 不適合數據量太大的情況,因為需要把數據全部載入進記憶體
  2. 不適合複雜查詢
  3. 不適合對name,id等唯一值做查詢

    後記

    因為我們的查詢業務比較簡單,唯一的要求是速度,並且數據量也不大,每張表的數據量都不超過100w,所以使用這種方式比較合適。
    在本篇文章中只談到瞭如何創建索引,以及最基本的查詢,在下一篇中我會繼續說明如何更新索引,以及一些複雜查詢,比如<,>,between and。

轉載請註明出處


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • "條記錄", 'prev' => "上一頁", 'next' => "下一頁", 'first'=> "首頁", 'last' => "末頁" ); //在分頁信息中顯示內容,可... ...
  • 參考:【Python yield 使用淺析】、【Python xrange與range的區別】等 一個帶有 yield 的函數就是一個 generator,它和普通函數不同,生成一個 generator 看起來像函數調用,但不會執行任何函數代碼,直到對其調用 next()(在 for 迴圈中會自動調 ...
  • 數組的定義:是用統一的名字代表這批數據,用序號來區分各個數據。數組是有序數據的集合。 如何理解:其實就是一個同時放很多數據的變數。 如 int a0;int a1; int a2; a=1; a=2; a=3; 這成了反覆賦值,最後a=3; a怎麼能同時放下1,2,3......? 必須是同樣的數據 ...
  • 一、前言 在工作中,難免遇到各種各樣的問題,每個人似乎都有一套自己的解決方案。而我,又不想每次解決完問題就把東西扔了,撿了芝麻,丟了西瓜,什麼時候才能進步勒?學習要靠積累,畢竟量變才能引起質變嘛。所以寫了這篇博文,不定時更新自己項目中遇到的問題、踩過的那些坑...... 二、項目 1、Java 將兩 ...
  • 引言 Sun所指定的JavaBean規範很大程度上是為IDE準備的 它讓IDE能夠以可視化的方式設置JavaBean的屬性。如果在IDE中開發一個可視化的應用程式,則需要通過屬性設置的方式對組成應用的各種組件進行定製,IDE通過屬性編輯器讓開發人員使用可視化的方式設置組件的屬性。 一般的IDE都支持 ...
  • 函數外部聲明有什麼作用? 讓我們定義的函數應用範圍更廣,生命更長久。共用。 也就是說所有的外部函數都可以直接調用。 ...
  • 觸發器 觸發器(trigger)是一些過程,與表關係密切,用於保護表中的數據,當一個基表被修改(INSERT、UPDATE或DELETE)時,觸發器自動執行,例如通過觸發器可實現多個表間數據的一致性和完整性。觸發器和應用程式無關。 觸發器的類型有三種: (1)DML觸發器。Oracle可以在DML( ...
  • host,$this->uid,$this->pwd,$this->dbname); //執行sql語句 $reslut = $db->query($sql); if(!$reslut){ die($db->error); } //從結果集對象裡面取數據 if($type==1) ... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...