團隊開發框架實戰—Aggregate

本文將介紹聚合以及與其高度相關的并發主題。
我在之前已經說過,初學者第一步需要將業務邏輯盡量放到實體或值對象中,給實體“充血”,這樣可以讓業務邏輯高度內聚,并為你提供業務邏輯的唯一訪問點。而聚合則是第二步,它將多個相關業務概念包裝到單一的概念中,從而大幅簡化系統設計,由于受傳統數據建模思維影響,我在聚合方面吃過大虧,花了將近一年才真正用起來,為了你少走彎路,我會把一些要點總結出來供你參考。

什么是聚合?

聚合包裝一組高度相關的對象,作為一個數據修改的單元。

聚合最外層的對象稱為聚合根,它是一個實體。聚合根劃分出一個清晰的邊界,聚合根外部的對象,不能直接訪問聚合根內部對象,如果需要訪問內部對象,必須首先訪問聚合根,再導航到聚合的內部對象。
  聚合代表很強的包含關系,聚合內部的對象脫離了聚合,應該是毫無意義的,或不是你真正關注的,它是聚合的一個組成部分,這與UML中的組成聚合概念相近。

聚合的作用

簡化系統設計

在剛開始接觸DDD時,我們受到傳統數據建模思維影響,根據范式要求設計出多張表,會很自然的每張表映射成一個實體,每個實體都是聚合。我最初就是這樣使用的,干了一年左右才醒悟過來,雖然這樣也可以實現功能。
  那么這有什么問題?
  當把每個表映射成獨立的聚合時,我們在思考問題的時候,會把每個表作為獨立對等的概念進行思考,從而使你的大腦分不清主次,淹沒在錯綜復雜的表關系中
  現在如果系統有100張數據庫表,每張表以任意方式關聯,映射成100個聚合。你在進行思考時,以相同方式對待這100個聚合,很快就會頭暈目眩。有經驗的開發者知道通過切割模塊可以降低復雜度,但各個模塊之間錯綜復雜的關系依然存在。
  如果通過聚合的方式進行思考,情況則大不相同。把高度相關的概念封裝到一個聚合中,并且將聚合中的對象盡量使用值對象建模,不僅可以減少表數量,在概念上也更加簡單和清晰。現在假定還是100張表,每5張表映射到一個聚合中,那么具有20個聚合。我們在思考問題時,整個聚合成為一個獨立思考的單元,聚合內部的附屬對象已經成為二等公民,你并不需要隨時想到它們。由于聚合根外部對象只能直接訪問聚合根,所以復雜的關系被封裝到聚合內部。我們現在只需要考慮聚合根之間的關系,整個系統設計會大幅簡化,系統的耦合度得到控制。
  另一方面,聚合對倉儲產生影響。由于倉儲代表的是聚合的集合,換句話說,每個聚合應該擁有一個倉儲。如果每個表都映射為聚合,那么會導致大量的倉儲,哪怕采用了依賴注入框架,整個系統的依賴復雜度還是非常高。

強制實施相關對象上的一致性規則

如果一組相關對象需要滿足某些業務規則,并且這幾個對象是離散的獨立對象,那么實施一致性規則就非常困難。你可能需要在每個用到的地方進行各種判斷,從而導致復雜度和冗余。
  我幾乎在每篇文章都給你反復強調充血模型的重要性,是想激起你的注意。對于上面的問題,實際上是需要一個統一的驗證點。能夠給你提供唯一的業務邏輯訪問點的位置就在實體中,所以把這一組相關對象組合為一個聚合,并在聚合上強制實施驗證規則可以很好的解決問題。

對并發更新提供保護

從聚合的定義可以看出,聚合不僅是一組對象的抽象概念,而且還要做一些實際工作,即作為一個整體更新數據。數據更新很容易就會碰到并發問題,聚合有義務提供相關支持來解決并發沖突,這是通過使用樂觀離線鎖來完成的。
  并發是一個復雜的問題,僅了解一點樂觀離線鎖并不能順利完成相關工作。有些業務場景需要使用悲觀離線鎖進行補充。另外,數據庫也有自己的并發模型,同樣有樂觀和悲觀模式,那么,聚合中使用的并發模型與數據庫中的并發模型關系怎樣?
  對并發問題認識不清,輕則導致系統性能低下,重則導致數據錯亂,所以我將在本文對開發中可能碰到的并發問題進行簡單介紹。

聚合的選擇

很多程序員都喜歡追求設計的“正確性”,比如他會問,這一堆對象中哪個才是正確的聚合。只要是設計問題,由于每個人理解不同,肯定答案不一樣。更有經驗的開發人員能夠得到更好的設計,更接近于“標準答案”,但那是建立在充分理解的基礎上。如果一個高手告訴你某個類應該是聚合,你卻沒有真正理解他的用意,這種情況可能導致你設計出一個艱澀的系統。所以正確性是因人而異的,你應該因地制宜,而不是人云亦云。
  另外,高手告訴你的聚合也不見得是合適的,因為他不一定了解你的業務實際情況,聚合不僅受邏輯上的概念影響,并且還受到并發、性能等因素制約。
  下面介紹選擇聚合的一般性規律,可以幫助你進行一些決策。
  第一步,尋找具有包含或組成關系的相關對象。
  某些對象有附屬的子項,比如訂單Order和訂單項OrderItem,它們具有包含關系,訂單包含訂單項的集合,或者可以認為一個訂單是由N個訂單項組成的。
  找到的N組相關對象成為聚合的候選,能不能成為聚合需要經過后面的篩選。
  第二步,考慮聚合內部的子對象集合,是否需要被聚合根外部的對象直接訪問,如果需要,將其從聚合中移出,并建模為獨立聚合。
  雖然一個對象可能從概念上被另一個對象包含,但如果這種包含關系很弱,一般意味著子對象離開該聚合可能仍然有意義,外界對象希望能夠直接和它打交道。
  第三步,聚合內部導致并發沖突嚴重時,進行聚合拆分。
  前兩步是從概念上選擇聚合,但聚合還受到其它因素影響,比如并發、性能等。
  通過樂觀離線鎖可以保證,兩次提交的聚合不會發生更新丟失。如果聚合只包含它本身,出現沖突的可能性就很小。但由于聚合中往往包含集合,甚至是多個集合,所以各個集合之間的修改可能導致并發沖突很嚴重。
  比如一個聚合中包含兩個實體集合,用戶A正在編輯聚合的第一組實體集合,與此同時,用戶B 開始編輯同一個聚合的第二組實體集合,第一個人提交成功,第二個人將更新失敗。
  如果用戶經常需要對聚合內的不同集合進行單獨編輯,這就說明聚合中的概念可能具有獨立性,應該拆分出來。當聚合內部集合經常導致更新失敗時,果斷進行拆分是必須的。
  設計一個大型聚合,除了可能經常導致并發沖突外,還可能導致低下的性能。比如酒店包含不同的房型,每個房型包含不同的價格政策,每種價格政策的價格又不同,價格可能每隔幾天都會變化,如果把酒店作為一個大型聚合,把其它都作為集合包含進來,創建一個酒店聚合的開銷可能很驚人。
  當聚合中的子對象集合的層級超過2級,比如子對象又包含孫對象集合,需要考慮是否會導致并發和性能問題。另外一個聚合中包含子對象集合的數量也需要控制,比如一個聚合包含10個子對象集合,出現沖突的可能性就會很大。還有一個問題是,包含的子對象集合的元素個數也要考慮,比如一個商品,需要記錄商品的價格變動歷史,由于價格是商品的一個屬性,所以可能會把價格變動歷史也放到商品中。如果價格經常變動,比如每天2次,一年就會產生700條記錄,可以看到,有些子對象集合剛開始數據量不大,但會持續增加,這種情況也需要進行聚合拆分。
  如果一個聚合良好表達了一個整體概念,把附屬信息都封裝起來,并且沒有導致并發沖突經常發生,還性能良好,可以認為設計相當成功了,當然,這很不容易。

識別聚合(邊界)的方法

聚合的核心概念

聚合可能包含一個或多個對象,有一個根,是數據修改和持久化的最小單元。

識別方法:

  • 考慮對象是否可以獨立存在
  • 對象是否會直接和其他對象打交道
  • 對象之間是否有不變性(Invariants),不變性指聚合內對象之間不管如何變化總是必須滿足某個數據一致性規則

聚合設計與實現原則

  • 將真正有不變性的對象聚合在一起
  • 聚合應盡量小
  • 聚合之間的關聯用ID,不用引用
  • 只保留必須的關聯,單向關聯
  • 聚合內強一致性,聚合間考慮最終一致性
  • 持久化聚合時,總是完全覆蓋

并發問題及解決方案

上面介紹了聚合的基本概念,由于聚合更新與并發密切相關,下面將介紹應用程序開發中隨時可能碰到的并發問題,并討論相關解決方案。同時,將應用程序級別的并發模型與數據庫事務級別的并發模型進行比較,這樣可以對并發解決方案有更清晰的認識。

數據一致性問題

如果多個操作同時集中在同一條數據上,就可能造成并發,導致數據不一致。并發產生的數據不一致現象主要有以下幾種:
  1. 臟讀
  當事務A正在更新數據,但還未提交,另一個事務B獲取了正在更新的數據,發生臟讀。由于當前數據處于中間狀態,如果事務A更新失敗,則發生回滾,將導致事務B讀取的數據是錯誤的。
  臟讀有百害而無一利,應該盡量避免。
  2. 不可重復讀
  事務A讀取了需要的數據,另一個事務B對這些數據進行了更改,當事務A準備用這些數據進行計算時,實際上數據已經被改變了,這種情況稱為不可重復讀。換句話說,在同一個事務中,兩次發出相同條件的Select語句獲取的結果不同。
  不可重復讀大部分時候都不是問題,在一次計算中,應該使用老版本的數據,還是必須使用最新的數據進行計算,這是一個業務問題。
  3. 幻讀
  事務A使用范圍條件讀取了需要的數據,另一個事務B在該范圍添加了一些數據,當事務A準備用剛才獲取的數據進行精確統計時,但實際上還有漏網之魚,這種情況稱為幻讀。
  絕大部分的系統都不需要考慮這個問題,避免幻讀只在某些高精度的場景下才需要,比如銀行對帳。
  4. 丟失更新
  前三種問題主要發生在數據庫事務級別,丟失更新則發生在應用程序業務級別。丟失更新的概念很簡單,就是后一個人把前一個人的操作覆蓋了,導致前一個人的更新丟失。
  客戶Customer,它有三個屬性:標識Id,名稱Name,描述Description,其中一條數據為:Id=1,Name=”a”,Description=”Hello”。
  現在張三把Id為1的客戶編輯界面打開,然后就吃飯去了。
  李四對Id=1的客戶進行編輯,修改了Name為“b”,保存成功。
  張三吃完飯回來,繼續干活,他把Description改成”Haha”,保存之后,李四修改的Name=”b”又變回Name=”a”,李四的工作白干了。
  丟失更新是嚴重的數據修改錯誤,應該堅決避免。
  5. 重復更新
  重復更新是前面幾種問題的變體,由于危害很大,所以我專門把它拿出來討論。
  重復更新在概念上也很簡單,本來只允許執行一次的操作,現在執行了多次。
  考慮一個在線充值的場景,現在用戶在第三方支付平臺支付了100元,第三方支付平臺向你的系統發送了一個支付成功的確認,你的系統現在需要為充值編號為1對應的客戶余額增加100。假定你開啟了一個數據庫事務來完成這個操作,正在執行的過程中,第三方支付平臺系統抽筋,又向你的系統重復發送了一次支付確認請求,如下圖所示。

040359059674030.jpg

上面的過程執行完畢,你的系統給客戶充值200元,客戶非常滿意,以為你買一送一。
  從上面可以看到,該程序員雖然不懂并發,但還是有防御編程意識,在事務開始的最前面,通過充值狀態判斷來防止重復充值。
  通過狀態判斷的方式一般可以抵擋大部分的重復更新操作,只在運氣極背的時候碰上并發而導致錯誤,由于并發極難重現,而且在數據量比較大時也不容易通過肉眼觀察出來,所以碰到這種問題一般都是不了了之。
  如果你的系統需要和錢打交道,那么加強并發知識的學習就非常有必要,這可以讓你的公司少賠一點錢。

Sql Server數據庫并發模型與事務隔離級別

觀察前三種并發問題,都是讀和寫之間并發造成的。Sql Server數據庫為了解決讀寫并發沖突,首先引入了悲觀并發模型,通過鎖進制來解決讀寫沖突
  前面說過,臟讀是必須要避免的問題。Sql Server數據庫在讀取前通過獲取共享鎖來解決這個問題,在更新數據時會獲取獨占鎖,由于共享鎖與獨占鎖無法共存,導致讀取數據時,更新被阻塞,或在更新數據時,讀取被阻塞,從而解決了臟讀。
  雖然臟讀被解決了,但卻引入了讀寫阻塞的問題,在有一些數據量和并發量的系統上,性能可能表現得很低下。有一些程序員發現可以通過添加鎖提示With(NoLock)獲得更好的性能,這其實是走回了老路。With(NoLock)鎖提示將默認的事務隔離級別(讀已提交)降低為讀未提交,讀未提交事務隔離級別在讀取數據前不獲取共享鎖,所以不會阻塞,但它會導致臟讀。更好的方法是通過添加緩存機制,以及數據讀寫分離,將頻繁的查詢從主庫卸載
  從Sql Server 2005開始支持樂觀并發模型,它通過在修改或刪除數據前將數據的老版本存儲到臨時數據庫TempDB的版本存儲區來解決讀寫并發導致的不一致,并解決了讀寫阻塞問題。Sql Server為樂觀并發提供了兩個新的事務隔離級別——快照隔離級別和讀已提交快照隔離級別
  快照隔離級別解決了不可重復讀和幻讀的問題,但需要犧牲更多的更新性能(因為在修改或刪除數據前需要先備份到版本存儲區)和TempDB存儲空間。由于大部分系統不可重復讀和幻讀都不是大問題,所以一般推薦使用讀已提交快照隔離級別,它不僅開銷更小,而且行為上與悲觀模型更兼容。
  悲觀并發模型還包括另外兩個事務隔離級別,可重復讀隔離級別通過把共享鎖生命周期延長到事務結束來解決不可重復讀的問題,而可序列化隔離級別通過鍵范圍鎖或表鎖來限制查詢范圍內的添加,解決了幻讀。這兩個事務隔離級別一般不要使用,因為將共享鎖的持續時間延長會導致更大范圍的阻塞,另外延長共享鎖持續時間可能導致轉換死鎖。可以通過使用更新鎖或快照隔離級別來代替這兩個事務隔離級別。
  在上面重復更新的例子中,進行充值狀態判斷是防止重復更新的關鍵,該范例之所以抵擋不住并發,是因為在獲取充值記錄時,默認獲取的是共享鎖,由于多個事務均可以獲取共享鎖,且共享鎖默認生命周期非常短暫,所以讓另一個事務有了可趁之機。解決辦法很簡單,在獲取充值記錄時添加鎖提示With(UpdLock),這樣在充值記錄L1上獲取到更新鎖,更新鎖的特點是只有一個事務能夠獲取更新鎖,生命周期持續到事務結束或成功轉換為獨占鎖,這樣在事務1獲取到充值記錄L1時,該記錄被更新鎖鎖定,事務2在開啟事務后,準備獲取充值記錄L1時就被阻塞,直到事務1提交事務。當事務1成功提交事務時,充值狀態已改為“已充值”,所以事務2進行判斷時就會跳出事務,后續充值不會被執行。
  使用With(UpdLock)解決重復更新需要手工編寫存儲過程,對于面向對象開發很明顯不太適用。
  聚合通過引入樂觀離線鎖可以解決丟失更新和重復更新的問題

樂觀離線鎖

觀察上面丟失更新的例子,張三把操作界面一打開就吃飯去了,請問如何通過數據庫事務解決這個問題?
  數據庫事務在開啟之后,會鎖定大量資源,如果它在某些數據上獲取了獨占鎖,在事務提交之前不會釋放,所以對事務的一個基本要求就是執行要快。很明顯,你不能在張三把界面一打開的時候,就開一個事務等待他輸入,在保存的時候再提交事務,因為他的輸入時間不確定,可能導致一個很長時間的事務。
  可以看到,數據庫的并發模型也不是萬能的,對于上面的場景需要使用應用程序級別的并發控制。如果張三和李四不會經常修改同一條記錄,就可以使用樂觀離線鎖來解決更新丟失的問題。
  樂觀是指并發沖突機率很低,離線是指操作不是在同一個數據庫事務中完成的,比如打開編輯頁面時使用一個事務進行讀取,中間則與數據庫事務無關,在保存時會開啟另一個事務進行更新,可以看到這個過程是跨數據庫事務的操作。樂觀鎖的優勢是最大化系統并發度。
  樂觀離線鎖通過為每行數據添加一個版本號來識別當前數據的版本,在獲取數據時將版本號保存下來,更新數據時將版本號作為Where中的過濾條件,如果該記錄被更新,則版本號會發生變化,所以導致更新數據時影響行數為0,通過引發一個并發更新異常讓你了解數據已經被別人更新。
  樂觀離線鎖不僅可以解決丟失更新,而且同樣可以解決重復更新。當第二個操作獲得充值聚合時,如果充值狀態為“未充值”,它繼續后面的步驟。第一個操作更新完成后版本號發生改變,當第二個操作試圖提交更新時,就會檢測到并發沖突。在并發異常處理中,甚至對第二個操作進行重試都是安全的,因為它重新獲取充值聚合時,充值狀態已經為“已充值”,這樣就攔截了非法操作。可以看到,重復更新的問題,不管用哪種方法,都需要根據狀態判斷進行防御編程。
  Sql Server數據庫提供了Timestamp的數據類型來支持樂觀離線鎖,每當有數據插入或更新,這個字段會自動生成版本數據。
  與此同時,Entity Framework也提供了IsRowVersion來配置樂觀離線鎖。
  從上面的描述可以看出,樂觀離線鎖是應用程序級別的并發模型,與數據庫的樂觀并發模型沒有什么關系,雖然Sql Server數據庫的樂觀并發模型也有行版本的概念。這也意味著你在應用程序級別使用的是樂觀鎖,而Sql Server數據庫中卻使用的是悲觀鎖。
  使用樂觀離線鎖的前提是并發沖突機率很低,如果沖突機率很高,使用樂觀離線鎖雖然不會導致系統數據錯亂,但會導致用戶十分抓狂,因為每次保存成功都需要運氣。
  對于沖突機率很高的場景,需要引入悲觀離線鎖,下面繼續介紹。

悲觀離線鎖

一個100人的客服團隊,他們的工作是對某種申請單進行處理。客服處理一個申請單的時間大致5分鐘,每成功處理一個申請單可提成1元,每當用戶提交一個申請單,所有客服都可以看見。
  一個編號為1的申請單過來了,為了爭取拿到那一元錢提成,100名客服爭先恐后的打開業務處理界面并開始授理。一名18歲的小妹眼明手快,只花了3分零2秒就提交了,“耶,1元到手”。另一名小妹花了3分零8秒,提交的時候,系統彈出一個友情提示“由于你的動作較慢,1元提成已經被人捷足先登了”。之后,接二連三的失敗,大家只能感嘆自己運氣不好,另外有點走神,希望下一次可以拿到提成。
  故事說完了,該系統采用樂觀離線鎖設計,雖然整個操作沒有導致數據出錯,但整個客服團隊的辦事效率低得嚇人,近乎串行操作。
  解決上面的問題,有兩個常見辦法。
  一種辦法是通過一套自動調度策略開發一個申請單自動分配服務,申請單一來,未處理前就已經確定好由誰處理了,這樣就不會造成激烈的競爭,使用樂觀離線鎖也許就能滿足需求。
  另一種辦法是使用悲觀離線鎖,開發一個鎖管理器,鎖管理器需要在數據庫中建表,記錄鎖定時間,鎖定人,業務編號等信息,在申請單列表界面的每行都放一個“鎖定”按鈕,當第一個人點擊“鎖定”按鈕時,向鎖管理器添加鎖記錄,一旦被鎖定,其它人不能編輯操作界面或進行提交,界面控件應該處于凍結狀態,更嚴格的甚至不能打開編輯界面。
  使用這種方案有一些問題,在點擊“鎖定”按鈕時可能存在并發問題,這可以通過為鎖管理器的業務編號建立唯一索引,保證不會在同一個業務編號上插入兩條鎖定記錄,當然,這要求你的業務編號可能是Guid,不然唯一性需要添加更多屬性來識別。
  既然允許鎖定,就需要有解鎖功能,解鎖可以通過簡單的刪除鎖定數據來完成。當編輯完成時,還需要對該業務編號自動解鎖。也可能需要根據角色權限進行解鎖,當某個客服鎖定數據后就下班回家了,這導致其它人無法處理,所以更高級別的小組長可能允許對他的下級鎖定的數據進行解鎖。
  如果需要強大的鎖管理器,你可以仿照Sql Server悲觀鎖進行設計,加入鎖模式、鎖粒度、持續時間等要素。
  可以看到,悲觀離線鎖,在實現和操作上并不簡單,它只應該成為樂觀離線鎖的補充。

粗粒度鎖與隱含鎖

你可以把樂觀離線鎖放到每個實體中,但這樣太復雜,把樂觀離線鎖放到聚合根上,則整個聚合都可以獲得并發控制能力,這稱為粗粒度鎖
  另外,可以在聚合根和映射的層超類型上將樂觀離線鎖封裝起來,稱為隱含鎖

總結

聚合的概念

  • 聚合包裝一組高度相關的對象,作為一個數據修改的單元。
  • 聚合根是聚合最外層的實體對象,它劃分了一個邊界,聚合根外部的對象,不能直接訪問聚合根內部對象。
  • 聚合體現了封裝的思想。每個聚合有一個根和一個邊界,邊界定義了一個聚合內部有哪些實體或值對象,根是聚合內的某個實體
  • 聚合聚合根開始導航,絕對不能繞過聚內部的對象之間可以相互引用,但是聚合外部如果要訪問聚合內部的對象時,必須通過合根直接訪問聚合內的對象,也就是說聚合根是外部可以保持對它的引用的唯一元素;
  • 聚合內除根以外的其他實體的唯一標識都是本地標識,也就是只要在聚合內部保持唯一即可,因為它們總是從屬于這個聚合的;
  • 聚合根負責與外部其他對象打交道并維護自己內部的業務規則;
  • 基于聚合的以上概念,我們可以推論出從數據庫查詢時的單元也是以聚合為一個單元,也就是說我們不能直接查詢聚合內部的某個非根的對象;
  • 聚合內部的對象可以保持對其他聚合根的引用;
  • 刪除一個聚合根時必須同時刪除該聚合內的所有相關對象,因為他們都同屬于一個聚合,是一個完整的概念;

聚合的作用

  • 簡化系統設計。不要采用每個表對應一個實體,每個實體都是聚合的設計方式。
  • 強制實施相關對象上的一致性規則。
  • 對并發更新提供保護。

聚合的選擇

  • 尋找具有包含或組成關系的相關對象。
  • 聚合內部的子對象需要被聚合根外部的對象直接訪問時,進行聚合拆分。
  • 聚合內部導致并發沖突嚴重時,進行聚合拆分。

識別聚合:

從業務的角度深入分析哪些對象它們的關系是內聚的,是一個整體;所謂關系是內聚的,是指這些對象之間會保持一個固定規則,固定規則是指在數據變化時必須保持不變的一致性規則

識別聚合根:

  • 判斷是否有獨立存在的意義
  • 判斷是否可以被從聚合外部直接訪問
  • 判斷實體的ID是否會獨立的出現在外面

聚合的例子

聚合的例子.png

并發問題及解決方案

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

推薦閱讀更多精彩內容