淺談事務與一致性問題

在高并發場景下,分布式儲存和處理已經是常用手段。但分布式的結構勢必會帶來“不一致”的麻煩問題,而事務正是解決這一問題而引入的一種概念和方案。我們常把它當做并發操作的基本單位。

從MySQL事務說起(剛性事務)

提到事務,腦海里第一個反應當然是數據庫里的Transaction了。緊接著就是事務的四大特性:ACID (原子性,一致性,隔離性,持久性),所以我們先從這四大特性說起。

原子性

原子性是我們對事務最直觀的理解:事務就是一系列的操作,要么全部都執行,要么全部都不執行。

想要保證事務的原子性,就意味著需要在操作發生異常時,對該事務所有之前執行過的操作進行回滾

在MySQL中,這個回滾是通過回滾日志(Undo Log)實現的。簡單的說,回滾日志就是記錄了你所有操作的逆操作,在需要回滾時,就把這個事務的回滾日志里的操作全部執行一次。

比如你的事務里每一個create其實都對應了一個效果跟其相反的delete語句,他們被記錄在回滾日志里,當事務發生異常觸發ROLLBACK時,就按照日志邏輯地將回滾日志里的操作全部執行,從而達到“撤銷”操作的效果。

事務的狀態

宏觀上看事務是具有原子性的,是一個密不可分的最小單位。但是它是有幾種不同的狀態的:Active,Commited,Failed,它要么在執行中,要么執行成功,要么就失敗。

深入事務的內部,他就變為一系列操作的集合,不再具有原子性了,包括了很多的中間狀態,比如部分提交,參考如下的事務狀態圖:

[圖片上傳失敗...(image-30ed42-1517751460929)]

  • Active 事務的初始狀態,表示正在執行
  • Partially Commited 部分執行,或者說在最后一條語句執行后
  • Failed 發現操作異常,事務無法繼續執行后
  • Commited 成功執行整個事務
  • Aborted 事務被回滾,數據庫恢復到執行前狀態后

并行事務的原子性

正常情況下事務都是并行執行的,這就會出現很多復雜的新問題。

首先是事務依賴,舉一個直觀的例子來說明:

假設事務T1對數據A進行了讀寫,然后(T1還沒有執行完)在同時,T2讀取了數據A,然后成功提交了事務。這時候T1發生了異常,進行回滾。我們可以看到事務T2是依賴于T1所修改的數據的,如果要保證T1的原子性,那就需要同時對T2進行回滾,但是它已經被提交了,我們沒法再回滾了,這種問題被稱為“不可恢復安排”。

為了避免這種情況的出現,在出現事務的依賴時,必須遵循以下的原則:

如果事務T2依賴于事務T2,那么T1必須在T2提交之前完成提交操作。

接下來我們還不得不面對級聯回滾,也就是出現了多個事務都依賴于事務A的時候,如果A回滾,那么這些事務必須也一并回滾。這會導致大量的工作撤回,至于這件事情如何處理才合適,我們會在后面介紹。

持久性

這是理解起來相對簡單的一個特性,持久性就是指,事務一旦被提交,那么數據一定會被寫入到數據庫中并持久儲存起來。

另外,當事務被提交后就無法再回滾,如果想要撤銷一個已經提交的事務,那就只能執行一個效果與其相反的事務,這也是持久性的一種體現。關于這點,MySQL依然是通過日志實現的。

重做日志

重做日志由兩部分組成,一是內存中的重做日志緩沖區,另一個是磁盤上的重做日志文件。

這個緩沖區和日志的關系跟我們日常IO中使用的buffer是差不多的:當我們在事務中嘗試對數據進行更改時,首先將數據從磁盤讀入內存,更新內存緩存的數據,然后會生成一條重做日志(本次修改的逆操作)緩存,放在重做日志緩沖區中。當事務真正提交時,再將剛才緩沖區中的日志寫入重做日志中做持久化保存,最后再把內存中的數據變動同步到磁盤上。

上面這個流程用圖片描述如下:

[圖片上傳失敗...(image-626f5a-1517751460929)]

再具體一點,InnoDB中,重做日志都是以512B的塊形式儲存的,因為磁盤的扇取也是512B,所以重做日志的寫入就保證了原子性,即便機器斷電也不會出現日志僅僅寫入一半而留下臟數據的情況。

另外需要注意的一點是,在原子性一節中提到的回滾日志也是需要持久化儲存的,因此他們也會創建對應的重做日志,在發生錯誤后,數據庫重啟時,會從重做日志中找出未被更新到的數據庫磁盤上的日志,重新執行來滿足事務的持久性。

*事務日志

在數據庫系統中,事務的原子性和一致性是由事務日志實現的,在具體的實現上,使用的就是之前提到的回滾日志和重做日志,它們保證了兩點:

  • 發生錯誤或者需要回滾的事務能夠成功回滾(原子性)
  • 事務提交后,數據還沒來得及寫入磁盤就宕機時,重啟后能夠成功恢復數據(一致性)

在數據庫中這兩者往往一起工作,因此我們可以把他們看作一個整體。一條事務日志的內容可以抽象成下面這樣:

[圖片上傳失敗...(image-e7e2a2-1517751460929)]

一條記錄同時保存了對應數據修改前后的值,就可以非常方便的實現回滾和重做兩種功能。

隔離性

事務的隔離性會跟并發等相關概念聯系的非常密切,因為它主要就是為了保證并行事務處理能夠達到“互不干擾”的效果。

我們在一致性中討論過事務在并發情況下執行時,可能發生的一系列問題:雖然單個事務執行并沒有錯誤,但是它的執行可能會牽連到其他事務的執行,最終導致數據庫的整體一致性出現偏差。

談到這里我們就要看看事務之間的互相干擾都有哪些層級,也就是我們數據庫中非常重要的概念:

事務的隔離級別

事務的隔離級別,其實是數據庫對數據隔離性能的一種約束,選擇不同的隔離級別會影響數據一致性的程度,同時也會影響數據庫的操作性能。

標準SQL中定義了以下4種隔離級別:

  • 未提交讀

    使用查詢語句不會加鎖,可能會讀到未提交的行(臟讀)

  • 提交讀

    只對記錄加記錄鎖,而不會在記錄之間增加間隙鎖,所以允許新的記錄被插入到被鎖定記錄附近,在多次使用查詢語句時,可能會得到不同的結果(不可重復讀)

  • 可重復讀

    多次讀取同一范圍的數據會返回第一次查詢的快照,不會返回不同的數據行,但是可能發生幻讀

      幻讀 : 是指當事務不是獨立執行時發生的一種現象,例如第一個事務對一個表中的數據進行了修改,這種修改涉及到表中的全部數據行。 同時,第二個事務也修改這個表中的數據,這種修改是向表中插入一行新數據。那么,以后就會發生操作第一個事務的用戶發現表中還有沒有修改的數據行,就好象 發生了幻覺一樣。
    
  • 串行化

    隱式地將全部的查詢語句都加上了共享鎖。

從上到下一致性逐漸增強,但是數據庫的讀寫性能也逐漸變差

大部分數據庫中使用提交讀作為默認的隔離級別,這是出于性能和一致性的平衡,而MySQL中則默認采用可重復讀作為配置。

對于開發者而言,不必去了解每個隔離級別具體的實現,但要能夠根據不同的場景選擇最合適的隔離級別。

隔離的實現

隔離的實現說到底其實是并發控制,因此不同隔離級別的實現,其實就是采用了不同的并發控制機制。

1.鎖

這個自然是最簡單的,也是相當常用的并發控制機制了。

不過在一個事務中,自然是不可能把整個數據庫都加鎖的,而是只對要訪問的數據加鎖(具體的粒度有行、表等)。而這些資源鎖也是理所當然地分為共享鎖(讀鎖)和互斥鎖(寫鎖)兩種。

讀鎖可以保證操作并發執行而不受影響,寫鎖則保證了更新數據庫時不會受到其他事務的干擾。

2.時間戳

用時間戳實現隔離性,需要為記錄配置兩個字段

  • 讀時間戳:用于保存所有訪問該記錄的事務中的最大時間戳(最后讀取時間)
  • 寫時間戳:用于保存將記錄改到當前值的事務的時間戳(最后修改時間)

這樣的事務在并行執行時,用的是樂觀鎖,先任由事務對數據進行修改,在寫回去的時候在判斷記錄的時間戳有沒有修改,如果沒有被修改,就寫入,否則,就生成一個新的時間戳并再次嘗試更新數據。

PostgreSQL就使用了這種思想來控制事務。

3.多版本和快照隔離

通過維護多個版本的數據,數據庫便可以允許事務并發執行遇到互斥鎖時,轉而讀取舊版本的數據快照。這樣就能顯著地提升讀取的性能。我們簡稱這一手段為MVCC。

級聯回滾

之前在討論原子性問題時,討論過級聯回滾的問題,那是因為事務之間產生了依賴而導致的。因此我們將事務隔離之后,就不會再產生需要級聯回滾的場景了。

比如一個事務寫入了A數據,那么這時候是需要加共享鎖的,因此其它的事務無法讀取A,當事務A回滾時不用考慮對其它事務的影響,因為其它的事務并不可能讀到數據。

一致性

好了,這時候我們終于回歸到了本文所想討論的主題上來。“一致性”在數據庫領域有兩個意義,一個是ACID中的C,另一個是CAP的C,前者是我們經常討論的,也是普遍意義上的數據庫事務一致性,而后一個將是之后會展開討論的,有關分布式事務的一致性。

ACID

事務的一致性定義基本可以理解為是事務對數據完整性約束的遵循。這些約束可能包括主鍵約束、外鍵約束或是一些用戶自定義約束。事務執行的前后都是合法的數據狀態,不會違背任何的數據完整性,這就是“一致”的意思。

當然這個含義中也隱含著對開發者的要求,就是不能寫出錯誤的事務邏輯,比如銀行的轉賬不能只加錢不減錢,這是應用層面的一致性要求。

CAP

CAP定理是分布式系統理論的基礎。CAP告訴我們,對于一個分布式系統(或者由于網絡隔離等原因產生的分區系統),它無法同時保證一致性、可用性和分區容忍性,而是必須要舍棄其中的一個。

p.s. 對于分布式系統一般我們是不可能舍棄分區容忍性的(因為分區的情況是無法避免的),所以一般是根據業務,在一致性和可用性中二選一。

這里說的一致性,具體在數據庫上,就是分布式數據庫中,每一個節點對于同一個數據必須有相同的拷貝(每個庫里的同一個數據內容必須是一致的)。

分布式事務

現在我們來看一看,當數據分布式儲存后,操作所帶來的一些問題。

眾所周知,現在大型服務出于性能和容災的考慮,都會使用分布式的服務架構,這意味著一個服務會有多個數據庫,分開儲存不同的數據,這種情況下就很容易出現數據不一致的問題了,一個最簡單的例子:

A要B給轉100元。但是A和B的記錄被分在了不同的數據庫實例上,如果這時候執行的某個事務中途出現了bug,如果沒有一個好的處理方式,回滾將會是一件難以面對的事情。

所以我們可以看到,在分布式環境下,事務的設計方案變得更加復雜,也更加重要了,下面我們來談談分布式事務的一些常見實現方式:

兩階段提交(2PC)

原理

兩階段提交是一種提交協議,在這種協議下,事務的實現被拆分成了幾個不同的模塊,一般分為協調器和若干的事務執行者,如下圖:

[圖片上傳失敗...(image-5fa697-1517751460929)]

在分布式系統中,每個節點雖然可以知道自己操作是否成功,但是卻無法得知其他節點上操作是否成功,因此當一個事務跨越了多個節點的時候,就需要一個協調者,能夠掌控到所有節點的執行情況,進而保證事務的ACID特性。

現在我們來分析2PC協議條件下,轉賬問題是如何被解決的(我們假設A是你的支付寶余額,B是你的余額寶)。

  1. A發起請求到協調器,協調器開始工作

  2. 準備憑證

    • 協調器將prepare信息寫到本地日志,這就是回滾日志了。
    • 向所有的參與者發起prepare信息,當然對于不同的執行者,這個prepare信息是不同的,這取決于他們的數據實例上要發生什么樣的變動,比如這個例子中,A得到的prepare消息是通知支付寶余額數據庫扣除100元,而B得到的prepare消息是通知余額寶數據庫增加100元。
  3. 執行者收到prepare消息之后,執行本機的具體事務,但不會commit,如果成功則向協調者發送yes回執,否則發送no

  4. 協調者判斷收集到的所有回執,如果均為yes,就向所有的執行者發送commit消息,執行器收到該消息后就會正式執行提交。反之,如果收到任何一個no,就向所有的實行者發送abort消息,執行器收到后會放棄提交并回滾相應的改動。

協調器上保存的回滾日志,可以用于某個執行器失敗后恢復的工作的場景,此時執行器可能會再次向協調器發送回執來確定自己的執行狀態。

問題

2PC實現的思路倒是很簡單,不過這個思路中存在著幾個非常嚴重的問題,因此幾乎不被使用:

  1. 涉及多次節點間的通信,假設網絡延遲比較高,通信時長基本是不可忍受的
  2. 事務時間變長了,也意味著資源上鎖的時間變長了,性能大打折扣
  3. 如果參與者多了,協調器的工作效率會下降,而整個流程也變得復雜起來

其實分布式事務的種種實現方案基本都借鑒了2PC的思路,但很快人們就發現一個問題,在分布式的系統中,如果仍然采用事務模型來進行數據的修改,性能將受到不可避免的影響,這在高并發的場景下是不能接受的。

最終一致性(柔性事務)

剛才我們講了分布式事務在高并發場景下的敗北,其實根據CAP原則我們很容易明白,想要保證可用性的同時保證一致性是不可能的,于是現在大多數的分布式系統中都對一致性做出了妥協:

我們不追求整個操作過程中每一時刻的一致性(強一致性),轉而追求最終結果的一致性(最終一致性)。

也即是說,在整個事務執行的流程中,我們是可以接受的短暫的數據不一致的,只要最后的結果沒問題就行。

至此,我們對于事務的研究,從滿足ACID的剛性事務,拓展到BASE(基本可用,軟狀態,最終一致性)的柔性事務。

BASE

BASE原則是在分布式場景下,為了保證高可用性,而做出的一種“妥協性”思想。總的來說是允許局部的錯誤和故障,但要保證全局的穩定。事實上當前大多數的分布式系統,或者說大多數的大型系統里,都在運用這種思想了。

在展開柔性事務之前,我們先來補充一些基礎知識。

重試與冪等

在接下來講到的各種思路中,我們都無法避免一個問題,那就是接口調用或者說操作的失敗,分布式情況下系統的狀態往往不如單機條件下確定,所以可能經常需要重試,而不是一失敗就回滾。

因此我們必須盡可能的避免重試對系統穩定性和性能的影響,于是有了冪等這個概念:

冪等

  • 數學定義:f(x) = f(f(x))的性質
  • 編程定義:對同一個系統,使用同樣的條件,一次請求和重復的多次請求對系統資源的影響是一致的

然后我們需要探討一下保證冪等常用的思路,我們以微博點贊這個操作為實際例子來看一下(點贊是不能重復的):

  1. MVCC

    數據更新時需要比較持有數據的版本號,版本號不一致的話是無法操作成功的。
    每個版本只有一次執行成功的機會,一旦失敗了就要重新獲取版本號。

    這樣每次點贊操作都對應著一個不同的版本號,即便失敗重復嘗試,也不會出現點贊數錯誤增加或減少的情況。

  2. 去重

    這個主要依賴數據庫的索引唯一性(鍵),以點贊操作為例,可以對[user_id,weibo_id]這個組合做一張“點贊操作表”,如果成功點贊,就添加一條新記錄。

    如果出現了錯誤的重試,因為表的索引是唯一的,已經有了記錄自后就不會再次插入,自然也就不會出現錯誤的情況了。

異步確保

2PC的處理過程中一個很大的問題是,存在大量的同步等待,這便意味著操作之間的強耦合,一旦發生了失敗或是超時,造成的影響往往是災難性的。但是分布式情況下,超時和失敗又是很可能出現的情況,所以2PC手段沒法保證系統的可用性。

那么怎么優化呢?可以將操作解耦,使用消息隊列(或者某種可靠的通信機制)來連接不同的實例上的操作。這樣的通信機制使操作異步化,于是我們還需要一個能夠確保消息執行成功的確保機制,以上兩點的綜合就是現在最常用的柔性事務解決方案,我們暫且叫它“異步確保”(因為這種方案并非有一個統一的叫法),核心思路其實就是:用消息隊列保證最終一致性。

下面我們一步一步深入,了解這種方案的基本思想和流程。

問題

我們依然使用經典的轉賬問題來展開討論:A要向B轉100元,但是A和B的賬戶在不同的實例上存儲。

用異步確保的思想,操作的流程應該如此處理:

  1. A所在的實例扣除A賬戶100元
  2. 向B所在的實例發送操作消息,通知它給B的賬戶增加100元

這是一個很理想的情況,其實我們有很多的問題要處理。

首先是原子性,其實很容易發現,無論順序如何,如果1和2這兩個操作有任何一個失敗了,那另一個操作也必然變得沒有意義,所以必須保證1和2這兩個操作的整體原子性。

這里很多人會想,直接利用剛性事務的ACID特性,把1和2放在同一個事務里不就ok了。但這是不可能的,原因如下:

  • 網絡的2將軍問題:發送消息如果失敗了,發送方并沒有辦法知道,是接收方沒收到消息,還是接收方返回響應的時候出現了故障,其實已經收到了?
  • 在DB事務里插入網絡操作,如果出現延遲,會導致事務執行時間變長,對DB性能影響極大,嚴重的話可能block整個DB。

所以事情沒那么簡單,所以在我們得做不少額外的工作才能解決這個問題,下面是現在常用的解決思路:消息表。

先說生產方(A的實例)

  1. 生產方添加一張消息表,用于記錄發送的消息以及消息的回執等內容。

  2. 生產者在向消費者發送業務操作數據時,同時也要在消息表里增加一個消息記錄,這兩個都是對生產者DB的操作,我們要把它們放在同一個事務里來保證一致性。舉個例子,轉賬問題在A端上這個操作的sql就是這樣的(有點隨意,會意即可):

    begin transaction;
    update account set amount = ($amount - 100) where user = A;
    insert into message values('b','account','-100');
    end transaction;
    
  3. 對于這張消息表,我們需要一個維護者,它的職責是,不斷地把表中未發送的消息放入消息隊列,另外檢測消息的執行是否超時或失敗,如果遇到這種異常情況,就進行重試。注意:允許消息重復,但是不能丟失,順序也不會打亂。

再說消費方(B的實例)

  1. 消費方的接口(我們稱為下游接口),必須實現冪等。這是因為生產方可能會發來很多的重試消息,我們必須保證重試操作不會對系統產生不良影響。如果之前說的冪等手段不適用,可以簡單的為消費方準備一個判重表,利用判重表的Insert操作來實現冪等(如果這么做,請注意在業務中保證消費操作和Insert判重表操作的原子性)。

  2. 消費方完成操作后,利用消息隊列向生產方發送確認消息就ok。

可以看到這個實現方案對于業務的生產方來說,需要維護很多額外的操作,尤其是需要設計維護消息表,可能還要做后臺任務處理等,某種程度上這會增加業務端不必要的邏輯耦合,以及性能負擔。

簡要工作流程如下圖所示:

[圖片上傳失敗...(image-ec7c6-1517751460929)]

事務消息

正如上文所說,異步確保的思路中,大多數操作其實與業務無關,可以封裝到消息隊列中去。于是產生了“事務消息”這一概念,也就衍生了很多能夠很好的支持分布式事務消息相關操作的消息隊列或者中間件,如RocketMQ和Notify。

我們來看看事務消息是如何優化和整合異步確保的邏輯的。

首先,把消息發送分成了2個階段:準備和確認階段,于是生產方步驟變為如下3步:

  1. 發送prepared消息給MQ
  2. 執行本地事務
  3. 根據本地事務執行結果,確認或者取消prepared消息

這里有一個問題,就是如果1和2失敗了,還是很容易回滾和取消的,但是第三步失敗或者超時了,要怎么做呢?

以RocketMQ為例,MQ會定期地掃描所有的prepared消息,詢問發送方,到底是要確認發送這條消息,還是要取消這條消息?這點底層是通過讓生產方實現一個約定好的Check接口來實現的,有點像訂閱者模式。

我們可以看出來,異步回調中,掃描消息表,確認或重發消息這個步驟被消息隊列實現了,減少了業務方開發的難度。

對于消費方,事務消息支持重試的特性,也就是說不必生產者去主動發起重試消息,消息隊列可以自動幫你重試這些操作,可以說是非常解放生產力了。

如果有極端情況,比如消費端異常,無論怎么重試都失敗,是否要回滾呢?其實最好的辦法就是人工介入,人工去處理這種概率極低的case,比開發一個高復雜的自動回滾系統要可靠的多,也更簡單。

事務補償(TCC)

除了比較常用的異步確保,我們再介紹一種常見的實現柔性事務的思路,稱為事務補償。

總結之前的內容,我們不難發現,分布式事務的難點在于,一方執行事務成功之后,無法確定其他參與方對應的事務是否能夠成功(除非犧牲系統可用性)。

事務補償的想法和回滾日志有些類似。既然我們沒辦法同時保證所有的參與方事務執行都成功,不如就讓他們隨意執行,誰成功了就提交本地事務。但是每個參與方的每個操作,都要注冊(注意是注冊,不是自動生成)一個對應的補償操作,這個補償操作由人為定義,用于撤銷已執行事務帶來的影響。

當某一方的事務執行失敗時,所有已經成功提交了事務的參與方,需要按照順序(提交的倒序)去執行各自的補償事務,來將整個系統“回滾”到之前的狀態。

補償型思路的一個典型實現是TCC(Try-Confirm-Cancel)事務,其實說是事務,不如說是一種業務模式,因為Try,Confirm,Cancel這三個操作都必須由業務方實現。

  • Try:資源預留&鎖定。事務發起方將調用服務提供方的Try方法來鎖定業務所需要的所有資源。
  • Confirm:確認執行業務邏輯操作。這里使用的資源一定都是在Try中預留的資源,Try + Confirm 組合起來是一次完整的業務邏輯。
  • Cancel:取消執行業務邏輯。這里和普通的補償性事務不同,因為Try階段只是預留資源,并未真正執行操作,因此取消操作只需要釋放Try階段預留的資源,而不需要執行數據庫操作來補償。

其實TCC可以認為是應用層的2CP協議。網上關于TCC的相關邏輯說法很多,也比較混亂,這里找到一個比較通俗普遍的例子來解釋TCC的流程。當然實際應用中,根據業務的場景不同,TCC的實現也不同:它只是一種思路,而并非是一種規范。

例子仍然是轉賬問題,我們把范圍稍微擴大一點,現在我們有三個用戶A,B,C分別位于三個不同的數據庫實例上,現在A,B要分別向C轉賬40元(一共80元)。

  1. Try階段:嘗試執行。

    • 業務檢查(一致性):檢查A,B,C的賬戶狀態是否正常,以及A,B的賬戶余額是否都不低于40元。
    • 預留資源(準隔離性):賬戶A、B的余額均凍結40元。這樣保證其他并發事務不會把A、B的余額扣成負數。
  2. Confirm階段:確認執行。

    • 真正執行事務:執行實際的業務操作:A、B賬戶減少40元,C賬戶增加80元。(這一步還是需要消息傳遞機制)
  3. Cancel階段:取消執行。

    • 釋放A,B賬戶上被成功凍結的金額。

小結

分布式的結構下,事務的實現依然沒有一個放之四海而皆準的標準。但是可以看到一個統一的原則,那就是盡可能的讓服務變得更具有彈性,能夠靈活地應對多種情況。

總的來說,分布式事務更大的挑戰在于,相關業務邏輯的開發思路:可用性與一致性的平衡。

參考文章

本文是學習和整理自下列文章:

如有侵權,請聯系我刪除相關內容。如有錯誤,歡迎評論糾正。

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