vitess兩階段提交事務(wù)

Vitess之前漫雕,先復(fù)習(xí)一下事務(wù)的四個(gè)基本特性

  • 原子性:一個(gè)事務(wù)對(duì)狀態(tài)的改變是原子的罩抗,要么都發(fā)生拉庵,要么都不發(fā)生,這些改變包括數(shù)據(jù)庫(kù)的改變套蒂、消息以及對(duì)轉(zhuǎn)換器的操作钞支。
  • 一致性:一個(gè)事務(wù)是對(duì)狀態(tài)的一個(gè)正確改變。作為一組操作沒(méi)有違反任何與狀態(tài)相關(guān)的完整性約束操刀。這要求事務(wù)是一個(gè)正確的程序烁挟。
  • 隔離性:盡管事務(wù)是并發(fā)執(zhí)行的,但看起來(lái)是單個(gè)執(zhí)行的骨坑,即對(duì)于一個(gè)事務(wù)T撼嗓,任何其他事務(wù)要么在T之前執(zhí)行,要么在T之后執(zhí)行欢唾,但不會(huì)既在T之前執(zhí)行且警,又在T之后執(zhí)行。
  • 永久性:一旦一個(gè)事務(wù)成功完成(提交)礁遣,它對(duì)狀態(tài)的改變不會(huì)受其他失敗的影響斑芜。

????舉一個(gè)銀行取錢(qián)事務(wù)的例子。如果同時(shí)完成了取出錢(qián)和賬戶的更改亡脸,那就是原子的押搪。如果賬戶減少的錢(qián)等于取出的錢(qián)树酪,那么就是一致的。如果這個(gè)過(guò)程不受其他程序并發(fā)讀寫(xiě)你賬戶的程序的影響(比如你的女朋友們正在并發(fā)的刷你的銀行卡)大州,那么它是隔離的续语。一旦事務(wù)完成了,(無(wú)論完成之后機(jī)器宕機(jī)厦画、斷電疮茄、還是網(wǎng)絡(luò)異常)賬戶的余額必然會(huì)反映取款后的情況,那么它是永久性的根暑。

????事務(wù)是數(shù)據(jù)庫(kù)的核心特性力试,MySQL、ORACLE排嫌、PostgreSQL這些數(shù)據(jù)庫(kù)都是支持事務(wù)的畸裳。國(guó)內(nèi)互聯(lián)網(wǎng)公司更多的使用MySQL。

????但是互聯(lián)網(wǎng)公司的數(shù)據(jù)一般比較大淳地,單機(jī)數(shù)據(jù)庫(kù)服務(wù)難以承擔(dān)這么大的數(shù)據(jù)量怖糊。這時(shí)候一般會(huì)做分庫(kù),將原來(lái)一個(gè)數(shù)據(jù)庫(kù)的數(shù)據(jù)拆分到多個(gè)庫(kù)(這里簡(jiǎn)單認(rèn)為一個(gè)MySQL實(shí)例上只有一個(gè)庫(kù))颇象,比用戶表做拆分伍伤,根據(jù)用戶id,id對(duì)64取余數(shù)遣钳,然后根據(jù)得到的余數(shù)定位特定的分庫(kù)扰魂。比如id等于1的用戶的信息就在第1個(gè)庫(kù),id等于2的用戶數(shù)據(jù)就在第2庫(kù)蕴茴,這樣就解決了單機(jī)數(shù)據(jù)容量問(wèn)題劝评。

????現(xiàn)在用戶1要下單了,需要白條扣款生成訂單兩個(gè)步驟荐开。兩個(gè)步驟需要保證原子性的付翁,要么扣款完成、訂單生成晃听,要么不扣款百侧、不下單(all-or-nothing)。如果訂單和白條表都是按照用戶id做拆分的那最好能扒,所有操作都會(huì)在同一個(gè)庫(kù)上面進(jìn)行佣渴,使用單機(jī)事務(wù),MySQL就保證了原子性初斑。如果白條表是按照訂單id拆分的辛润,那很可能不在第1分庫(kù)了??。這時(shí)候就涉及到分布式事務(wù)见秤。

本來(lái)事務(wù)的執(zhí)行流程應(yīng)該是這樣:

    BEGIN;
    生成訂單; 白條扣款;
    COMMIT/ROLLBACK

????問(wèn)題就出在分布式場(chǎng)景中的提交事務(wù)砂竖。我們需要連接兩個(gè)庫(kù)開(kāi)啟兩個(gè)分庫(kù)的兩個(gè)事務(wù)真椿,TX1和TX2, TX1生成訂單寫(xiě)入分庫(kù)1,TX2執(zhí)行白條扣款寫(xiě)入分庫(kù)2乎澄。

在業(yè)務(wù)代碼里執(zhí)行時(shí)間線(t1到t5表示先后的時(shí)間點(diǎn))會(huì)是這樣的:

 (t1)SESSION1 BEGIN TX1突硝;
 (t2)SESSION2 BEGIN TX2;
 (t3)SESSION1 生成訂單置济,訂單信息寫(xiě)入分庫(kù)1解恰;
 (t4)SESSION2白條扣款寫(xiě)入分庫(kù)2;
 (t5)SESSION1 COMMIT TX1浙于; 
 (t6)SESSION2 COMMIT TX2.

????如果t5執(zhí)行正常护盈,給客戶下單了,分庫(kù)2宕機(jī)了羞酗,提交TX2失敗腐宋,TX2自動(dòng)回滾。分布式事務(wù)只提交了一部分整慎,訂單生成了脏款,但是白條沒(méi)扣款(我做游戲的時(shí)候都是先扣錢(qián)、扣錢(qián)成功再發(fā)裝備??)裤园。這就經(jīng)典的是部分提交(PARTIAL COMMIT)問(wèn)題。

????在分布式場(chǎng)景中保證原子性避免部分提交剂府,有一個(gè)辦法拧揽,那就是兩階段提交(TWO PHASE COMMIT

????上面的分布式事務(wù)涉及到了分庫(kù)1和分庫(kù)2。其實(shí)還有一個(gè)角色腺占,就是執(zhí)行業(yè)務(wù)邏輯的worker節(jié)點(diǎn)淤袜,worker在兩階段提交過(guò)程中也是一個(gè)重要的角色,我們稱之為事務(wù)管理器(Transaction Manager)衰伯,事務(wù)管理器在兩階段提交過(guò)程中是一個(gè)協(xié)調(diào)者的角色铡羡。事務(wù)管理器負(fù)責(zé)整個(gè)事務(wù)的開(kāi)始、執(zhí)行意鲸、回滾或者提交烦周。事務(wù)管理器異常也可能導(dǎo)致整個(gè)事務(wù)的異常,比如worker節(jié)點(diǎn)在t5之后怎顾,t6之前宕機(jī)读慎,也會(huì)導(dǎo)致整個(gè)事務(wù)的部分提交。我們也給分庫(kù)1和分庫(kù)2一個(gè)專業(yè)的名字槐雾,叫做資源管理器(Resource Manager),資源管理器在分布式事務(wù)中是參與者的角色夭委,負(fù)責(zé)執(zhí)行協(xié)調(diào)者下達(dá)的指令,此外資源管理器是能夠支持本地事務(wù)的募强。

MySQL提供了XA分布式事務(wù)接口供外部協(xié)調(diào)者調(diào)用株灸。

關(guān)于XA事務(wù)的兩個(gè)概念:

  • 資源管理器(Resource Manager):用來(lái)管理系統(tǒng)資源崇摄,是通向事務(wù)資源的途徑。數(shù)據(jù)庫(kù)就是一種資源管理器慌烧。資源管理還應(yīng)該具有管理事務(wù)提交或回滾的能力配猫。

  • 事務(wù)管理器(Transaction Manager):事務(wù)管理器是分布式事務(wù)的核心管理者。事務(wù)管理器與每個(gè)資源管理器(Resource Manager)進(jìn)行通信杏死,協(xié)調(diào)并完成事務(wù)的處理泵肄。事務(wù)的各個(gè)分支由唯一命名進(jìn)行標(biāo)識(shí)。

上面例子中的生成訂單白條扣款的分布式事務(wù)中淑翼,MySQL服務(wù)器相當(dāng)于XA事務(wù)資源管理器腐巢,與MySQL鏈接的客戶端,也就是執(zhí)行業(yè)務(wù)邏輯的worker節(jié)點(diǎn)相當(dāng)于事務(wù)管理器玄括。

但是MySQL早期的版本XA事務(wù)存在BUG冯丙,Vitess自己實(shí)現(xiàn)了整套兩階段提交,所以Vitess是學(xué)習(xí)兩階段提交的一個(gè)很好的資源遭京。Vitess地址: https://github.com/vitessio/vitess

先引入Vitess的幾個(gè)概念:

image

上面是Vitess的整體架構(gòu)胃惜,兩階段提交過(guò)程中涉及到的模塊主要是vtgatevttabletMySQL哪雕。

vttabletMySQL其實(shí)可以看做一個(gè)整體船殉,我們稱之為Shard。Shard在兩階段提交中是參與者斯嚎,也就是資源管理器利虫。

vtgate是事務(wù)管理器,也就是事務(wù)的協(xié)調(diào)者堡僻,業(yè)務(wù)的應(yīng)用Application連接到vtgate上面糠惫,vtgate通過(guò)路由將業(yè)務(wù)的請(qǐng)求轉(zhuǎn)發(fā)到后端的一個(gè)或者多個(gè)Shard,然后在將結(jié)果匯總發(fā)生Application钉疫。Application發(fā)起一個(gè)事務(wù)硼讽,比如插入兩條數(shù)據(jù),根據(jù)vtgate做路由后可能將兩條數(shù)據(jù)插入到一個(gè)或者兩個(gè)Shard牲阁。

上圖中的三個(gè)Shard是三個(gè)資源管理器固阁,也是兩階段提交的參與者。vtgate是事務(wù)管理器咨油,是兩階段提交的的協(xié)調(diào)者您炉。

??vtgate到vttablet請(qǐng)求是通過(guò)grpc實(shí)現(xiàn),vttablet維護(hù)了到MySQL的連接池役电。

??對(duì)于非事務(wù)請(qǐng)求赚爵,vtgate請(qǐng)求發(fā)送到vttablet,vttablet隨便從連接池里拿到一個(gè)連接,執(zhí)行SQL冀膝,返回結(jié)果給vtgate唁奢,然后將連接放回到連接池,這樣一個(gè)請(qǐng)求單獨(dú)占用連接的時(shí)間很短窝剖。

??對(duì)于事務(wù)請(qǐng)求麻掸,vtgate發(fā)生請(qǐng)求到vttablet,vttablet會(huì)從事務(wù)連接池里拿到一個(gè)連接赐纱,并生產(chǎn)一個(gè)本地事務(wù)id脊奋,執(zhí)行SQL,將執(zhí)行結(jié)果和本地事務(wù)id返回給vtgate疙描,最后將事務(wù)連接放入一個(gè)單獨(dú)的activePool诚隙,這樣下次vtgate需要在該事務(wù)里繼續(xù)執(zhí)行SQL,只需要請(qǐng)求帶著事務(wù)id起胰,通過(guò)事務(wù)id從activePool拿到事務(wù)連接淮菠,執(zhí)行SQL即可涯穷,直到vtgate提交或者回滾事務(wù),vttablet才將連接從activePool放回到事務(wù)連接池鳄乏。

下面的代碼就是vttablet根據(jù)本地事務(wù)id從activePool拿連接速梗。

// Get fetches the connection associated to the transactionID.
// You must call Recycle on TxConnection once done.
func (axp *TxPool) Get(transactionID int64, reason string) (*TxConnection, error) {
    v, err := axp.activePool.Get(transactionID, reason)
    if err != nil {
        return nil, vterrors.Errorf(vtrpcpb.Code_ABORTED, "transaction %d: %v", transactionID, err)
    }
    return v.(*TxConnection), nil
}

Vitess的兩階段提交實(shí)現(xiàn)

回到上面的分布式事務(wù)徽龟,vtgate要在第一個(gè)Shard和第二個(gè)Shard寫(xiě)入數(shù)據(jù)绩聘,vtgate在兩個(gè)SESSION里面開(kāi)啟兩個(gè)事務(wù)踢代,執(zhí)行并記錄SQL。

      (t1)SESSION1 BEGIN TX1 IN Shard1;  

      (t2)SESSION2 BEGIN TX2 IN Shard2

      (t3)SESSION1 生成訂單寫(xiě)入Shard1未提交瓜客,將對(duì)應(yīng)SQL語(yǔ)句作為redo log記錄在內(nèi)存中 

      (t4)SESSION2白條扣款寫(xiě)入Shard2未提交适瓦,將對(duì)應(yīng)SQL語(yǔ)句作為redo log記錄在內(nèi)存中 

????除了把事務(wù)中執(zhí)行的SQL記錄在內(nèi)存中之外,前面四個(gè)步驟和之前沒(méi)有任何太多區(qū)別谱仪,區(qū)別就在最后的提交階段。下面我們結(jié)合代碼看Vitess是如何做兩階段提交的否彩。

下面是Vitess中vtgate作為協(xié)調(diào)者的代碼模塊疯攒,結(jié)合代碼分析Vitess是如何做兩階段提交的

1 func (txc *TxConn)commit2PC(ctx context.Context, session *SafeSession)error {
2  if len(session.ShardSessions) <=1 {
3    return txc.commitNormal(ctx, session)
4  }
5
6 participants :=make([]*querypb.Target, 0, len(session.ShardSessions)-1)
7 for _, s :=range session.ShardSessions[1:] {
8   participants = append(participants, s.Target)
9 }
10 
11 mmShard := session.ShardSessions[0]
12 dtid := dtids.New(mmShard)
13 err := txc.gateway.CreateTransaction(ctx, mmShard.Target, dtid, participants)
14 if err != nil {
15 // Normal rollback is safe because nothing was prepared yet.
16      txc.Rollback(ctx, session)
17   return err
18  }
19
20 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {
21  return txc.gateway.Prepare(ctx, s.Target, s.TransactionId, dtid)
22 })
23
24 if err != nil {
25    if resumeErr := txc.Resolve(ctx, dtid); resumeErr != nil {
26      log.Warningf("Rollback failed after Prepare failure: %v", resumeErr)
27 }
28 // Return the original error even if the previous operation fails.
29      return err
30 }
31 err = txc.gateway.StartCommit(ctx, mmShard.Target, mmShard.TransactionId, dtid)
32 if err != nil {
33   return err
34 }
35 err = txc.runSessions(session.ShardSessions[1:], func(s *vtgatepb.Session_ShardSession)error {
36   return txc.gateway.CommitPrepared(ctx, s.Target, dtid)
37 })
38 if err != nil {
39  return err
40 }
41 return txc.gateway.ConcludeTransaction(ctx, mmShard.Target, dtid)
42 }

從代碼2~4(表示第2行到第4行,下同)可以看到列荔,對(duì)于只涉及一個(gè)Shard的事務(wù)敬尺,不會(huì)涉及部分提交問(wèn)題,MySQL的事務(wù)就可以保證原子性贴浙,不需要走兩階段提交砂吞,直接提交即可。

兩階段提交主要分為以下幾個(gè)步驟

1崎溃、生成分布式的事務(wù)id (11~12)

????分布式的事務(wù)id為第一個(gè)參與者的Shard信息+該Shard的本地事務(wù)id蜻直,這里我們簡(jiǎn)單的認(rèn)為是Shard1:TX1,我們將該Shard1稱之Shard1:TX1這個(gè)分布式事務(wù)的MetadataManager Shard,因?yàn)镾hard1上管理該分布式事務(wù)的元數(shù)據(jù)概而,縮寫(xiě)成mmShard呼巷。

????注意Shard1Shard1:TX1這個(gè)事務(wù)的mmShard,因?yàn)樵摲植际绞聞?wù)的第一個(gè)參與者是Shard1赎瑰。

????如果分布式事務(wù)的參與者是Shard3上面的TX3Shard2上面的TX2王悍,Shard3是第一個(gè)參與者,那么該分布式事務(wù)的id為Shard3:TX3餐曼。Shard3為該分布式事務(wù)的mmShard压储。這樣mmShard能夠比較均衡的分布式在所有Shard中,一方面可以避免對(duì)mmShard操作造成熱點(diǎn)源譬,另外也可以避免單個(gè)Shard故障影響整個(gè)Vitess集群集惋。

????分布式事務(wù)id定義成Shard:TX是有意義的,這樣后續(xù)的流程中可以根據(jù)分布式事務(wù)id直接知道該分布式事務(wù)的元數(shù)據(jù)信息在哪個(gè)Shard上面瓶佳,方便讀取芋膘。

2、在mmShard記錄元數(shù)據(jù)信息 (13~18)

????記錄分布式事務(wù)的元數(shù)據(jù)信息到mmShard的MySQL數(shù)據(jù)庫(kù)中(也就是記錄到Shard1的MySQL中)霸饲,記錄的信息包括分布式事務(wù)id为朋、事務(wù)當(dāng)前狀態(tài)、開(kāi)始時(shí)間厚脉,其他參與者(participants)

 分布式事務(wù)id     事務(wù)當(dāng)前狀態(tài)           事務(wù)開(kāi)始時(shí)間            事務(wù)的其他所有參與者信息

  Shard1:TX1     初始Prepare狀態(tài)    比如1989-09-20 00:00:00    Shard2:TX2

????此處所有的記錄操作不是在Shard1的事務(wù)TX1习寸,而是單獨(dú)使用了一個(gè)連接,因?yàn)橛涗浀脑獢?shù)據(jù)信息需要直接提交寫(xiě)入磁盤(pán)傻工,如果放入TX1中就連同TX1一起提交了霞溪。

3、Prepare階段 (20-22)

????給所有參與者發(fā)送Prepare指令中捆,參與者接收到帶有本身事務(wù)id的Prepare指令之后
????1. ?通過(guò)本地事務(wù)id(TX1鸯匹、TX2)將事務(wù)連接從普通的事務(wù)連接池拿出來(lái)放到一個(gè)叫做preparedPool的特有連接池,preparedPool通過(guò) 分布式事務(wù)id作為key泄伪,也就是Shard1和Shard2都是通過(guò)Shard1:TX1作為key殴蓬,SESSION1和SESSION2 使用的事務(wù)連接作為value存儲(chǔ)到preparedPool中。
????注意普通事務(wù)連接池和preparedPool不一樣的地方是蟋滴,普通事務(wù)連接池以本地事務(wù)id作為key存儲(chǔ)染厅、查找的,preparedPool中的連接是分布式事務(wù)id作為key來(lái)存儲(chǔ)查找的津函。問(wèn)題1肖粮,為什么需要一個(gè)特殊的preparedPool呢?

????2.?將之前事務(wù)TX1尔苦、TX2里記錄的redo log涩馆,也就是在事務(wù)里執(zhí)行的SQL對(duì)于的全局事務(wù)id號(hào) Shard1:TX1行施,記錄到本地?cái)?shù)據(jù)庫(kù)中,注意是需要使用單獨(dú)的新的連接凌净,寫(xiě)入數(shù)據(jù)庫(kù)并提交(此時(shí)不能使用TX1悲龟、TX2使用的連接提交redo log,這樣就把TX1冰寻、TX2一起提交了)须教。redo log寫(xiě)入到數(shù)據(jù)庫(kù)之后,即便TX1斩芭、TX2沒(méi)有提交轻腺、即便vttablet發(fā)升重啟、如果需要我們也可以重新執(zhí)行redo log划乖。

4贬养、StartCommit階段 (30~34)

????在步驟2,我們將事務(wù)的元數(shù)據(jù)信息記錄到了mmShard(也就是Shard1)的MySQL數(shù)據(jù)庫(kù)中琴庵,記錄的狀態(tài)是Prepare狀態(tài)∥笏悖現(xiàn)在將Prepare狀態(tài)修改成為Committed狀態(tài)。這是一個(gè)關(guān)鍵步驟迷殿,修改該狀態(tài)之前出任何異常我們都認(rèn)為事務(wù)尚未提交儿礼,修改成為Committed狀態(tài)之后,出任何異常我們都認(rèn)為事務(wù)已經(jīng)提交庆寺。出現(xiàn)異常之后蚊夫,我們需要做的就是根據(jù)記錄下的該狀態(tài)做相應(yīng)的補(bǔ)償措施。

 分布式事務(wù)id     事務(wù)當(dāng)前狀態(tài)           事務(wù)開(kāi)始時(shí)間            事務(wù)的其他所有參與者信息
  Shard1:TX1     Committed狀態(tài)    比如1989-09-20 00:00:00    Shard2:TX2

???? 在Shard1上記錄事務(wù)狀態(tài)標(biāo)記為Committed之后懦尝,我們就認(rèn)為兩階段提交成功了知纷,所以這時(shí)候我們可以順便把Shard1上面的TX1也提交了。使用單獨(dú)的連接先修改事務(wù)狀態(tài)再提交TX1陵霉,那不如直接在TX1里修改當(dāng)前事務(wù)狀態(tài)琅轧,直接提交TX1,效果一樣一樣的踊挠。

???? 在是否在TX1里直接修改狀態(tài)鹰晨,也是一個(gè)有意思的問(wèn)題,如果直接在TX1里修改狀態(tài)止毕,之后馬上提交,那么修改狀態(tài)和提交操作在一個(gè)事務(wù)里漠趁,兩個(gè)行為必然是原子操作扁凛,如果沒(méi)有在TX1里修改事務(wù)狀態(tài),而是使用了單獨(dú)的連接修改闯传,那么我們?cè)谧霎惓L幚淼臅r(shí)候就需要多考慮一種情況谨朝,狀態(tài)修改成功但是TX1未提交成功。

5、CommitPrepare階段(35~37)

????vtgate給出mmShard之外的其他參與者發(fā)送CommitPrepare指令字币,分布式事務(wù)id Shard1:TX1作為參數(shù)

????因?yàn)镾hard1是mmShard则披,mmShard在StartCommit階段已經(jīng)把事務(wù)提交了,這里以Shard2為例子說(shuō)明洗出,Shard2上面的vttablet接受到CommitPrepare指令之后士复,根據(jù)分布式事務(wù)id Shard1:TX1preparedPool里面取出之前的事務(wù)連接,TX2執(zhí)行了白條扣款但是尚未提交翩活。之前提到阱洪,在Prepare階段,我們還在本地?cái)?shù)據(jù)庫(kù)里記錄了redo log和對(duì)應(yīng)的全局事務(wù)id菠镇,這時(shí)候拿到了事務(wù)連接冗荸,先根據(jù)全局事務(wù)id刪除redo log,然后執(zhí)行Commit利耍。
????和StartCommit步驟一樣蚌本,刪除redo log直接放到TX2事務(wù)中,這樣能保證兩個(gè)操作的原子性隘梨,避免部分成功程癌,否則我們還需要多處理一種異常。

6出嘹、ConcludeTransaction結(jié)束分布式事務(wù)(41~41)

????只需要根據(jù)分布式事務(wù)id Shard1:TX1 刪除Shard1上面的元數(shù)據(jù)信息

異常分析

????上面是正常的兩階段提交流程席楚,但是兩階段最核心的任務(wù)是處理異常,下面我們看看Vitess是如何處理各種異常的税稼。

1. 創(chuàng)建分布式事務(wù)異常(14~17)

????對(duì)所有Shard發(fā)送Rollback指令烦秩,這樣Shard1回滾TX1,Shard2回滾TX2郎仆。如果是Shard1節(jié)點(diǎn)異常導(dǎo)致的寫(xiě)入分布式事務(wù)異常只祠,那么Shard1回滾也會(huì)報(bào)錯(cuò),不過(guò)沒(méi)關(guān)系扰肌,反正事務(wù)TX1沒(méi)有提交抛寝。Shard1回滾異常也不影響其他Shard執(zhí)行回滾操作。

  // Rollback rolls back the current transaction. There are no retries on this operation.
func (txc *TxConn) Rollback(ctx context.Context, session *SafeSession) error {
    if !session.InTransaction() {
        return nil
    }
    defer session.Reset()

    return txc.runSessions(session.ShardSessions, func(s *vtgatepb.Session_ShardSession) error {
        return txc.gateway.Rollback(ctx, s.Target, s.TransactionId)
    })
}

????對(duì)萬(wàn)一在mmShard已經(jīng)將分布式事務(wù)的元數(shù)據(jù)寫(xiě)入了呢曙旭?這個(gè)問(wèn)題和后面的ConcludeTransaction類似盗舰。

2. Prepare異常(24~27)

????創(chuàng)建事務(wù),寫(xiě)事務(wù)元數(shù)據(jù)信息到Shard1(mmShard)成功桂躏,Prepare階段異常钻趋,因?yàn)楫?dāng)前事務(wù)狀態(tài)仍然是Prepared狀態(tài)(未提交狀態(tài)),按照我們理解如果Prepare異常直接rollback就好了,但是在commit2PC函數(shù)中并未直接rollback剂习,而是調(diào)用了Resolve函數(shù)蛮位,為什么呢不直接rollback而是Resolve呢较沪?我們先看Resolve函數(shù)的實(shí)現(xiàn)。


// Resolve resolves the specified 2PC transaction.
1func (txc *TxConn) Resolve(ctx context.Context, dtid string) error {
2   mmShard, err := dtids.ShardSession(dtid)
3   if err != nil {
4       return err
5   }
6
7   transaction, err := txc.gateway.ReadTransaction(ctx, mmShard.Target, dtid)
8   if err != nil {
9       return err
10  }
11  if transaction == nil || transaction.Dtid == "" {
12      // It was already resolved.
13      return nil
14  }
15  switch transaction.State {
16  case querypb.TransactionState_PREPARE:
17      // If state is PREPARE, make a decision to rollback and
18      // fallthrough to the rollback workflow.
19      if err := txc.gateway.SetRollback(ctx, mmShard.Target, transaction.Dtid, mmShard.TransactionId); err != nil {
20          return err
21      }
22      fallthrough
23  case querypb.TransactionState_ROLLBACK:
24      if err := txc.resumeRollback(ctx, mmShard.Target, transaction); err != nil {
25          return err
26      }
27  case querypb.TransactionState_COMMIT:
28      if err := txc.resumeCommit(ctx, mmShard.Target, transaction); err != nil {
29          return err
30      }
31  default:
32      // Should never happen.
33      return vterrors.Errorf(vtrpcpb.Code_INTERNAL, "invalid state: %v", transaction.State)
34  }
35  return nil
36}

????創(chuàng)建首先根據(jù)事務(wù)idShard1:TX1拿到mmShard,也就是Shard1失仁,然后去Shard1讀取該事務(wù)的元數(shù)據(jù)信息,發(fā)現(xiàn)事務(wù)是Prepare狀態(tài)尸曼,尚未提交:

 分布式事務(wù)id     事務(wù)當(dāng)前狀態(tài)           事務(wù)開(kāi)始時(shí)間            事務(wù)的其他所有參與者信息
  Shard1:TX1     Prepared狀態(tài)    比如1989-09-20 00:00:00    Shard2:TX2

????創(chuàng)建首先修改mmShard事務(wù)狀態(tài)為回滾狀態(tài)SetRollback

分布式事務(wù)id     事務(wù)當(dāng)前狀態(tài)           事務(wù)開(kāi)始時(shí)間            事務(wù)的其他所有參與者信息
 Shard1:TX1     Rollbck狀態(tài)        比如1989-09-20 00:00:00    Shard2:TX2

????創(chuàng)建Resolve函數(shù)中22行fallthrough表示,如果SetRollback成功無(wú)異常萄焦,那么繼續(xù)執(zhí)行下一個(gè)case控轿,即resumeRollback

????創(chuàng)建通過(guò)下面的resumeRollback函數(shù)我們明白了,不能簡(jiǎn)單的執(zhí)行Rollback操作是因?yàn)榭赡芤恍┓制呀?jīng)Prepare成功并記錄了redo log楷扬,我們需要?jiǎng)h除redo log解幽;
????此外,上面的Prepare階段提到過(guò)烘苹,Prepare會(huì)把事務(wù)連接以分布式事務(wù)idShard1:TX1作為key躲株,存儲(chǔ)到 preparedPool中,現(xiàn)在回滾Prepare镣衡,還需要將preparedPool中的以Shard1:TX1為key的連接放回到普通的事務(wù)連接池霜定,這樣后面的事務(wù)請(qǐng)求可以繼續(xù)使用,避免連接泄露廊鸥。

????所以我們是需要做一些對(duì)之前Prepare操作的清理工作望浩,對(duì)每個(gè)Shard執(zhí)行RollbackPrepared操作,刪除redo log惰说,將連接從preparedPool放回事務(wù)連接池避免連接泄露磨德,。

1 func (txc *TxConn) resumeRollback(ctx context.Context, target *querypb.Target, transaction *querypb.TransactionMetadata) error {
2   err := txc.runTargets(transaction.Participants, func(t *querypb.Target) error {
3       return txc.gateway.RollbackPrepared(ctx, t, transaction.Dtid, 0)
4   })
5   if err != nil {
6       return err
7   }
8   return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)
9}

最后吆视,刪除mmShard上面記錄的事務(wù)元數(shù)據(jù)信息典挑,resumeRollback函數(shù)第9行刪除mmShard的元數(shù)據(jù)信息,結(jié)束分布式事務(wù)啦吧。

3. StartCommit異常 (32~34)

????創(chuàng)建前面提到過(guò)您觉,StartCommit就是修改分布式事務(wù)狀態(tài),這是一個(gè)原子性的操作授滓,修改事務(wù)狀態(tài)只要修改成為Committed狀態(tài)琳水,就認(rèn)為事務(wù)是提交狀態(tài)。如果狀態(tài)還是prepared狀態(tài)般堆,那么就認(rèn)為事務(wù)沒(méi)有提交在孝。
????當(dāng)然StartCommit也可以發(fā)生異常,返回錯(cuò)誤淮摔,甚至數(shù)據(jù)庫(kù)已經(jīng)修改成功浑玛,但是網(wǎng)絡(luò)或者其他原因,vtgate沒(méi)有收到噩咪,仍然需要進(jìn)行錯(cuò)誤處理顾彰。此處的錯(cuò)誤處理邏輯和CommitPrepared是一樣的。此時(shí)給客戶端返回一個(gè)錯(cuò)誤碼胃碾。

????對(duì)于這種異常的兩階段提交涨享,Vitess采取了補(bǔ)償策略。

????創(chuàng)建Vitess的策略是在每個(gè)Shard的vttablet上開(kāi)啟一個(gè)watchDog仆百。watchDog定時(shí)去去讀存儲(chǔ)在MySQL上面的分布式事務(wù)元數(shù)據(jù)信息厕隧,只有事務(wù)執(zhí)行完成才會(huì)將元數(shù)據(jù)信息刪除。
????前面我們提到俄周,mmShard記錄了事務(wù)的開(kāi)始時(shí)間吁讨,那vttablet又定義了一個(gè)事務(wù)超時(shí)時(shí)間,比如10秒峦朗。對(duì)于超過(guò)10秒未刪除的分布式事務(wù)建丧,vttablet自動(dòng)給vtgate集群(注意這里是集群,可以是任何一個(gè)vtgate)發(fā)送ResolveTransaction指令,告訴vtgate可能有事務(wù)異常了波势,需要vtgate看看咋回事翎朱,ResolveTransaction傳遞一個(gè)分布式事務(wù)id作參數(shù),比如Shard1:TX1尺铣。vtgate收到ResolveTransaction指令后拴曲,對(duì)分布式事務(wù)進(jìn)行Resolve,Resolve函數(shù)前面已經(jīng)介紹過(guò)一次凛忿,現(xiàn)在又派上用場(chǎng)了澈灼。

????回到上面的StartCommit異常,可能StartCommit已經(jīng)修改事務(wù)狀態(tài)未成功店溢,事務(wù)狀態(tài)仍然為prepare叁熔。這時(shí)候和處理prepare異常一致,RollbackPrepare逞怨。
????如果修改事務(wù)狀態(tài)成功者疤,事務(wù)狀態(tài)已經(jīng)成為Committed狀態(tài)。從Resolve函數(shù)中(27~30)可以看到叠赦,執(zhí)行resumeCommit驹马。

1 func (txc *TxConn) resumeCommit(ctx context.Context, target *querypb.Target, transaction *querypb.TransactionMetadata) error {
2   err := txc.runTargets(transaction.Participants, func(t *querypb.Target) error {
3       return txc.gateway.CommitPrepared(ctx, t, transaction.Dtid)
4   })
5   if err != nil {
6       return err
7   }
8   return txc.gateway.ConcludeTransaction(ctx, target, transaction.Dtid)
9 }

????從代碼中可以看出,resumeCommit其實(shí)就是再StartCommit的基礎(chǔ)上繼續(xù)往下執(zhí)行除秀,繼續(xù)對(duì)每個(gè)Shard執(zhí)行CommitPrepared糯累,然后執(zhí)行ConcludeTransaction刪除mmShard上的事務(wù)員數(shù)據(jù)。這時(shí)候某些Shard可能已經(jīng)成功執(zhí)行過(guò)CommitPrepared册踩,這樣可能會(huì)重復(fù)執(zhí)行泳姐。嗯,沒(méi)錯(cuò)暂吉,CommitPrepared函數(shù)是冪等的胖秒,調(diào)用一次和調(diào)用多次效果一致缎患。

4. CommitPrepared異常 (38~40)

????最后一步驟,提交事務(wù)阎肝,Shard1提交TX1挤渔,Shard2提交TX2

????此處CommitPrepared函數(shù)的參數(shù)是分布式事務(wù)id风题,即Shard1:TX1判导。Shard1和Shard2的vttablet接受到vtgate發(fā)送的rpc請(qǐng)求后根據(jù)參數(shù)分布式事務(wù)id去preparedPool里拿到TX1和TX2所使用的事務(wù)連接,然后使用該連接沛硅,也就是在TX1和TX2兩個(gè)事務(wù)中眼刃,分別刪除對(duì)于的redo log,然后提交事務(wù),同樣在TX1和TX2中刪除redo log保證了摇肌,刪除redo log和提交事務(wù)是一個(gè)原子操作擂红。

????該流程可能是commit本地事務(wù)異常,也可能是commit都成功返回成功信息時(shí)候網(wǎng)絡(luò)異常等朦蕴。

????無(wú)論何種異常gate收到的是一個(gè)錯(cuò)誤篮条,這時(shí)候,gate只能給客戶端返回一個(gè)錯(cuò)誤碼吩抓。

????對(duì)于CommitPrepared異常處理策略和上面一樣涉茧,vttablet上面的watchDog發(fā)現(xiàn)本地MySQL有記錄分布式事務(wù)超時(shí),根據(jù)發(fā)送分布式事務(wù)id到vtgate疹娶,vtgate對(duì)分布式事務(wù)進(jìn)行Resolve伴栓。

5. ConcludeTransaction異常

????ConcludeTransaction只是將mmShard的元數(shù)據(jù)刪除,刪除操作可能成功可能失敗雨饺,最終返回給客戶端一個(gè)錯(cuò)誤碼钳垮。

????對(duì)于ConcludeTransaction和上面3、4異常處理策略一樣额港。都會(huì)執(zhí)行resumeCommit饺窿。

????resumeCommit會(huì)調(diào)用冪等的CommitPrepared,然后調(diào)用ConcludeTransaction刪除mmShard上的分布式事務(wù)元數(shù)據(jù)信息移斩。

????同樣ConcludeTransaction操作也是冪等的肚医。

6. 其他異常

  • 事務(wù)長(zhǎng)時(shí)間未完成
    ????vttablet配置了分布式事務(wù)超時(shí)時(shí)間,mmShard記錄了事務(wù)開(kāi)始時(shí)間向瓷,長(zhǎng)時(shí)間未提交自動(dòng)發(fā)起Resolve肠套,避免長(zhǎng)時(shí)間未提交的事務(wù)。

  • vtgate宕機(jī)
    ????在commit2PC函數(shù)中猖任,執(zhí)行到任何一個(gè)步驟vtgate都可能宕機(jī)你稚,但是通過(guò)上面的錯(cuò)誤分析得知,對(duì)于任何異常,要么回滾要么補(bǔ)償刁赖,Vitess都可以避免部分提交搁痛。
    ????此外,vttablet需要知道協(xié)調(diào)者vtgate地址乾闰,對(duì)于長(zhǎng)時(shí)間未提交事務(wù)發(fā)起Resolve落追。vtgate是一個(gè)集群,避免單點(diǎn)故障涯肩。
    ????Resolve的參數(shù)是分布式事務(wù)id。根據(jù)分布式事務(wù)id巢钓,我們能拿到mmShard病苗。從mmShard上能讀到分布式事務(wù)的元數(shù)據(jù)信息(不管mmShard和vtgate是否發(fā)生過(guò)宕機(jī)、重啟症汹、網(wǎng)絡(luò)異常等)硫朦。有了事務(wù)的元數(shù)據(jù)信息,就可以發(fā)起Resolve操作了背镇。

  • vttablet或者M(jìn)ySQL宕機(jī)
    vttablet或者M(jìn)ySQL宕機(jī)咬展、重啟、或者發(fā)生主從切換之后瞒斩,需要從代碼邏輯上保證各個(gè)接口能夠正常服務(wù)破婆,比如下面的問(wèn)題1。

幾個(gè)小問(wèn)題

  1. 為什么我們需要一個(gè)preparedPool而不是直接復(fù)用之前的activePool胸囱?
    ????因?yàn)楫?dāng)vttablet或者M(jìn)ySQL重啟后祷舀,我們需要能夠從之前記錄的redo log中恢復(fù)回來(lái)。preparedPool是以分布式事務(wù)id作為key的烹笔。當(dāng)vttablet重啟裳扯,初始化過(guò)程就從MySQL中去取redo log(prepare步驟記錄下來(lái)的SQL就是redo log已經(jīng)對(duì)應(yīng)的分布式事務(wù)id),有redo log就說(shuō)明有分布式事務(wù)未完成谤职。然后對(duì)每個(gè)分布式事務(wù)分配一個(gè)連接饰豺,重新執(zhí)行redo log,然后將該連接放到preparedPool。這樣即便vttablet發(fā)生重啟允蜈,我們可以重新生成一個(gè)preparedPool,vttablet仍然可以執(zhí)行vtgate在Commit或者Resolve時(shí)候發(fā)送過(guò)來(lái)的CommitPrepared或者RollbackPrepared指令冤吨,從而保證分布式事務(wù)的正常執(zhí)行。

  2. vttablet發(fā)起Resolve對(duì)接口的調(diào)用和vtgate正常事務(wù)流程對(duì)接口的調(diào)用是否有可能并發(fā)陷寝?
    ????有可能锅很,vtgate第一步記錄分布式事務(wù)的元數(shù)據(jù)信息,但是事務(wù)超過(guò)10秒未完成凤跑。那么vttablet自動(dòng)發(fā)起Resolve爆安,發(fā)現(xiàn)事務(wù)未提交調(diào)用RollbackPrepared,同時(shí)有可能vtgate因?yàn)槟承┊惓R舶l(fā)起了RollbackPrepared仔引。CommitPrepared扔仓、ConcludeTransaction也有一樣的問(wèn)題褐奥。所以Resolve過(guò)程中調(diào)用的接口需要考慮到并發(fā)、冪等性等翘簇。

  3. 應(yīng)用端執(zhí)行commit返回錯(cuò)誤就是事務(wù)提交失敗么撬码?
    ????不一定,通過(guò)前面的例子我們可以看到版保,可能兩階段提交前面都成功了呜笑,只是最后刪除mmShard上面的元數(shù)據(jù)失敗了,還是會(huì)返回給應(yīng)用端一個(gè)error彻犁。
    甚至所有步驟都成功了叫胁,要正常返回給應(yīng)用端OK包了,斷網(wǎng)了汞幢,應(yīng)用端也是會(huì)收到網(wǎng)絡(luò)異常錯(cuò)誤驼鹅,這就需要應(yīng)用端有合適的補(bǔ)償機(jī)制處理這種異常。

  4. Vitess中的兩階段提交事務(wù)滿足文章開(kāi)頭提到的事務(wù)四個(gè)特性么森篷?
    ????不滿足输钩,Vitess的兩階段提交只是保證了分布式事務(wù)的原子性,即便使用兩階段提交仲智,在Vitess中是有可能讀取到部分提交結(jié)果的买乃。 但是兩階段提交是分布式中經(jīng)典問(wèn)題,也是最基礎(chǔ)的算法坎藐,幾乎所有的復(fù)雜的分布式算法都會(huì)使用到兩階段提交为牍。

  5. Vitess中只有兩階段提交一種事務(wù)模型么?
    ????不是岩馍,除了兩階段提交外碉咆,Vitess還支持單節(jié)點(diǎn)事務(wù),單節(jié)點(diǎn)事務(wù)強(qiáng)制要求事務(wù)只能在一個(gè)Shard執(zhí)行蛀恩;還有多節(jié)點(diǎn)事務(wù)疫铜,多節(jié)點(diǎn)事務(wù)對(duì)部分提交問(wèn)題不做處理。

  6. Vitess中兩階段提交性能如何?
    ????Vitess兩階段提交比一般的多節(jié)點(diǎn)事務(wù)會(huì)多四次vtgate到vttablet的交互双谆,我的測(cè)試結(jié)果兩階段提交帶來(lái)的延遲在5毫秒以內(nèi)壳咕。

  7. 線上的交易訂單系統(tǒng)真是例子中這樣的么?
    ????復(fù)雜的多顽馋,首先訂單和白條可能都不在同一個(gè)大部門(mén)谓厘,比如訂單業(yè)務(wù)屬于商城交易部門(mén),白條業(yè)務(wù)屬于金融部門(mén)寸谜。各個(gè)服務(wù)間可能通過(guò)WebService或者mq交互竟稳。其次業(yè)務(wù)也要復(fù)雜的多,一個(gè)下單操作可能涉及到幾十個(gè)甚至上百個(gè)接口的調(diào)用,各個(gè)接口能夠穩(wěn)定的提供服務(wù)最主要的原因是依賴于基礎(chǔ)平臺(tái)的各種中間件他爸,好了編不下去了聂宾,其實(shí)我不知道??。

  8. 不是說(shuō)好的兩階段提交么诊笤,Vitess為啥分了好幾個(gè)階段啊系谐,這還是正經(jīng)兩階段提交么?
    ????兩階段指的是協(xié)調(diào)者和參與者之間的交互主要分兩個(gè)階段讨跟。
    ????兩階段提交第一步纪他,在提交之前協(xié)調(diào)者vtgate向所有參與者vttablet發(fā)出是否可提交的詢問(wèn),CreateTransaction相當(dāng)于先記錄事務(wù)狀態(tài)晾匠,然后向mmShard發(fā)起了是否可提交的詢問(wèn)止喷,Prepare請(qǐng)求相當(dāng)于向除mmShard之外的其他參與者發(fā)起了詢問(wèn)。
    ????兩階段提交第二步混聊,是如果所有參與都答復(fù)可以正常提交,那么乾巧,協(xié)調(diào)者給所有參與者發(fā)出提交指令句喜。Vitess中詢問(wèn)之后得到了可以提交的答復(fù),首先通過(guò)StartCommit將狀態(tài)記錄下了沟于,這樣可以避免協(xié)調(diào)組vtgate異常導(dǎo)致分布式事務(wù)異常咳胃。記錄可以提交狀態(tài)之后,協(xié)調(diào)者vtgate向所有參與者發(fā)出提交指令CommitPrepared旷太。Vitess做的一點(diǎn)優(yōu)化是元數(shù)據(jù)記錄在了mmShard,所以在記錄事務(wù)已經(jīng)提交的步驟中展懈,直接把mmShard上的事務(wù)提交了。
    ????兩階段提交上面已經(jīng)完成了供璧,ConcludeTransaction是做了一些清理元數(shù)據(jù)的工作存崖。
    ????翻了一下事務(wù)處理,在講兩階段提交過(guò)程有明確指出[P14]睡毒,
    ????對(duì)于參與者的答復(fù)来惧,事務(wù)管理器將在日志中記錄下這個(gè)事實(shí)。
    ????Vitess做法是將這個(gè)狀態(tài)記錄到了mmShard演顾。對(duì)于資源管理器重啟供搀,事務(wù)處理[P14]中也提到:
    ????作為重啟邏輯的一部分,資源管理器向事務(wù)管理器發(fā)出詢問(wèn)钠至,這時(shí)候事務(wù)管理器告訴他們 發(fā)生故障時(shí)每一個(gè)活動(dòng)的事務(wù)的執(zhí)行結(jié)果葛虐。一些可能提交了,一些可能終止了棉钧,一些可能仍在提交過(guò)程中屿脐。資源管理器可以獨(dú)立的恢復(fù)其已提交的狀態(tài),也可以參與事務(wù)管理器對(duì)日志重做或者撤銷的檢測(cè)。
    ????Vitess也是資源管理器vttablet向事務(wù)管理器發(fā)出詢問(wèn)摄悯,只不過(guò)事務(wù)管理器維護(hù)的事務(wù)日志信息還是放到了資源管理器赞季,因?yàn)橘Y源管理器有MySQL啊,正好存儲(chǔ)事務(wù)信息不會(huì)丟奢驯。
    ????所以Vitess的兩階段提交是正經(jīng)的兩階段提交申钩。而沒(méi)有實(shí)現(xiàn)上面兩點(diǎn)的兩階段提交是不能從錯(cuò)誤正常恢復(fù)瘪阁,也就不能保證原子性撒遣。

參考:
????https://github.com/vitessio/vitess/blob/master/doc/TwoPhaseCommitDesign.md

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市管跺,隨后出現(xiàn)的幾起案子义黎,更是在濱河造成了極大的恐慌,老刑警劉巖豁跑,帶你破解...
    沈念sama閱讀 211,348評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件廉涕,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡艇拍,警方通過(guò)查閱死者的電腦和手機(jī)狐蜕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,122評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)卸夕,“玉大人层释,你說(shuō)我怎么就攤上這事】旒” “怎么了贡羔?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,936評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)个初。 經(jīng)常有香客問(wèn)我乖寒,道長(zhǎng),這世上最難降的妖魔是什么勃黍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,427評(píng)論 1 283
  • 正文 為了忘掉前任宵统,我火速辦了婚禮,結(jié)果婚禮上覆获,老公的妹妹穿的比我還像新娘马澈。我一直安慰自己,他們只是感情好弄息,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,467評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布痊班。 她就那樣靜靜地躺著,像睡著了一般摹量。 火紅的嫁衣襯著肌膚如雪涤伐。 梳的紋絲不亂的頭發(fā)上馒胆,一...
    開(kāi)封第一講書(shū)人閱讀 49,785評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音凝果,去河邊找鬼祝迂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛器净,可吹牛的內(nèi)容都是我干的型雳。 我是一名探鬼主播,決...
    沈念sama閱讀 38,931評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼山害,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼纠俭!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起浪慌,我...
    開(kāi)封第一講書(shū)人閱讀 37,696評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤冤荆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后权纤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體钓简,經(jīng)...
    沈念sama閱讀 44,141評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,483評(píng)論 2 327
  • 正文 我和宋清朗相戀三年汹想,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了涌庭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,625評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡欧宜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出拴魄,到底是詐尸還是另有隱情冗茸,我是刑警寧澤,帶...
    沈念sama閱讀 34,291評(píng)論 4 329
  • 正文 年R本政府宣布匹中,位于F島的核電站夏漱,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏顶捷。R本人自食惡果不足惜挂绰,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,892評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望服赎。 院中可真熱鬧葵蒂,春花似錦、人聲如沸重虑。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)缺厉。三九已至永高,卻和暖如春隧土,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背命爬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工曹傀, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人饲宛。 一個(gè)月前我還...
    沈念sama閱讀 46,324評(píng)論 2 360
  • 正文 我出身青樓皆愉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親落萎。 傳聞我的和親對(duì)象是個(gè)殘疾皇子亥啦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,492評(píng)論 2 348

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