分布式鎖可以這么簡(jiǎn)單屉栓?(續(xù))

本文是上一篇文章 分布式鎖可以這么簡(jiǎn)單沼溜? 的續(xù)篇,主要是記錄分析在封裝過(guò)程中碰到的難點(diǎn)以及對(duì)應(yīng)的解決方案客税。

注:閱讀本文需要有一定的 面向切面編程 的基礎(chǔ)。

源碼

準(zhǔn)備工作

要把問(wèn)題暴露出來(lái)撕贞,需要先把部分代碼注釋一下更耻。

1. 注釋 TransactionEnhancerAspect 的注入代碼

DistributionLockAutoConfiguration 中的以下幾行代碼注釋掉:

DistributionLockAutoConfiguration

2. DistributedLockAspect 不實(shí)現(xiàn)接口 Ordered

DistributedLockAspect

DistributedLockAspect

分布式鎖注解不生效?

先來(lái)看一個(gè)測(cè)試用例:

public class TestItemService { 
    // 省略其他

    @DistributedLock(
            lockName = "#{#testItem.id}",
            lockNamePre = "item",
            checkBefore = "#{#root.target.checkbefore(#testItem)}"
    )
    @Transactional(rollbackFor = Throwable.class)
    public Integer testInoperative(TestItem testItem) {
        sleep(100L);

        TestItem item = this.getById(testItem.getId());
        Integer stock = item.getStock();
        System.out.println(String.format("current thread: %s, current stock: %d", getCurrentThreadName(), item.getStock()));

        if (stock > 0) {
            stock = stock - 1;
            item.setStock(stock);
            this.saveOrUpdate(item);
        } else {
            stock = -1;
        }

        return stock;
    }

    public void checkbefore(TestItem testItem) {
        TestItem item = this.getById(testItem.getId());
        System.out.println(String.format("current thread: %s, check before. stock: %d", getCurrentThreadName(), item.getStock()));
    }
}
public class DistributedLockTests {
    @Test
    public void testInoperative() {
        Consumer<TestItem> consumer = testItem -> {

            Integer stock = testItemService.testInoperative(testItem);

            if (stock >= 0) {
                System.out.println(Thread.currentThread().getName() + ": rest stock = " + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + ": sold out.");
            }
        };

        commonTest(consumer);
    }
}

啟動(dòng)測(cè)試用例麻掸,結(jié)果類似如下:


testInoperative

可以看到酥夭,所有線程都正常扣庫(kù)存了脊奋,但全部跑完后熬北,數(shù)據(jù)庫(kù)的庫(kù)存還剩下 7,這還得了诚隙?

其中 check before. stock: 8 是在方法 checkbefore 中打印的讶隐,該方法在獲得鎖之前就會(huì)被執(zhí)行。

那為什么會(huì)出現(xiàn)這種情況呢久又?明明已經(jīng)加了鎖了呀巫延。

在回答這個(gè)問(wèn)題之前,需要知道一個(gè)數(shù)據(jù)庫(kù)的知識(shí)點(diǎn)地消,即數(shù)據(jù)庫(kù)事務(wù)的隔離級(jí)別炉峰。一共有4種隔離級(jí)別,分別為:讀未提交(Read uncommitted)脉执、讀已提交(Read committed)疼阔、可重復(fù)讀(Repeatable read)、串行讀(Serializable),由于這不是本文的重點(diǎn)婆廊,這里就不展開(kāi)說(shuō)了迅细。

因?yàn)楸疚氖褂玫氖?Mysql,所以就拿它來(lái)說(shuō)明淘邻。Mysql 的默認(rèn)隔離級(jí)別為 可重復(fù)讀茵典,該隔離級(jí)別有什么特點(diǎn)呢?先看下面的圖:(比較熟悉可以跳過(guò)這部分)

Repeatable read

在數(shù)據(jù)庫(kù)的可視化界面新開(kāi)2個(gè)查詢窗口宾舅,分別對(duì)應(yīng)上圖的 Session A统阿,Session B,然后按步驟贴浙,一次在2個(gè)窗口中執(zhí)行砂吞。根據(jù)執(zhí)行的結(jié)果,可以看出崎溃,事務(wù)B 開(kāi)啟后蜻直,無(wú)論 事務(wù)A 如何操作,id = 1 的庫(kù)存一直都為 10袁串,直到 事務(wù)B 結(jié)束后概而,才能看到 事務(wù)A 提交的數(shù)據(jù)庫(kù)操作結(jié)果。

明白了這點(diǎn)之后囱修,我們?cè)賮?lái)看另一張圖:


Transactional and DistributedLock Aspects

上面的圖赎瑰,是將上面測(cè)試用例的重點(diǎn)流程可視化后的結(jié)果(只畫(huà)出前2個(gè)線程)。當(dāng)有多個(gè)切面時(shí)破镰,遵循的是先進(jìn)后出的原則餐曼,比如上圖中有2個(gè)切面,一前一后分別是 Transactional Aspect鲜漩、DistributedLock Aspect源譬,所以進(jìn)入對(duì)應(yīng)的方法時(shí),會(huì)先執(zhí)行 Transactional Aspect 的相關(guān)邏輯孕似,再執(zhí)行 DistributedLock Aspect 的相關(guān)邏輯踩娘。

上圖中,比較有爭(zhēng)議的地方是 步驟12喉祭,為什么獲取到的庫(kù)存是 8养渴?線程1 明明已經(jīng)把庫(kù)存更新為 7,且事務(wù)已經(jīng)提交泛烙。

其實(shí)理卑,可以看到,線程2 在等待鎖的時(shí)候蔽氨,已經(jīng)開(kāi)啟了一個(gè)新事務(wù)傻工,再根據(jù) 可重復(fù)讀 隔離級(jí)別的特點(diǎn),可以輕松得出:無(wú)論線程1 怎么操作數(shù)據(jù)庫(kù)并提交孵滞,無(wú)論增刪改中捆,只要 線程2 的事務(wù)沒(méi)有提交,對(duì)于 線程2 來(lái)說(shuō)都是不可見(jiàn)的坊饶,所以 線程2 獲取到的庫(kù)存是 8泄伪。

這樣一分析下來(lái),很明顯匿级,獲得鎖的邏輯不能放在 事務(wù)開(kāi)啟 之后蟋滴,即 DistributedLock Aspect 要在 Transactional Aspect 之前,這樣一來(lái)痘绎,都是拿到分布式鎖后津函,才去開(kāi)啟事務(wù),這樣才不會(huì)出現(xiàn)上面測(cè)試用例的情況孤页。

問(wèn)題找到了尔苦,那要怎么保證這兩個(gè)切面的先后順序呢?Spring AOP 已經(jīng)考慮到這點(diǎn)行施,可以讓切面類通過(guò)實(shí)現(xiàn)接口 org.springframework.core.Ordered允坚,該接口需要實(shí)現(xiàn)一個(gè)方法 int getOrder()Spring AOP 會(huì)根據(jù)返回的數(shù)值去對(duì)切面進(jìn)行排序蛾号,數(shù)值越小稠项,優(yōu)先級(jí)越高,即越先執(zhí)行鲜结。

所以我們只需要在構(gòu)造 DistributedLock Aspect 的時(shí)候展运,動(dòng)態(tài)獲取 Transactional Aspectorder 數(shù)值,然后返回一個(gè)比它小的數(shù)值即可精刷。問(wèn)題來(lái)了拗胜,如何動(dòng)態(tài)獲取 Transactional Aspectorder 數(shù)值?要解決這個(gè)問(wèn)題贬养,需要對(duì) Spring 事務(wù) 的源碼有一定了解挤土,文末有分析源碼的相關(guān)文章,有興趣的可以去研究研究误算。

Spring 事務(wù) 的開(kāi)啟仰美,涉及到一個(gè)注解 @EnableTransactionManagement,該注解有一個(gè)屬性 order儿礼,該 order 就是我們想要的咖杂,下面來(lái)看看如何在 Spring 容器 啟動(dòng)階段,動(dòng)態(tài)獲取 order 數(shù)值蚊夫。

public class DistributedLockAspect implements ApplicationContextAware, Ordered {
    // ... 省略其他

    public int getOrder() {
        try {
            int minOrder = Integer.MAX_VALUE;

            Map<String, Object> beansWithAnnotation = applicationContext.getBeansWithAnnotation(EnableTransactionManagement.class);
            for (Map.Entry<String, Object> entry : beansWithAnnotation.entrySet()) {
                Object value = entry.getValue();

                Class<?> proxyTargetClass = ClassUtils.getUserClass(value);
                EnableTransactionManagement annotation = proxyTargetClass.getAnnotation(EnableTransactionManagement.class);
                int order = annotation.order();
                minOrder = Math.min(order, minOrder);
            }

            return Math.min(0, minOrder) - 1;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

簡(jiǎn)單解釋一下上面的代碼诉字,首先,applicationContext.getBeansWithAnnotation 可以獲取 Spring 容器中,所有被給定注解標(biāo)記過(guò)的 Bean壤圃;將這些 Bean 找出來(lái)后陵霉,接著遍歷所有 Bean,因?yàn)?Spring 容器中的 Bean 大多都是代理對(duì)象伍绳,所以這里使用 ClassUtils.getUserClass 來(lái)獲取所代理的類對(duì)象踊挠,最后即可找出標(biāo)記 EnableTransactionManagement 注解的時(shí)候,設(shè)置的 order 數(shù)值冲杀,一般都為默認(rèn)值效床。最后,我們只要保證比它的 order 數(shù)值小并返回就行权谁。

將上面的代碼補(bǔ)充到 DistributedLockAspect 后剩檀,再重新啟動(dòng)上面的測(cè)試用例,可以看到類似如下:

testInoperative 2

終于正常了旺芽。

鎖被動(dòng)釋放后沪猴,數(shù)據(jù)無(wú)法正常回滾

先來(lái)看一個(gè)新的測(cè)試用例:

public class TestItemService {
    @DistributedLock(
            lockName = "#{#testItem.id}",
            lockNamePre = "item",
            checkBefore = "#{#root.target.checkbefore(#testItem)}"
    )
    @Transactional(rollbackFor = Throwable.class)
    public Integer testRollbackWhenLostTheLock(TestItem testItem) {

        TestItem item = this.getById(testItem.getId());
        Integer stock = item.getStock();

        int ci = i.getAndDecrement();
        if (ci == 10) {
            System.out.println(String.format("current thread: %s, got the lock first, now sleep a few seconds.", getCurrentThreadName()));
            sleep(6000L);
        }

        System.out.println(String.format("current thread: %s, current stock: %d", getCurrentThreadName(), item.getStock()));
        if (stock > 0) {
            stock = stock - 1;
            item.setStock(stock);
            this.saveOrUpdate(item);
        } else {
            stock = -1;
        }

        return stock;
    }
}
public class DistributedLockTests {
    @Test
    public void testRollbackWhenLostTheLock() {
        Consumer<TestItem> consumer = testItem -> {

            Integer stock = testItemService.testRollbackWhenLostTheLock(testItem);

            if (stock >= 0) {
                System.out.println(Thread.currentThread().getName() + ": rest stock = " + stock);
            } else {
                System.out.println(Thread.currentThread().getName() + ": sold out.");
            }
        };

        commonTest(consumer);
    }
}

啟動(dòng)測(cè)試用例甥绿,類似如下:


testRollbackWhenLostTheLock

不出意外的話字币,這時(shí)數(shù)據(jù)庫(kù)中的庫(kù)存為 7,很明顯共缕,這又有問(wèn)題洗出。

上面的測(cè)試用例是什么邏輯呢?第1個(gè)獲得鎖的線程(線程1)图谷,獲得數(shù)據(jù)后翩活,休眠了 6s,而鎖的有效時(shí)間為 5s便贵,也就是說(shuō)休眠恢復(fù)后菠镇,鎖已經(jīng)被動(dòng)釋放,且被其他線程獲得承璃,這個(gè)時(shí)候 線程1 還以為鎖還是它所持有利耍,然后繼續(xù)剩余的流程,最后把庫(kù)存更新為 7盔粹,但其實(shí)這個(gè)時(shí)候的庫(kù)存已經(jīng)為 0 了隘梨,所以這個(gè)時(shí)候如果把數(shù)據(jù)更新到數(shù)據(jù)庫(kù),那大概率是錯(cuò)誤的舷嗡。

針對(duì)這種情況轴猎,應(yīng)該怎么辦呢,既然提交的數(shù)據(jù)已經(jīng)不可信了进萄,那肯定是不能讓它正常 commit捻脖,必須讓它回滾掉锐峭。

我們都知道,加上注解 @Transactional(rollbackFor = Throwable.class) 后可婶,如果方法執(zhí)行過(guò)程中拋異常的話沿癞,會(huì)回滾數(shù)據(jù)。因此扰肌,我們可以借助這一特性抛寝,進(jìn)行一系列改造,讓其在 commit 之前曙旭,先檢查一下鎖是否由當(dāng)前線程持有,如果是晶府,則可以放心 commit桂躏,如果不是,那就大膽回滾吧川陆。

聰明的你剂习,這時(shí)候可能注意到了控制臺(tái)打印的一行文字:鎖釋放失敗, 當(dāng)前線程不是鎖的持有者,如果把打印邏輯換成 拋異常较沪,這樣能否達(dá)到我們想要的效果呢鳞绕?可以嘗試一下,按照下圖進(jìn)行改造尸曼,改造的類名為 RedisDistributedLockTemplate

method tryLock

再次運(yùn)行測(cè)試用例们何,可以看到類似如下:


testRollbackWhenLostTheLock 2

異常倒是能正常拋出,但看了下數(shù)據(jù)庫(kù)控轿,好像沒(méi)達(dá)到預(yù)想中的效果冤竹,庫(kù)存還是為 7,為啥呢茬射?

在解決前一個(gè)問(wèn)題的時(shí)候鹦蠕,我們已經(jīng)保證了一點(diǎn)—— 分布式鎖定切面的優(yōu)先級(jí)高于 事務(wù)切面,加了這個(gè)改造后在抛,會(huì)對(duì)數(shù)據(jù)的回滾有什么影響呢钟病?先來(lái)看一張圖:


Transactional and DistributedLock Aspects

這是改造后的流程圖,可以看到 釋放鎖 的時(shí)候刚梭,事務(wù)其實(shí)已經(jīng) commit 了肠阱,那這個(gè)時(shí)候去拋異常,顯然是沒(méi)辦法觸發(fā)回滾的望浩,那怎么辦呢辖所?我們先來(lái)簡(jiǎn)單分析一下:
這兩個(gè)切面的順序肯定是不能調(diào)整的,又必須得在事務(wù) commit 前判斷鎖是否由當(dāng)前線程持有磨德,然后兩個(gè)切面的邏輯也是非常獨(dú)立的缘回,既然這樣吆视,直接改 Transactional Aspect 的相關(guān)源碼肯定是行不通的,那就只能找找 Transactional Aspect 有沒(méi)有什么可以擴(kuò)展的地方了酥宴。

在解決這個(gè)問(wèn)題之前啦吧,又得先了解幾個(gè)知識(shí)點(diǎn),一個(gè)是 當(dāng)有多個(gè)切面的時(shí)候拙寡,每個(gè)切面的 Advice 的詳細(xì)執(zhí)行順序授滓,這一點(diǎn),下面的圖就能很明顯看出來(lái)了(其中藍(lán)色的 Method 是目標(biāo)業(yè)務(wù)方法):

多切面 Advices 流轉(zhuǎn)

如果把 DistributedLock AspectTransactional Aspect 套進(jìn)去的話肆糕,切面A 就是 DistributedLock Aspect般堆,切面B 就是 Transactional Aspect

另一個(gè)是 事務(wù)是在什么時(shí)間點(diǎn)開(kāi)啟和結(jié)束的诚啃,這里先直接說(shuō)答案淮摔,是在 @Arround 階段就開(kāi)啟的,而且也是在該階段 commit 的始赎;

最后一個(gè)是 Spring 事務(wù) 提供的一個(gè)接口 TransactionSynchronization和橙,這個(gè)接口有一個(gè)方法 void beforeCommit(boolean readOnly),該方法會(huì)在事務(wù)提交前被調(diào)用造垛,當(dāng)然魔招,前提是 在事務(wù)開(kāi)啟后需要將該接口的實(shí)現(xiàn)類注冊(cè)到 TransactionSynchronizationManager 中,注冊(cè)成功后會(huì)被存儲(chǔ)在一個(gè) ThreadLocal<Set<TransactionSynchronization>> 中五辽,且僅限當(dāng)前事務(wù)有效办斑。詳細(xì)的可研究推薦閱讀給出的參考鏈接:Spring 事務(wù)源碼分析

將這3個(gè)知識(shí)點(diǎn)關(guān)聯(lián)起來(lái)奔脐,基本就可以得出一個(gè)解決方案:只有在下圖圈起來(lái)的 @Before AdviceTransactionSynchronization 的實(shí)現(xiàn)類注冊(cè)到 TransactionSynchronizationManager 中俄周。具體原因可參考附錄:為什么增強(qiáng)邏輯只能在 Before Advice

多切面 Advices 流轉(zhuǎn)

接下來(lái)看具體實(shí)現(xiàn)方法髓迎。

TransactionEnhancerAspect

定義一個(gè)切面類

@Aspect
public class TransactionEnhancerAspect {

    private final DistributedLockTemplate distributedLockTemplate;

    public TransactionEnhancerAspect(DistributedLockTemplate distributedLockTemplate) {
        this.distributedLockTemplate = distributedLockTemplate;
    }

    @Before(value = "@annotation(transactional)")
    public void doBefore(JoinPoint jp, Transactional transactional) {

        // 是否開(kāi)啟了 可寫(xiě)的事務(wù)
        if (!isWithinWritableTransaction()) {
            return;
        }

        DistributedLock annotation = PointCutUtils.getAnnotation(jp, DistributedLock.class);
        if (annotation == null) {
            return;
        }
        
        // 該 context 在創(chuàng)建鎖的時(shí)候初始化的峦朗,并把 lock 對(duì)象設(shè)置進(jìn)去
        DistributedLockContext context = DistributedLockContextHolder.getContext();
        Object lock = context.getLock();

        UnlockFailureProcessor unlockFailureProcessor = new UnlockFailureProcessor(distributedLockTemplate, lock);
        TransactionSynchronizationManager.registerSynchronization(unlockFailureProcessor);

    }

    /**
     * 是否在一個(gè)可寫(xiě)的事務(wù)中
     *
     * @return
     */
    private boolean isWithinWritableTransaction() {
        boolean isTransactionActive = TransactionSynchronizationManager.isActualTransactionActive();
        boolean isTransactionReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();

        return isTransactionActive && !isTransactionReadOnly;
    }

}

UnlockFailureProcessor

定義一個(gè) TransactionSynchronization 實(shí)現(xiàn)類

public class UnlockFailureProcessor implements TransactionSynchronization {

    private final Object lock;

    private final DistributedLockTemplate distributedLockTemplate;

    public UnlockFailureProcessor(DistributedLockTemplate distributedLockTemplate, Object lock) {
        this.distributedLockTemplate = distributedLockTemplate;
        this.lock = lock;
    }

    @Override
    public void beforeCommit(boolean readOnly) {
        boolean heldByCurrentThread = distributedLockTemplate.isHeldByCurrentThread(lock);

        if (!heldByCurrentThread) {
            ResponseEnum.LOCK_NO_MORE_HOLD.assertFailWithMsg("釋放鎖時(shí), 當(dāng)前線程不是鎖的持有者");
        }
    }

}

可以看到,在 commit 之前排龄,判斷是否當(dāng)前線程持有鎖波势,如果不是,拋異常橄维,讓數(shù)據(jù)回滾尺铣。

其他

上面介紹的類中,涉及到的 DistributedLockContextHolder争舞、DistributedLockContext 因?yàn)楸容^簡(jiǎn)單也不影響整體代碼邏輯凛忿,就不貼源碼了。

最后竞川,只需要在 DistributionLockAutoConfiguration 配置類中店溢,加入 TransactionEnhancerAspect 的注入 Spring 容器 邏輯叁熔。

測(cè)試驗(yàn)證

經(jīng)過(guò)這一番改造,基本即可以解決問(wèn)題床牧,下面再運(yùn)行一下測(cè)試用例荣回。(最好把之前在 RedisDistributedLockTemplate 中添加的拋異常去掉,不然可能會(huì)看到拋2個(gè)異常)

啟動(dòng)測(cè)試用例戈咳,可以看到類似如下:


testRollbackWhenLostTheLock 3

可以看到心软,第一個(gè)拿到鎖的線程,最后拋異常了著蛙,符合預(yù)期删铃,再看看數(shù)據(jù)庫(kù)的庫(kù)存,如果不出意外册踩,看到的是 0泳姐,那就代表成功了。

結(jié)語(yǔ)

至此暂吉,已經(jīng)把我認(rèn)為比較有挑戰(zhàn)的難點(diǎn)列出缎患,并拆解、分析嫉父、解決了擂红,至于其他树碱,各位看官如果覺(jué)得哪里有疑惑的成榜,可以評(píng)論留言,一起討論、學(xué)習(xí)。

謝謝~~

推薦閱讀

統(tǒng)一異常處理介紹及實(shí)戰(zhàn)
分布式鎖可以這么簡(jiǎn)單?(上篇)

Spring 事務(wù)源碼分析

附錄

為什么增強(qiáng)邏輯只能在 Before Advice

源碼 TransactionAspectSupport#invokeWithinTransaction

invokeWithinTransaction

上圖中瓷耙,第一個(gè)圈中的地方,是創(chuàng)建一個(gè)新事務(wù)(有必要的話);
第二個(gè)圈中的地方谁尸,注釋的內(nèi)容簡(jiǎn)單翻譯一下,這里是一個(gè) Around Advice,當(dāng)前的位置只是攔截器鏈中的某一個(gè),需要繼續(xù)觸發(fā)下一個(gè)攔截器胸囱;
第三個(gè)圈中的地方裳扯,是目標(biāo)方法返回后, commit 事務(wù)。

所以,Transactional Aspectaround advice 階段完成后,事務(wù)的相關(guān)邏輯基本都 完成了,之后的 After Advice翘簇、AfterReturning Advice 這2個(gè)階段都已不在事務(wù)中了夫否,所以能注冊(cè) TransactionSynchronization 的階段就只有 Before Advice 了。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末买乃,一起剝皮案震驚了整個(gè)濱河市姻氨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌剪验,老刑警劉巖肴焊,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件前联,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡娶眷,警方通過(guò)查閱死者的電腦和手機(jī)似嗤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)届宠,“玉大人烁落,你說(shuō)我怎么就攤上這事∠浚” “怎么了顽馋?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)幌羞。 經(jīng)常有香客問(wèn)我寸谜,道長(zhǎng),這世上最難降的妖魔是什么属桦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任熊痴,我火速辦了婚禮,結(jié)果婚禮上聂宾,老公的妹妹穿的比我還像新娘果善。我一直安慰自己,他們只是感情好系谐,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布巾陕。 她就那樣靜靜地躺著,像睡著了一般纪他。 火紅的嫁衣襯著肌膚如雪鄙煤。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,954評(píng)論 1 283
  • 那天茶袒,我揣著相機(jī)與錄音梯刚,去河邊找鬼。 笑死薪寓,一個(gè)胖子當(dāng)著我的面吹牛亡资,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播向叉,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼锥腻,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了植康?” 一聲冷哼從身側(cè)響起旷太,我...
    開(kāi)封第一講書(shū)人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后供璧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體存崖,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年睡毒,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了来惧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡演顾,死狀恐怖供搀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情钠至,我是刑警寧澤葛虐,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站棉钧,受9級(jí)特大地震影響屿脐,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜宪卿,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一的诵、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧佑钾,春花似錦西疤、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至兽掰,卻和暖如春管跺,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背禾进。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留廉涕,地道東北人泻云。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像狐蜕,于是被迫代替她去往敵國(guó)和親宠纯。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

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