對(duì)面的程序員趕緊看過(guò)來(lái)斜友!布隆過(guò)濾器又有新玩法了~ | 博學(xué)谷狂野架構(gòu)師

布隆過(guò)濾器

img
  • 作者: 博學(xué)谷狂野架構(gòu)師
  • GitHub:GitHub地址 (有我精心準(zhǔn)備的130本電子書(shū)PDF)

只分享干貨、不吹水八秃,讓我們一起加油碱妆!??

什么是布隆過(guò)濾器

布隆過(guò)濾器(Bloom Filter)是1970年由布隆提出的。它實(shí)際上是一個(gè)很長(zhǎng)的二進(jìn)制向量和一系列隨機(jī)映射函數(shù)。布隆過(guò)濾器可以用于檢索一個(gè)元素是否在一個(gè)集合中。它的優(yōu)點(diǎn)是空間效率和查詢時(shí)間都比一般的算法要好的多扶叉,缺點(diǎn)是有一定的誤識(shí)別率和刪除困難舶治。

布隆過(guò)濾器可以理解為一個(gè)不怎么精確的 set 結(jié)構(gòu)葱跋,當(dāng)你使用它的 contains 方法判斷某個(gè)對(duì)象是否存在時(shí),它可能會(huì)誤判。但是布隆過(guò)濾器也不是特別不精確,只要參數(shù)設(shè)置的合理繁成,它的精確度可以控制的相對(duì)足夠精確,只會(huì)有小小的誤判概率淑玫。

當(dāng)布隆過(guò)濾器說(shuō)某個(gè)值存在時(shí)巾腕,這個(gè)值可能不存在;當(dāng)它說(shuō)不存在時(shí)絮蒿,那就肯定不存在尊搬。打個(gè)比方,當(dāng)它說(shuō)不認(rèn)識(shí)你時(shí)土涝,肯定就不認(rèn)識(shí)佛寿;當(dāng)它說(shuō)見(jiàn)過(guò)你時(shí),可能根本就沒(méi)見(jiàn)過(guò)面但壮,不過(guò)因?yàn)槟愕哪樃J(rèn)識(shí)的人中某臉比較相似 (某些熟臉的系數(shù)組合)冀泻,所以誤判以前見(jiàn)過(guò)你。
套在上面的使用場(chǎng)景中茵肃,布隆過(guò)濾器能準(zhǔn)確過(guò)濾掉那些已經(jīng)看過(guò)的內(nèi)容,那些沒(méi)有看過(guò)的新內(nèi)容袭祟,它也會(huì)過(guò)濾掉極小一部分 (誤判)验残,但是絕大多數(shù)新內(nèi)容它都能準(zhǔn)確識(shí)別。這樣就可以完全保證推薦給用戶的內(nèi)容都是無(wú)重復(fù)的巾乳。

布隆過(guò)濾器的原理

其本質(zhì)就是一個(gè)只包含0和1的數(shù)組您没。具體操作當(dāng)一個(gè)元素被加入到集合里面后鸟召,該元素通過(guò)K個(gè)Hash函數(shù)運(yùn)算得到K個(gè)hash后的值,然后將K個(gè)值映射到這個(gè)位數(shù)組對(duì)應(yīng)的位置氨鹏,把對(duì)應(yīng)位置的值設(shè)置為1欧募。查詢是否存在時(shí),我們就看對(duì)應(yīng)的映射點(diǎn)位置如果全是1仆抵,他就很可能存在(跟hash函數(shù)的個(gè)數(shù)和hash函數(shù)的設(shè)計(jì)有關(guān))跟继,如果有一個(gè)位置是0,那這個(gè)元素就一定不存在镣丑。

img
  1. 首先需要初始化一個(gè)二進(jìn)制的數(shù)組舔糖,長(zhǎng)度設(shè)為 L,同時(shí)初始值全為 0 莺匠。
  2. 當(dāng)寫(xiě)入一個(gè) A1=1000 的數(shù)據(jù)時(shí)金吗,需要進(jìn)行 H 次 hash 函數(shù)的運(yùn)算(這里為 2 次);與 HashMap 有點(diǎn)類(lèi)似趣竣,通過(guò)算出的 HashCode 與 L 取模后定位到 0摇庙、2 處,將該處的值設(shè)為 1遥缕。
  3. A2=2000 也是同理計(jì)算后將 4卫袒、7 位置設(shè)為 1。
  4. 當(dāng)有一個(gè) B1=1000 需要判斷是否存在時(shí)通砍,也是做兩次 Hash 運(yùn)算玛臂,定位到 0、2 處封孙,此時(shí)他們的值都為 1 迹冤,所以認(rèn)為 B1=1000 存在于集合中。
  5. 當(dāng)有一個(gè) B2=3000 時(shí)虎忌,也是同理泡徙。第一次 Hash 定位到 index=4 時(shí),數(shù)組中的值為 1膜蠢,所以再進(jìn)行第二次 Hash 運(yùn)算堪藐,結(jié)果定位到 index=5 的值為 0,所以認(rèn)為 B2=3000 不存在于集合中挑围。

整個(gè)的寫(xiě)入礁竞、查詢的流程就是這樣,匯總起來(lái)就是:

對(duì)寫(xiě)入的數(shù)據(jù)做 H 次 hash 運(yùn)算定位到數(shù)組中的位置杉辙,同時(shí)將數(shù)據(jù)改為 1 模捂。當(dāng)有數(shù)據(jù)查詢時(shí)也是同樣的方式定位到數(shù)組中。一旦其中的有一位為 0 則認(rèn)為數(shù)據(jù)肯定不存在于集合,否則數(shù)據(jù)可能存在于集合中狂男。

布隆過(guò)濾器的特點(diǎn)

只要返回?cái)?shù)據(jù)不存在综看,則肯定不存在

返回?cái)?shù)據(jù)存在岖食,但只能是大概率存在红碑。

同時(shí)不能清除其中的數(shù)據(jù)。

在有限的數(shù)組長(zhǎng)度中存放大量的數(shù)據(jù)泡垃,即便是再完美的 Hash 算法也會(huì)有沖突析珊,所以有可能兩個(gè)完全不同的 A、B 兩個(gè)數(shù)據(jù)最后定位到的位置是一模一樣的兔毙。

刪除數(shù)據(jù)也是同理唾琼,當(dāng)我把 B 的數(shù)據(jù)刪除時(shí),其實(shí)也相當(dāng)于是把 A 的數(shù)據(jù)刪掉了澎剥,這樣也會(huì)造成后續(xù)的誤報(bào)锡溯。

基于以上的 Hash 沖突的前提,所以 Bloom Filter 有一定的誤報(bào)率哑姚,這個(gè)誤報(bào)率和 Hash 算法的次數(shù) H祭饭,以及數(shù)組長(zhǎng)度 L 都是有關(guān)的。

應(yīng)用場(chǎng)景

緩存穿透

我們經(jīng)常會(huì)把一部分?jǐn)?shù)據(jù)放在Redis等緩存叙量,比如產(chǎn)品詳情倡蝙。這樣有查詢請(qǐng)求進(jìn)來(lái),我們可以根據(jù)產(chǎn)品Id直接去緩存中取數(shù)據(jù)绞佩,而不用讀取數(shù)據(jù)庫(kù)寺鸥,這是提升性能最簡(jiǎn)單,最普遍品山,也是最有效的做法胆建。一般的查詢請(qǐng)求流程是這樣的:先查緩存,有緩存的話直接返回肘交,如果緩存中沒(méi)有笆载,再去數(shù)據(jù)庫(kù)查詢,然后再把數(shù)據(jù)庫(kù)取出來(lái)的數(shù)據(jù)放入緩存涯呻,一切看起來(lái)很美好凉驻。但是如果現(xiàn)在有大量請(qǐng)求進(jìn)來(lái),而且都在請(qǐng)求一個(gè)不存在的產(chǎn)品Id复罐,會(huì)發(fā)生什么涝登?既然產(chǎn)品Id都不存在,那么肯定沒(méi)有緩存效诅,沒(méi)有緩存胀滚,那么大量的請(qǐng)求都懟到數(shù)據(jù)庫(kù)咳短,數(shù)據(jù)庫(kù)的壓力一下子就上來(lái)了,還有可能把數(shù)據(jù)庫(kù)打死蛛淋。

使用布隆過(guò)濾器的特點(diǎn),只要返回?cái)?shù)據(jù)不存在篡腌,則肯定不存在褐荷,返回?cái)?shù)據(jù)存在,但只能是大概率存在嘹悼,這種特點(diǎn)可以大批量的無(wú)效請(qǐng)求過(guò)濾掉叛甫,能夠穿透緩存的知識(shí)漏網(wǎng)之魚(yú),無(wú)關(guān)緊要杨伙。

檢查單詞拼寫(xiě)

檢查一個(gè)單詞拼寫(xiě)是否正確其监,因?yàn)橛泻A康膯卧~數(shù)量,每天可能有新的單詞限匣,使用布隆過(guò)濾器抖苦,可以將單詞映射到很小的內(nèi)存中,可以經(jīng)過(guò)簡(jiǎn)單的幾次hash運(yùn)行就可以進(jìn)行校驗(yàn)米死,只要返回?cái)?shù)據(jù)不存在锌历,則肯定不存在,返回?cái)?shù)據(jù)存在峦筒,但只能是大概率存在究西,雖然可能有誤報(bào),但是對(duì)系統(tǒng)的提升是革命性的物喷。

Guava的布隆過(guò)濾器

這就又要提起我們的Guava了卤材,它是Google開(kāi)源的Java包,提供了很多常用的功能峦失。

Guava中扇丛,布隆過(guò)濾器的實(shí)現(xiàn)主要涉及到2個(gè)類(lèi),BloomFilter和BloomFilterStrategies宠进,首先來(lái)看一下BloomFilter的成員變量晕拆。需要注意的是不同Guava版本的BloomFilter實(shí)現(xiàn)不同。

布隆過(guò)濾器解析

成員變量分析
COPY/** guava實(shí)現(xiàn)的以CAS方式設(shè)置每個(gè)bit位的bit數(shù)組 */
 private final LockFreeBitArray bits;
 /** hash函數(shù)的個(gè)數(shù) */
 private final int numHashFunctions;
 /** guava中將對(duì)象轉(zhuǎn)換為byte的通道 */
 private final Funnel<? super T> funnel;
 /**
  * 將byte轉(zhuǎn)換為n個(gè)bit的策略材蹬,也是bloomfilter hash映射的具體實(shí)現(xiàn)
  */
 private final Strategy strategy;

這是它的4個(gè)成員變量:

  • LockFreeBitArray是定義在BloomFilterStrategies中的內(nèi)部類(lèi)实幕,封裝了布隆過(guò)濾器底層bit數(shù)組的操作。

  • numHashFunctions表示哈希函數(shù)的個(gè)數(shù)堤器。

  • Funnel昆庇,它和PrimitiveSink配套使用,能將任意類(lèi)型的對(duì)象轉(zhuǎn)化成Java基本數(shù)據(jù)類(lèi)型闸溃,默認(rèn)用java.nio.ByteBuffer實(shí)現(xiàn)整吆,最終均轉(zhuǎn)化為byte數(shù)組拱撵。

  • Strategy是定義在BloomFilter類(lèi)內(nèi)部的接口,代碼如下表蝙,主要有2個(gè)方法拴测,put和mightContain。

    COPYinterface Strategy extends java.io.Serializable {
        /** 設(shè)置元素 */
        <T> boolean put(T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits);
        /** 判斷元素是否存在*/
        <T> boolean mightContain(
        T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits);
        .....
    }
    

創(chuàng)建布隆過(guò)濾器府蛇,BloomFilter并沒(méi)有公有的構(gòu)造函數(shù)集索,只有一個(gè)私有構(gòu)造函數(shù),而對(duì)外它提供了5個(gè)重載的create方法汇跨,在缺省情況下誤判率設(shè)定為3%务荆,采用BloomFilterStrategies.MURMUR128_MITZ_64的實(shí)現(xiàn)。

BloomFilterStrategies.MURMUR128_MITZ_64是Strategy的兩個(gè)實(shí)現(xiàn)之一穷遂,Guava以枚舉的方式提供這兩個(gè)實(shí)現(xiàn)函匕,這也是《Effective Java》書(shū)中推薦的提供對(duì)象的方法之一。

COPYenum BloomFilterStrategies implements BloomFilter.Strategy {
    MURMUR128_MITZ_32() {//....}
    MURMUR128_MITZ_64() {//....}
}

二者對(duì)應(yīng)了32位哈希映射函數(shù)蚪黑,和64位哈希映射函數(shù)盅惜,后者使用了murmur3 hash生成的所有128位,具有更大的空間忌穿,不過(guò)原理是相通的酷窥,我們選擇相對(duì)簡(jiǎn)單的MURMUR128_MITZ_32來(lái)分析。

先來(lái)看一下它的put方法伴网,它用兩個(gè)hash函數(shù)來(lái)模擬多個(gè)hash函數(shù)的情況蓬推,這是布隆過(guò)濾器的一種優(yōu)化。

put方法
COPYpublic <T> boolean put(
    T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
    long bitSize = bits.bitSize();
    // 先利用murmur3 hash對(duì)輸入的funnel計(jì)算得到128位的哈希值澡腾,funnel現(xiàn)將object轉(zhuǎn)換為byte數(shù)組沸伏,
    // 然后在使用哈希函數(shù)轉(zhuǎn)換為long
    long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
    // 根據(jù)hash值的高低位算出hash1和hash2
    int hash1 = (int) hash64;
    int hash2 = (int) (hash64 >>> 32);

    boolean bitsChanged = false;
    // 循環(huán)體內(nèi)采用了2個(gè)函數(shù)模擬其他函數(shù)的思想,相當(dāng)于每次累加hash2
    for (int i = 1; i <= numHashFunctions; i++) {
    int combinedHash = hash1 + (i * hash2);
    // 如果是負(fù)數(shù)就變?yōu)檎龜?shù)
    if (combinedHash < 0) {
        combinedHash = ~combinedHash;
    }
    // 通過(guò)基于bitSize取模的方式獲取bit數(shù)組中的索引,然后調(diào)用set函數(shù)設(shè)置动分。
    bitsChanged |= bits.set(combinedHash % bitSize);
    }
    return bitsChanged;
}

在put方法中毅糟,先是將索引位置上的二進(jìn)制置為1,然后用bitsChanged記錄插入結(jié)果澜公,如果返回true表明沒(méi)有重復(fù)插入成功姆另,而mightContain方法則是將索引位置上的數(shù)值取出,并判斷是否為0坟乾,只要其中出現(xiàn)一個(gè)0迹辐,那么立即判斷為不存在。

mightContain方法
COPYpublic <T> boolean mightContain(
    T object, Funnel<? super T> funnel, int numHashFunctions, BitArray bits) {
    long bitSize = bits.bitSize();
    long hash64 = Hashing.murmur3_128().hashObject(object, funnel).asLong();
    int hash1 = (int) hash64;
    int hash2 = (int) (hash64 >>> 32);

    for (int i = 1; i <= numHashFunctions; i++) {
    int combinedHash = hash1 + (i * hash2);
    // Flip all the bits if it's negative (guaranteed positive number)
    if (combinedHash < 0) {
        combinedHash = ~combinedHash;
    }
    // 和put的區(qū)別就在這里甚侣,從set轉(zhuǎn)換為get明吩,來(lái)判斷是否存在
    if (!bits.get(combinedHash % bitSize)) {
        return false;
    }
    }
    return true;
}

Guava為了提供效率,自己實(shí)現(xiàn)了LockFreeBitArray來(lái)提供bit數(shù)組的無(wú)鎖設(shè)置和讀取殷费。我們只來(lái)看一下它的put函數(shù)印荔。

COPYboolean set(long bitIndex) {
    if (get(bitIndex)) {
    return false;
    }

    int longIndex = (int) (bitIndex >>> LONG_ADDRESSABLE_BITS);
    long mask = 1L << bitIndex; // only cares about low 6 bits of bitIndex

    long oldValue;
    long newValue;
    // 經(jīng)典的CAS自旋重試機(jī)制
    do {
    oldValue = data.get(longIndex);
    newValue = oldValue | mask;
    if (oldValue == newValue) {
        return false;
    }
    } while (!data.compareAndSet(longIndex, oldValue, newValue));

    bitCount.increment();
    return true;
}

Guava布隆過(guò)濾器使用

引入坐標(biāo)
COPY<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>28.0-jre</version>
</dependency>
代碼實(shí)現(xiàn)
COPY
public class GuavaBloomFilter {
    /**
     * 設(shè)置布隆過(guò)濾器大小
     */
    private static final int size = 100000;
    /**
     * 構(gòu)建一個(gè)BloomFilter
     *  第一個(gè)參數(shù)Funnel類(lèi)型的參數(shù)
     *  第二個(gè)參數(shù) 期望處理的數(shù)據(jù)量
     *  第三個(gè)參數(shù) 誤判率 可不加低葫,默認(rèn) 0.03D
     */
    private static final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), size);


    public static void main(String[] args) {
        //成功計(jì)數(shù)
        float success = 0;
        //失敗計(jì)數(shù)
        float fial = 0;
        Set<String> stringSet = new HashSet<String>();
        for (int i = 0; i < size; i++) {
            //生成隨機(jī)字符串
            String randomStr = RandomStringUtils.randomNumeric(10);
            //加入到set中
            stringSet.add(randomStr);
            //加入到布隆過(guò)濾器
            bloomFilter.put(randomStr);
        }

        for (int i = 0; i < size; i++) {
            //生成隨機(jī)字符串
            String randomStr = RandomStringUtils.randomNumeric(10);
            //布隆過(guò)濾器校驗(yàn)存在
            if (bloomFilter.mightContain(randomStr)) {
                //set中存在
                if (stringSet.contains(randomStr)) {
                    //成功計(jì)數(shù)
                    success++;
                } else {
                    //失敗計(jì)數(shù)
                    fial++;
                }
                //布隆過(guò)濾器校驗(yàn)不存在
            } else {
                //    set中存在
                if (stringSet.contains(randomStr)) {
                    //失敗計(jì)數(shù)
                    fial++;
                } else {
                    //成功計(jì)數(shù)
                    success++;
                }
            }

        }
        System.out.println("判斷成功數(shù):"+success + "仍律,判斷失敗數(shù):" + fial + ",誤判率:" + fial / 100000);
    }

輸出

COPY判斷成功數(shù):97084.0水泉,判斷失敗數(shù):2916.0,誤判率:0.02916

可以通過(guò)增加誤判率的參數(shù)來(lái)調(diào)整誤判率

COPY/**
 * 構(gòu)建一個(gè)BloomFilter
 *  第一個(gè)參數(shù)Funnel類(lèi)型的參數(shù)
 *  第二個(gè)參數(shù) 期望處理的數(shù)據(jù)量
 *  第三個(gè)參數(shù) 誤判率 可不加茶行,默認(rèn) 0.03D
 */
private static final BloomFilter<CharSequence> bloomFilter = BloomFilter.create(Funnels.stringFunnel(), size,0.00001);

輸出

COPY判斷成功數(shù):100000.0,判斷失敗數(shù):0.0登钥,誤判率:0.0

本文由傳智教育博學(xué)谷狂野架構(gòu)師教研團(tuán)隊(duì)發(fā)布。

如果本文對(duì)您有幫助牧牢,歡迎關(guān)注點(diǎn)贊;如果您有任何建議也可留言評(píng)論私信塔鳍,您的支持是我堅(jiān)持創(chuàng)作的動(dòng)力伯铣。

轉(zhuǎn)載請(qǐng)注明出處!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轮纫,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子掌唾,更是在濱河造成了極大的恐慌,老刑警劉巖糯彬,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異似扔,居然都是意外死亡搓谆,警方通過(guò)查閱死者的電腦和手機(jī)炒辉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)泉手,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人啡氢,你說(shuō)我怎么就攤上這事√仁牵” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵搀崭,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我升敲,道長(zhǎng)轰传,這世上最難降的妖魔是什么驴党? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任港庄,我火速辦了婚禮,結(jié)果婚禮上恕曲,老公的妹妹穿的比我還像新娘鹏氧。我一直安慰自己,他們只是感情好佩谣,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開(kāi)白布把还。 她就那樣靜靜地躺著,像睡著了一般笨篷。 火紅的嫁衣襯著肌膚如雪瓣履。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,144評(píng)論 1 285
  • 那天袖迎,我揣著相機(jī)與錄音,去河邊找鬼辜贵。 笑死归形,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的暇榴。 我是一名探鬼主播蕉世,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼婆硬,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了向楼?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤湖蜕,失蹤者是張志新(化名)和其女友劉穎宋列,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡仇参,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年诈乒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喂饥。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肠鲫。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖导饲,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情硝岗,我是刑警寧澤袋毙,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布,位于F島的核電站听盖,受9級(jí)特大地震影響裂七,放射性物質(zhì)發(fā)生泄漏月幌。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一捉兴、第九天 我趴在偏房一處隱蔽的房頂上張望录语。 院中可真熱鬧倍啥,春花似錦澎埠、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至祥国,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間舌稀,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工觉至, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留睡腿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓沃暗,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親孽锥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容