分布式——分布式鎖

[TOC]

前言

突然覺得想要安穩的度過一生簡直可以稱之為臆想,想想歷史上的盛世,大都不過三四十年,如何能保證自己生活的大時間一定是在那三四十年之中(不過真的希望未來越來越好,大勢要好,個人也要好)。一份穩定的工作,愛自己的人,自己愛的人感覺都有點奢求。我知道這些在生活種都會慢慢被發現,也許一眨眼就要回首往昔了,發現這些東西的選擇并不是自由的,而是被時間推動的。好似隨著年齡的增長越發覺得自己的渺小,過去的東西已成定局且越來越多,而未來的選擇也愈少,可選擇的也愈少。
唉,言歸正傳,本來上次寫完那個CAP,感覺自己查了挺多資料的,寫的還不錯。現在回過頭一看,寫的是啥嘛!還是腦子太笨,就是屬于那種記得慢,理解的慢,忘得快的那種人。所以想著這次不說怎樣怎樣好吧,至少要比上次的好。對了,上一篇等我有時間要詳細補補,先立個flag再說。

冪等性

定義

HTTP/1.1中對冪等性的定義是:一次和多次請求某一個資源對于資源本身應該具有同樣的結果(網絡超時等問題除外)。用函數來表示的話就是f....f(f(x))=f(x)。

目的

1.在重要操作(如交易,轉賬)中防止請求重試帶來的災難性后果。

冪等的范圍

請求

1.讀請求——天生的冪等
2.寫請求——非冪等,需要控制。

數據庫層面

1.INSERT——非冪等,需要通過內容進行控制。
2.UPDATE——通過WHERE條件控制冪等性,盡量少用相對值進行操作。如UPDATE table1 SET column1 = column1 + 1 WHERE column2 = 2;
每次執行的結果都會發生變化,這種不是冪等的。而UPDATE table1 SET column1 = 1 WHERE column2 = 2;無論執行成功多少次狀態都是一致的,因此也是冪等操作。
3.DELETE——同樣通過WHERE條件控制,盡量少用相對值進行操作。如有
DELETE table1 WHERE column1 < now();應改為 DELETE table1 WHERE column1 < "2019-10-01";更好。

業務層面

1.在冗余部署多個服務的情況下,請求存在并發消費的情況,需要將請求由并行轉化為串行。

保證冪等性的策略

保證冪等性的本質是做好資源的串行化處理,這種串行化是多方位的。
查詢請求自不必說,新增請求需要做好重要控制屬性的防重處理,更新請求通過樂觀鎖也可以得到很好的控制。
分布式系統中則通過分布式鎖將請求由并行化轉為串行化的處理。

鎖的屬性

1.可重入/不可重入

可重入:同一線程外層函數獲取到鎖之后,內層函數中含有獲取該鎖的代碼不受影響,不必重新去申請鎖,可直接調用執行。
不可重入:同上相反,同一個線程內外都要獲取到鎖才執行。很容易造成死鎖。
synchronized和Reentrantlock都是可重入鎖

2.公平/非公平

公平:先來先得,請求鎖的線程組成隊列消費鎖。
非公平:來了先請求鎖,請求到了就執行,未獲取到就扔到等待獲取鎖的隊列尾部。
synchronized 非公平
Reentrantlock可選擇

3.讀/寫

讀鎖:可以多人讀,但是讀的時候不能寫,上讀鎖。
寫鎖:寫的時候不能讀,且只能一個人寫,上寫鎖。

4.共享/獨占

獨占鎖:每次只能一個線程持有鎖,ReentrantLock就是以獨占方式實現的互斥鎖。獨占是一種悲觀的加鎖策略。
共享鎖:該鎖可以被多個線程持有,如ReadWriteLock。放寬了加鎖策略,允許多個讀操作的線程同時訪問共享資源。
note : AQS(AbstractQueuedSynchronized)同時提供了互斥模式(exclusive)和共享模式(shared)兩種不同的同步邏輯。

5.可中斷/不可中斷

可中斷:鎖的操作過程可被中斷。ReentrantLock可中斷。
不可中斷:與上面相反。synchronized就是不可中斷鎖。

分布式鎖

用來保證某一特定共享資源在非同一系統中的多進程的環境下的互斥訪問。實質是通過對該資源的請求進行串行化,避免重復處理。但并不能解決請求的冪等性問題,仍需要在業務代碼中進行冪等性的控制。需要在系統外部創建一個共享存儲服務器,用來保存鎖信息。

設計目標

安全屬性

1.互斥。不管任何時候只能有一個客戶端持有同一個鎖。即鎖的存在是全局強一致性的。
2.無死鎖,具備鎖失效機制。一是提供鎖服務的系統本身高可用,系統穩健。多節點服務,任意節點宕機或發生網絡分區不影響鎖的獲取;二是客戶端對于鎖的自動續約和自動釋放。
3.對稱,對于任意鎖,其加鎖和解鎖必須是同一個客戶端。

效率屬性

1.容錯,高可用,高性能。服務本身高可用,系統穩健。多節點服務,任意節點宕機或發生網絡分區不影響鎖的獲取。
2.可重入,可有效減少死鎖的發生情況。
3.多選擇,可以選擇嘗試獲取鎖的時間。
4.客戶端調用簡單。要求鎖的代碼高度抽象,業務接入極簡。

設計思路

1.對共享資源的控制。可用某一資源的標識符作為鎖的key,這樣可以到資源進行唯一進程的控制。
2.鎖的唯一性控制。獲取鎖的進程單獨獲取一個唯一標識符(value),作為對鎖釋放時的條件,避免鎖被別的進程釋放。同時需要保證判斷和del操作的原子性,防止產生誤刪行為。通常使用lua腳本。
3.避免進程未執行完鎖被釋放,資源被并行訪問。進程及時續約,避免業務未完成鎖被釋放。
4.防止死鎖的產生。鎖的定期釋放,設定釋放時間。

邊界條件

1.提供鎖注冊的服務本身的穩定性與一致性。
2.鎖未能如期續租。如心跳續租不成功、服務啟動GC,GC期間服務掛起時間超出鎖的有效時間等。
3.業務假死,TTL仍在繼續。

設計要點

1.鎖的注冊。保證鎖的注冊是原子性的,即判斷鎖是否存在和注冊是串行化的。
2.鎖的續期。如何在對業務代碼入侵最小的情況下續期鎖的租約。CAS原子性。
3.鎖的釋放。鎖的使用者只釋放自己所持有的鎖。

不同的實現

redis實現

原理
1.單實例實現原理

redis的唯一線程串行處理,即本身是一個冪等線性的系統。但是這樣會產生單點故障問題。如果redis使用主從模式,因為redis的復制是異步 的所以會出現:

1.客戶端A在master節點獲取到鎖。
2.master節點在將信息寫入slave之前宕機。
3.slave被提升為master。
4.客戶端B獲取到與A同一資源的鎖。此時該資源進入多進程并行消費狀態。有悖互斥原則。

當然這種情況出現的概率是非常低的,如果資源對于這種情況并不敏感,通常也是可以接受的。
或者使用單實例的redis實現方式,這種情況下即程序需對單點故障的問題容忍度較高才行。

2.RedLock算法實現

該實現方式實質是在redis集群中實現一個一致性協議來實現key的唯一。不過要求所有的節點均為redis master節點,且完全相互獨立,不存在主從復制或者其他集群協調機制。

如果要獲取鎖,客戶端進行的操作需要有:

1.客戶端獲取當前時間(毫秒)
2.使用相同的key和不同的value去順序請求存在的N個master節點。(
該步驟需要客戶端設置一個比自動釋放時間小的多請求超時時間。比如說,當自動釋放時間為10秒時,設置超時時間為5~50毫秒。這是用來防止在宕掉的節點上消耗過多的時間。一個節點不可用,應立即進行下個節點的請求。
)
3.客戶端計算獲取鎖消耗的時間(step2消耗的時間——當前時間-step1獲取到的時間)。當且僅當在N/2+1個節點獲取到鎖,獲取鎖的消耗時間小于鎖的失效時間,才認為鎖的獲取是成功的。
4.若鎖被獲得,其有效時間可以被視為——初始的過期時間-獲取鎖的消耗時間。
5.如果客戶端獲取鎖失敗,無論什么原因(其實就兩種原因,一是成功節點小于N/2+1,二是超時),都將在所有節點進行鎖的釋放(即便是哪些根本沒有獲取到鎖的節點);

根據上面的算法描述來看,redis集群至少需要三個master節點。且實現上也較為繁瑣。加鎖開銷過大。對于redis運行問題要求也較高。總的來說實現成本過高。不過好在已經有人實現了——Redisson

單實例的實現

之前看到的資料一般都是用setNX這個命令來進行鎖的注冊,但是這個命令無法設置自動過期時間,只能通過設置value為時間戳來進行過期控制,其實不是很好。現在redis官方更推薦使用SET命令。

SET resource-name anystring NX EX max-lock-time

自2.6.12版本SET命令支持多種操作:
    EX seconds -- Set the specified expire time, in seconds.
    PX milliseconds -- Set the specified expire time, in milliseconds.
    NX -- Only set the key if it does not already exist.
    XX -- Only set the key if it already exist.

在釋放鎖的時候我們則可以用lua腳本語言用來保證原子性進行鎖的釋放

    if redis.call("get",KEYS[1]) == ARGV[1]
    then
        return redis.call("del",KEYS[1])
    else
        return 0
    end   

具體的代碼實現
這里用到的是lettuce

1.鎖的實現
 /** 
 *  如果鎖處于空閑狀態,當前線程獲取到鎖 
 *  如果鎖已經被其它線程持有,禁用當前線程,直到當前線程獲取到鎖 
 */
 @Override
 public void lock() {    
 //選定同步方式    
    while (true){        
        if (tryLock())return;        
        this.sleepByMillisecond(renewalTime >> 1);    
    }
 }
 /** 
 *  嘗試獲取鎖 
 *  如果鎖可用返回true 否則返回 false 
 * @return 
 */
 @Override
 public boolean tryLock() {    
    //使用ThreadLocal保存當前鎖對象 作為可重入鎖的控制
    if (threadLocal.get() != null) return true;
    String set = statefulRedisConnection.sync().set(lockKey, lockValue, new SetArgs().nx().ex(lockTime));   
    if (set != null && "OK".equals(set)){        
        System.out.println("線程id:"+Thread.currentThread().getId() + "加鎖成功!時間:"+ LocalTime.now());        
        isOpenExpirationRenewal = true; 
        threadLocal.set(this);
        this.scheduleExpirationRenewal();        
        return true;    
    }    
    return false;
 }
 /** 
 *  嘗試獲取鎖 
 *  在指定時間內可以獲取到  返回true 否則返回false 
 * @param time 
 * @param unit 
 * @return 
 * @throws InterruptedException 
 */
 @Override
 public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {   
     //獲取進入時間    
     LocalTime now = LocalTime.now();    
     while (now.isBefore(now.plus(time, (TemporalUnit) unit))){       
        if (tryLock())return true;    
     }    
     return false;
 }

2.鎖的續租

可能存在兩個問題,一是主線程掛掉,續租線程無法關閉;二是續租本身失敗。目前感覺對于第一種情況最好的方式是在加鎖代碼中設置異常捕捉,最后finally代碼塊中執行解鎖。第二種方式則是需要做好日志記錄分析代碼問題具體解決了。不過您要是有更好的實現方式,歡迎傳道解惑。


@Override
protected void scheduleExpirationRenewal() {    
    Thread thread = new Thread(new ExpirationRenewal());    
    thread.start();
}
private class ExpirationRenewal implements Runnable{    
    @Override    
    public void run() {        
        while (isOpenExpirationRenewal){            
            try {                
                Thread.sleep(renewalTime);           
            } catch (InterruptedException e) {               
                e.printStackTrace();           
            }            
            String expirFromLua = "if redis.call('get', KEYS[1]) == ARGV[1]"                    
                    + " then "                    
                    + "return redis.call('expire',KEYS[1],ARGV[2])"                    
                    + " else "                   
                    + "return 0"                   
                    + " end";            
            Object eval = statefulRedisConnection.sync().eval(expirFromLua, 
                ScriptOutputType.INTEGER, new String[]{lockKey}, lockValue, lockTime.toString());            
            System.out.println("續租獲取的結果值:" + eval + ((long)eval==1?" 續租成功":" 續租失敗"));        
        }   
    }
}
3.鎖的釋放
@Override
public void unlock() {    
//關閉續租    
isOpenExpirationRenewal = false;    
    //刪除鎖    
    String delFromLua = "if redis.call(\"get\", KEYS[1]) == ARGV[1]"    
        + " then "    
        + "return redis.call(\"del\",KEYS[1])"    
        + " else "    
        + "return 0"    
        + " end";    
    Long eval = statefulRedisConnection.sync().eval(delFromLua, ScriptOutputType.INTEGER, new String[]{lockKey}, lockValue);    
    if (eval == 1){       
        System.out.println("鎖釋放成功");    
    }else {       
        //最好做好日志記錄
        System.out.println("鎖 早已經釋放");    
    }
}

4.總結

上面只實現了可重入,需要考慮一下如何實現公平和非公平的切換,以及讀寫鎖。實現公平鎖的關鍵是維護一個跨進程的請求鎖隊列,只能用redis本身來實現。

2.RedLock算法實現
1.Redisson簡介

偷個懶,直接搬個官方的簡介

Based on high-performance async and lock-free Java Redis client and Netty framework.

Redisson功能還是挺強大的,推薦有時間可以去看看(有中文文檔哦)。

2.RedLock算法的實現

Redissond的紅鎖(RedissonRedLock)對象實現了RedLock介紹的加鎖算法。使用方式如下

RLock lock1 = redissonInstance1.getLock("lock1");
RLock lock2 = redissonInstance2.getLock("lock2");
RLock lock3 = redissonInstance3.getLock("lock3");

RedissonRedLock lock = new RedissonRedLock(lock1, lock2, lock3);
// 同時加鎖:lock1 lock2 lock3// 紅鎖在大部分節點上加鎖成功就算成功。
lock.lock();
...
lock.unlock();

需要創建多個redissonInstance1并創建多個RLock對象,使用這些來組成分布式鎖。其在redis種選用的數據結構是hash,用來實現公平鎖較方便。

使用起來還是比較麻煩的。

zookeeper實現

原理

zookeeper是一個分布協調服務,實現了一致性協議。分為Sever端和Client端。其中Server端是分布式應用。zab一致性協議,znode的臨時順序節點,watchs機制是zookeeper實現分布式鎖的關鍵。

1.zookeeper的數據模型

hierarchal name space(層級名稱空間),可以看作為一個分布式文件系統。具有樹形結構,每個節點以“/”分隔。大部分unicode字符都可以作為節點名稱。
zookeeper tree中的節點稱為znode。znode節點中維護著一個stat的數據結構,其中保存數據更改、acl(Access Control List——訪問控制列表,每個節點都存在,該列表限制誰可以做什么,相當于節點的權限列表,有:CREATE、READ、WRITE、DELETE、ADMIN)更改的版本號以及對應的時間戳。
znode的讀寫是原子性的。

znode的節點可以分為:

  • 持久節點 Persistent Nodes
    即使在創建該特定znode的客戶端斷開連接后,持久節點仍然存在。默認情況下,除非另有說明,否則所有znode都是持久的。
  • 臨時節點 Ephemeral Nodes
    臨時節點是伴隨著客戶端和服務端的會話創建的,當一個會話結束,其所對應的znode也會被刪除。因此臨時節點是無法創建子節點的。臨時節點在leader選舉中起著重要作用。
  • 順序節點 Sequence Nodes
    順序節點可以是持久的或臨時的。當一個新的znode被創建為一個順序節點時,ZooKeeper通過將10位的序列號附加到原始名稱來設置znode的路徑。該序列號是一個單調遞增的數列。例如,如果將具有路徑 /mynode 的znode創建為順序節點,則ZooKeeper會將路徑更改為 /mynode0000000001 ,并將下一個序列號設置為0000000002。當遞增超過2147483647(2^31 - 1)時,計數器將溢出(導致名稱“-2147483648”)。順序節點在鎖定和同步中起重要作用
  • Container Nodes 3.5.3版本添加
    該類型節點是為特殊請情況設置的,如leader,lock等。當容器內最后一個子節點被刪除后,容器將在未來的某個時候成為服務器要刪除的候選對象(個人感覺類似于redis中的定期刪除)。因此在創建Container node子節點時有可能會拋出KeeperException.NoNodeException,所以當子節點時需要判斷是否有該異常拋出,且在拋出后重新創建容器節點。
  • TTL Nodes 3.5.3版本添加
    該類節點只能是持久節點。在創建持久節點或持久順序節點時,可以選擇設置節點的TTL。若該節點的TTL時間未發生修改,且無子節點,那么該節點將在未來的某個時刻被服務器刪除。TTL節點必須通過系統屬性啟用,因為它們在默認情況下是禁用的。
2.Watchs

客戶端可以通過設置一個用來監聽znode節點信息的Watchs來獲取節點的更改信息。

實現步驟

通常實現鎖的關鍵是臨時順序節點和Watchs機制。即通過臨時順序節點創建一個獲取鎖的隊列,通過watchs機制監聽前一數據的消費情況,判斷自己是否被消費。

1.創建持久節點lock作為鎖的父節點。
2.請求鎖的客戶端A在lock節點下創建臨時順序節點lock000000N;
3.獲取lock下的節點列表,獲取編號比客戶端A次小的節點,若不存在,表示當前線程序號最小,獲取到鎖。
4.若編號比客戶端A次小的節點存在,設置Watchs監聽次小節點。次小節點刪除,判斷自己是不是最小的節點,是則獲取到鎖。

apache的開源庫Curator提供了相應的分布式鎖的實現。
不過有時間可以自己實現一個,不止使用上面的常規方式,也可使用Container節點或者TTL節點。

總結

zookeeper的實現相較于redis來講,不必考慮鎖的一致性問題,省去了一大麻煩。等我有時間看看Curator源碼去。

etcd實現

什么是etcd

一個分布式鍵值對(K-V)存儲服務。其實現了一致性算法Raft(糟了又忘了,這個一致性算法感覺寫的還是不夠詳細,等我有時間再好好研究一下),故高可用一定是2N+1個。主要用來存儲不常發生改變的數據,并提供監視查詢。可使用http/https/grpc調用服務端api,grpc方式自帶api,使用時不必關心續約問題。其中grpc是v3版本中添加可用。v3版本仍支持http/https的rest方式調用,但是需要注意的是v2版本同v3版本并不兼容,命令大多發生修改,數據不互通,如使用v2版本創建的數據,v3版本不可見。且v3版本對rest的調用方式支持并不是很好。

原理
數據模型

etcd是基本的Key-Value結構。

  • 邏輯視圖:
    存儲區的邏輯視圖是一個平面二進制(flat binary 說實話我對這個詞不是很明白,需要查查資料,我個人理解是一種數據的層級結構,類似樹的結構。有知道的望不吝賜教,抱拳)key space。Key空間維護多個Revision。每次進行事務,將在Key空間上創建一個新的Revision。Key從創建到銷毀就是Key的一個生命周期(generation)。每個Key可能擁有一個到多個生命周期(意味著保存著key多次創建修改銷毀的詳細記錄)。若Key不存在于當前修改中,那么當前Key的版本(version)從1開始。刪除Key則生成一個標志,該標志中包含當前被關閉key的代信息(通過重置version 為0)。每次修改key的version都會自增。在一個key的生命周期內version是單調遞增的。一次compaction發生,任何在compaction revision之前的key生命周期記錄(已結束的generation)會被刪除,并刪除在compaction revision之前的設置的值(最新的除外)。

  • 物理視圖
    etcd的KV數據以B+書的形式持久化到磁盤種,其更新數據時并不是對原有數據結構上的更新,而是生成一個更新后的數據結構。故其讀寫性能無法與redis之類相比。

etcd實現分布式鎖的基礎
  • Lease(A short-lived renewable contract that deletes keys associated with it on its expiry)機制:即租約機制(TTL, Time To Live)。續租問題。
  • Revision(A 64-bit cluster-wide counter that is incremented each time the keyspace is modified)機制:每個Key帶有一個Revision,每次進行事務將在key space創建一個新的Revision,該Revision是單調遞增的,初始值為零。通過Revision大小可以知道進行寫操作的順序。在實現分布式鎖時,可以依據Revision的大小進行鎖獲取的順序,實現公平鎖。
  • Prefix機制:即前綴機制,也稱目錄機制。key可以以目錄的形式創建,如key1 = "/mylock/00001", key2 = "/mylock/00002"。通過前綴/mylock范圍查詢,獲取KV列表,通過Revision的大小控制鎖的獲取順序,實現公平鎖。
  • Watch機制:監聽機制,Watch可以監聽單個key,也支持范圍監聽。
實現步驟

對比zookeeper和etcd我們會發現整體機制上是相同的。所以其實現方式大致也相同。
其步驟如下:

1.創建對應資源的分布式鎖的全局key的前綴,如/mylock,則第一個請求的客戶端創建的key則為/mylock/uuid001,第二個為/mylock/uuid002。
2.為該對應資源創建租約Lease為特定合適時間。
3.客戶端put自己的key,如/mylock/uuid001。這里的value不為空即可。獲取自己put后的revision號,并記錄。
4.獲取key前綴下的所有鍵值對信息,并根據revision號大小排序,小的在前。
5.對比put的revision值是否是最小的,是的話獲取鎖,并開啟續租線程。不是,則獲取前一個加鎖操作索引,并創建一個Watcher,用于監聽前一一個key(將處于阻塞狀態)。

etcd最新版本中有提供分布式鎖的實現。不必關心鎖的釋放和續約,當調用unlock時自動釋放。關鍵字lock,其實現原理基本同上述所講。
etcd的java客戶端有etcd4j,jetcd等。

總結

etcd的實現方式較為簡單,且其本身已經實現了分布式鎖,只用一個lock命令就可以輕松的獲取鎖。但是etcd在java的其他生態應用中似乎不是很常用,如果僅僅是用來做分布式鎖的獲取的話其實并不是很劃算。

結束語

終于寫完了,其實是趕工的,中間的zookeeper并沒有詳細的去寫出來,redis的那個也沒有實現公平鎖。現在這些東西提供的越來越全面了,有些不必自己去寫,但是原理一定要懂。其實這些都是比較-續約-釋放的具體實現。有機會還是要看看源碼,拓展一下自己的思路,希望接下來有時間能把Redisson、Curator、Jetcd的鎖實現源碼看看。唉,一直立flag一直爽,一直拖也一直爽,不知道哪個能更強一頭...
睡覺啦,睡覺啦...
分布式——分布式鎖


參考資料:
redis:https://redis.io/topics/distlock
zookeeper:http://zookeeper.apache.org/
etcd:https://etcd.io/

冪等性:https://www.cnblogs.com/javalyy/p/8882144.html
zookeeper分布式鎖的實現:https://www.cnblogs.com/javalyy/p/8882144.html
etcd鎖實現:http://blogspring.cn/view/120
jetcd:https://github.com/etcd-io/jetcd

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,533評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,055評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,365評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,561評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,346評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,889評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,978評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,118評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,637評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,558評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,739評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,246評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,980評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,362評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,619評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,347評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,702評論 2 370

推薦閱讀更多精彩內容