面試不懂分布式鎖瞒斩?看這篇文章就夠了

背景

分布式鎖,是指在分布式的部署環(huán)境下涮总,通過鎖機(jī)制來讓多客戶端互斥的對(duì)共享資源進(jìn)行訪問胸囱。

分布式鎖要滿足哪些要求呢?

  • 排他性:在同一時(shí)間只會(huì)有一個(gè)客戶端能獲取到鎖瀑梗,其它客戶端無法同時(shí)獲取

  • 避免死鎖:這把鎖在一段有限的時(shí)間之后旺矾,一定會(huì)被釋放(正常釋放或異常釋放)

  • 高可用:獲取或釋放鎖的機(jī)制必須高可用且性能佳

分布式鎖的實(shí)現(xiàn)方式

目前主流的有三種,從實(shí)現(xiàn)的復(fù)雜度上來看夺克,從上往下難度依次增加:

  • 基于數(shù)據(jù)庫實(shí)現(xiàn)

  • 基于Redis實(shí)現(xiàn)

  • 基于ZooKeeper實(shí)現(xiàn)

無論哪種方式,其實(shí)都不完美嚎朽,依舊要根據(jù)咱們業(yè)務(wù)的實(shí)際場(chǎng)景來選擇铺纽。

為什么用分布式鎖?

在討論這個(gè)問題之前哟忍,我們先來看一個(gè)業(yè)務(wù)場(chǎng)景:

系統(tǒng)A是一個(gè)電商系統(tǒng)狡门,目前是一臺(tái)機(jī)器部署,系統(tǒng)中有一個(gè)用戶下訂單的接口锅很,該接口的流程是用戶下訂單之前一定要去檢查一下庫存其馏,確保庫存足夠了才會(huì)給用戶下單

由于系統(tǒng)有一定的并發(fā)爆安,所以會(huì)預(yù)先將商品的庫存保存在redis中叛复,用戶下單的時(shí)候會(huì)更新redis的庫存。

此時(shí)系統(tǒng)架構(gòu)如下:

image

但是這樣一來會(huì)產(chǎn)生一個(gè)問題:假如某個(gè)時(shí)刻扔仓,redis里面的某個(gè)商品庫存為1褐奥,此時(shí)兩個(gè)請(qǐng)求同時(shí)到來,其中一個(gè)請(qǐng)求執(zhí)行到上圖的第3步翘簇,更新數(shù)據(jù)庫的庫存為0撬码,但是第4步還沒有執(zhí)行。

而另外一個(gè)請(qǐng)求執(zhí)行到了第2步版保,發(fā)現(xiàn)庫存還是1呜笑,就繼續(xù)執(zhí)行第3步夫否。

這樣的結(jié)果,是導(dǎo)致賣出了2個(gè)商品叫胁,然而其實(shí)庫存只有1個(gè)凰慈。

很明顯不對(duì)啊曹抬!這就是典型的\color{green}{庫存超賣}問題溉瓶,產(chǎn)生這種問題實(shí)質(zhì)是2 3 4步驟不是原子操作

此時(shí),我們很容易想到解決方案:用鎖把2谤民、3堰酿、4步鎖住(其實(shí)是把2步驟鎖渍抛恪)触创,讓他們執(zhí)行完4步驟之后把鎖解開,另一個(gè)線程才能進(jìn)來執(zhí)行第2步为牍。這里又會(huì)涉及到另一個(gè)問題哼绑,數(shù)據(jù)庫和緩存一致性的問題,即3 4步驟碉咆,這里不做討論抖韩。

image

按照上面的圖,在執(zhí)行第2步時(shí)疫铜,使用Java提供的synchronized或者ReentrantLock來鎖住茂浮,然后在第4步執(zhí)行完之后才釋放鎖。

這樣一來壳咕,2席揽、3、4 這3個(gè)步驟就被“鎖”住了谓厘,多個(gè)線程之間只能串行化執(zhí)行幌羞。

但是好景不長(zhǎng),整個(gè)系統(tǒng)的并發(fā)飆升竟稳,一臺(tái)機(jī)器扛不住了∈翳耄現(xiàn)在要增加一臺(tái)機(jī)器,如下圖:

image

增加機(jī)器之后住练,系統(tǒng)變成上圖所示地啰,我的天!

假設(shè)此時(shí)兩個(gè)用戶的請(qǐng)求同時(shí)到來讲逛,但是落在了不同的機(jī)器上亏吝,那么這兩個(gè)請(qǐng)求是可以同時(shí)執(zhí)行了,還是會(huì)出現(xiàn)庫存超賣的問題盏混。

為什么呢蔚鸥?因?yàn)樯蠄D中的兩個(gè)A系統(tǒng)惜论,運(yùn)行在兩個(gè)不同的JVM里面,他們加的鎖只對(duì)屬于自己JVM里面的線程有效止喷,對(duì)于其他JVM的線程是無效的馆类。

因此,這里的問題是:Java提供的原生鎖機(jī)制在多機(jī)部署場(chǎng)景下失效了

這是因?yàn)閮膳_(tái)機(jī)器加的鎖不是同一個(gè)鎖(兩個(gè)鎖在不同的JVM里面)弹谁。

那么乾巧,我們只要保證兩臺(tái)機(jī)器加的鎖是同一個(gè)鎖,問題不就解決了嗎预愤?

此時(shí)沟于,就該分布式鎖隆重登場(chǎng)了,分布式鎖的思路是:

在整個(gè)系統(tǒng)提供一個(gè)全局植康、唯一的獲取鎖的“東西”旷太,然后每個(gè)系統(tǒng)在需要加鎖時(shí),都去問這個(gè)“東西”拿到一把鎖销睁,這樣不同的系統(tǒng)拿到的就可以認(rèn)為是同一把鎖供璧。

至于這個(gè)“東西”,可以是Redis冻记、Zookeeper睡毒,也可以是數(shù)據(jù)庫。

文字描述不太直觀冗栗,我們來看下圖:

image

通過上面的分析吕嘀,我們知道了庫存超賣場(chǎng)景在分布式部署系統(tǒng)的情況下使用Java原生的鎖機(jī)制無法保證線程安全,所以我們需要用到分布式鎖的方案贞瞒。

那么,如何實(shí)現(xiàn)分布式鎖呢趁曼?接著往下看军浆!

基于數(shù)據(jù)庫實(shí)現(xiàn)分布式鎖(悲觀鎖)

拿換領(lǐng)碼換領(lǐng)優(yōu)惠券按理來說

  1. 查該碼狀態(tài)是否已經(jīng)被換取,如果未換取執(zhí)行下一步
  2. 換取優(yōu)惠券
  3. 更新該碼狀態(tài)

看似邏輯很正常挡闰,但如果用戶同一時(shí)間并發(fā)提交換領(lǐng)碼乒融,在沒有加鎖限制的情況下,用戶則可以使用同一個(gè)換領(lǐng)碼同時(shí)兌換到多張優(yōu)惠券摄悯。比如用戶A在執(zhí)行完第二步但還沒執(zhí)行第三步時(shí)赞季,用戶B也使用該碼進(jìn)行兌換,在第一步是查詢到的是該碼未被兌換奢驯。

所以在第一步的時(shí)候就要加鎖申钩。為代碼如下

start transaction;
//加排他鎖,有別的線程并發(fā)讀的時(shí)候瘪阁,此處就處于加鎖狀態(tài)(阻塞狀態(tài))
select state from coupon where number='11111143' for update;
....
換取優(yōu)惠券
....
//更新狀態(tài)碼狀態(tài)
update coupon set state=1 where number='1111143';

這樣在第一步查詢時(shí)就加鎖撒遣,這樣就可以防止同一碼被多次兌換邮偎。如果使用正常的select state from coupon where number='11111143',在mvcc中讀是不加鎖的。如果使用共享鎖select state from coupon where number='11111143' lock in share mode的話义黎,并發(fā)時(shí)其他線程也是可以獲得該共享鎖的禾进。所以這里只能用排他鎖。

總結(jié):如果第一步查詢時(shí)是使用redis查詢的話廉涕,就使用redis來實(shí)現(xiàn)分布式鎖泻云,如果是數(shù)據(jù)庫查詢時(shí)就可以使用數(shù)據(jù)庫鎖來實(shí)現(xiàn)。

基于Redis實(shí)現(xiàn)分布式鎖

上面分析為啥要使用分布式鎖了狐蜕,這里我們來具體看看分布式鎖落地的時(shí)候應(yīng)該怎么樣處理宠纯。

最常見的一種方案就是使用Redis做分布式鎖

使用Redis做分布式鎖的思路大概是這樣的:在redis中設(shè)置一個(gè)值表示加了鎖缅叠,然后釋放鎖的時(shí)候就把這個(gè)key刪除渡紫。

具體代碼是這樣的:


這種方式有幾大要點(diǎn):

  • 一定要用使用SET key value NX PX milliseconds 命令(原子操作)

    如果不用這種方式奕翔,采用先設(shè)置了值降盹,再設(shè)置過期時(shí)間护戳,這個(gè)不是原子性操作拴疤,有可能在設(shè)置過期時(shí)間之前宕機(jī)迈套,會(huì)造成死鎖(key永久存在)甲脏。

    這種方式一定要設(shè)置過期時(shí)間治力,同樣也是避免死鎖蒙秒。如果沒設(shè)置過期時(shí)間的情況下,機(jī)器(不是redis服務(wù)器宵统,指api服務(wù)器)獲取鎖成功晕讲,但是刪除鎖時(shí)機(jī)器宕機(jī),這是造成死鎖马澈,其他線程將一直獲取不到該鎖瓢省。

  • value要具有唯一性

    這個(gè)是為了在解鎖的時(shí)候,需要驗(yàn)證value是和加鎖的一致才刪除key痊班。

    這是避免了一種情況:假設(shè)A獲取了鎖勤婚,過期時(shí)間30s,此時(shí)35s之后涤伐,鎖已經(jīng)自動(dòng)釋放了馒胆,A去釋放鎖,但是此時(shí)可能B獲取了鎖凝果。A客戶端就不能刪除B的鎖了祝迂。

    如果真出現(xiàn)A超過過期時(shí)間還沒有執(zhí)行完的情況,這里也會(huì)出現(xiàn)一個(gè)很嚴(yán)重的問題器净,也就是線程安全問題型雳,同樣會(huì)出現(xiàn)上述線程超賣的情況。所以要使用這種方式的話,過期時(shí)間一定要合理控制四啰。

image

除了要考慮客戶端要怎么實(shí)現(xiàn)分布式鎖之外宁玫,還需要考慮redis的部署問題。

redis有3種部署方式:

  • 單機(jī)模式

  • master-slave + sentinel選舉模式

  • redis cluster模式

使用redis做分布式鎖的缺點(diǎn)在于:如果采用單機(jī)部署模式柑晒,會(huì)存在單點(diǎn)問題欧瘪,只要redis故障了。加鎖就不行了匙赞。

采用master-slave模式佛掖,加鎖的時(shí)候只對(duì)一個(gè)節(jié)點(diǎn)加鎖,即便通過sentinel做了高可用涌庭,但是如果master節(jié)點(diǎn)故障了芥被,發(fā)生主從切換,此時(shí)就會(huì)有可能出現(xiàn)鎖丟失的問題坐榆。

如果采用redis cluster模式拴魄,同樣會(huì)有上述問題,加鎖會(huì)先對(duì)集群中的某個(gè)主節(jié)點(diǎn)加鎖席镀,然后異步復(fù)制到其備節(jié)點(diǎn)匹中。如果還沒復(fù)制到備節(jié)點(diǎn)時(shí),主節(jié)點(diǎn)宕機(jī)豪诲,這時(shí)備節(jié)點(diǎn)上選舉為主節(jié)點(diǎn)顶捷。這把鎖已經(jīng)丟失。

所以這種方式有以下倆個(gè)缺點(diǎn):

  • 會(huì)有鎖丟失的問題
  • set key value nx px 3000 如果3000毫秒之后業(yè)務(wù)邏輯還沒執(zhí)行完屎篱,key過期服赎,鎖釋放。其他線程獲取到鎖交播。這樣一來也會(huì)造成線程安全問題重虑。

php代碼實(shí)現(xiàn)如下:模擬下單間庫存,避免庫存超賣現(xiàn)象

public function lock(Request $request, Response $response)
    {
        $data = $this->validate($request, [
            'shop_id' => 'required',
            'num' => 'required'
        ]);
        $lockState = false;
        $lockValue = microtime(true);
        while (!$lockState) {
            //加鎖 set aaa 2324242 px 60000 nx
            $lockState = Redis::set($data['shop_id'] . "lock", $lockValue, 'px', 60000, 'nx');
            //避免重復(fù)無用循環(huán)秦士,浪費(fèi)cpu等資源
            sleep(1)
        }
        //獲取庫存緩存,要預(yù)熱嚎尤,或者后臺(tái)添加商品時(shí)添加緩存
        $inventory = Redis::get($data['shop_id'] . "sku");
        if ($inventory <= 0) {
            //庫存為0 已經(jīng)賣完
            return $response->setStatusCode(401);
        }
        // 下單


        //修改庫存,涉及到雙寫一致問題。
        //本次雙寫使用的先更新數(shù)據(jù)庫在更新緩存的策略伍宦,這種策略通過需要加事務(wù)來避免數(shù)據(jù)不一致性。
        //但是代碼上面已經(jīng)加上了分布式鎖乏梁,也就是整個(gè)過程其實(shí)只能有一個(gè)線程來執(zhí)行了次洼,不會(huì)出現(xiàn)上面提到的數(shù)據(jù)不一致性問題了,所以這里不加事務(wù)也是可以的遇骑。
        try {
            //加事務(wù)
            DB::beginTransaction();
            //修改數(shù)據(jù)庫庫存
            $updateInventory = Shop::where('id', $data['shop_id'])->decrement('inventory', $data['num']);
            if ($updateInventory) {
                //庫存存入
                Redis::decrby($data['shop_id'] . "sku", $data['num'])卖毁;
            }
            DB::commit();
            //解鎖 用到了get和delete操作 使用lua腳本
            Redis::eval(file_get_contents(storage_path("app/lock.lua")), 1, $data['shop_id'] . "lock", $lockValue);
        
        } catch (\Exception $e) {
            //解鎖 用到了get和delete操作 使用lua腳本
            Redis::eval(file_get_contents(storage_path("app/lock.lua")), 1, $data['shop_id'] . "lock", $lockValue);
            $this->addError('sku error', ['error' => $e->getMessage()]);
            DB::rollBack();
        }
        

    }

lua腳本如下

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

要注意:獲取鎖是一個(gè)阻塞的過程,如果鎖被線程A占用,這時(shí)線程B來獲取時(shí)會(huì)一直處于阻塞狀態(tài)亥啦,直到線程A釋放鎖時(shí)線程B才會(huì)獲取鎖炭剪。

使用這種方法需要注意幾個(gè)問題:

1. 鎖必須要設(shè)置一個(gè)過期時(shí)間

否則的話翔脱,當(dāng)一個(gè)客戶端獲取鎖成功之后奴拦,假如它崩潰了,或者由于發(fā)生了網(wǎng)絡(luò)分割(network partition)導(dǎo)致它再也無法和Redis節(jié)點(diǎn)通信了届吁,那么它就會(huì)一直持有這個(gè)鎖错妖,而其它客戶端永遠(yuǎn)無法獲得鎖了。antirez在后面的分析中也特別強(qiáng)調(diào)了這一點(diǎn)疚沐,而且把這個(gè)過期時(shí)間稱為鎖的有效時(shí)間(lock validity time)暂氯。獲得鎖的客戶端必須在這個(gè)時(shí)間之內(nèi)完成對(duì)共享資源的訪問。

分布式鎖的過期時(shí)間的長(zhǎng)短非常重要亮蛔,設(shè)置太長(zhǎng)影響系統(tǒng)性能痴施,設(shè)置太短會(huì)導(dǎo)致鎖提前釋放,整體加鎖失敗究流。

那么如果在鎖馬上要釋放時(shí)辣吃,邏輯還沒有執(zhí)行完,對(duì)于這種情況該怎么應(yīng)對(duì)梯嗽?答案是續(xù)鎖齿尽。

其實(shí)設(shè)置的加鎖時(shí)間是整體邏輯的最大耗時(shí)了,比如邏輯正常執(zhí)行完平均時(shí)間是3s灯节,那么我們一般會(huì)設(shè)置加鎖時(shí)間大于3s循头,設(shè)置為5s或者6s。
但是不排除特殊情況下導(dǎo)致整體邏輯耗時(shí)加長(zhǎng)炎疆,導(dǎo)致邏輯執(zhí)行時(shí)間大于設(shè)置的過期時(shí)間了卡骂。

此時(shí)看下續(xù)鎖的操作是如何進(jìn)行的,假設(shè)鎖的過期時(shí)間設(shè)置為5s,正常情況下邏輯執(zhí)行完為3s形入。

2. 設(shè)置鎖和設(shè)置鎖的過期時(shí)間必須時(shí)原子性的全跨。

第一步獲取鎖的操作,網(wǎng)上不少文章把它實(shí)現(xiàn)成了兩個(gè)Redis命令:

SETNX resource_name my_random_value
EXPIRE resource_name 30

雖然這兩個(gè)命令和前面算法描述中的一個(gè)SET命令執(zhí)行效果相同亿遂,但卻不是原子的浓若。如果客戶端在執(zhí)行完SETNX后崩潰了,那么就沒有機(jī)會(huì)執(zhí)行EXPIRE了蛇数,導(dǎo)致它一直持有這個(gè)鎖挪钓。

3. 設(shè)置鎖的值時(shí)的唯一性是很有必要的。

它保證了一個(gè)客戶端釋放的鎖必須是自己持有的那個(gè)鎖耳舅。假如獲取鎖時(shí)SET的不是一個(gè)隨機(jī)字符串碌上,而是一個(gè)固定值,那么可能會(huì)發(fā)生下面的執(zhí)行序列:

  • 客戶端1獲取鎖成功。

  • 客戶端1在某個(gè)操作上阻塞了很長(zhǎng)時(shí)間馏予。

  • 過期時(shí)間到了天梧,鎖自動(dòng)釋放了。

  • 客戶端2獲取到了對(duì)應(yīng)同一個(gè)資源的鎖霞丧。

  • 客戶端1從阻塞中恢復(fù)過來呢岗,釋放掉了客戶端2持有的鎖。

之后蚯妇,客戶端2在訪問共享資源的時(shí)候敷燎,就沒有鎖為它提供保護(hù)了。

4. 釋放鎖的操作必須使用Lua腳本來實(shí)現(xiàn)箩言。

釋放鎖其實(shí)包含三步操作:'GET'硬贯、判斷和'DEL',用Lua腳本來實(shí)現(xiàn)能保證這三步的原子性陨收。否則饭豹,如果把這三步操作放到客戶端邏輯中去執(zhí)行的話,就有可能發(fā)生與前面第三個(gè)問題類似的執(zhí)行序列:

  • 客戶端1獲取鎖成功务漩。
  • 客戶端1訪問共享資源拄衰。

  • 客戶端1為了釋放鎖,先執(zhí)行'GET'操作獲取隨機(jī)字符串的值饵骨。

  • 客戶端1判斷隨機(jī)字符串的值翘悉,與預(yù)期的值相等。

  • 客戶端1由于某個(gè)原因阻塞住了很長(zhǎng)時(shí)間居触。

  • 過期時(shí)間到了妖混,鎖自動(dòng)釋放了。

  • 客戶端2獲取到了對(duì)應(yīng)同一個(gè)資源的鎖轮洋。

*客戶端1從阻塞中恢復(fù)過來制市,執(zhí)行DEL操縱,釋放掉了客戶端2持有的鎖弊予。

實(shí)際上祥楣,在上述第三個(gè)問題和第四個(gè)問題的分析中,如果不是客戶端阻塞住了汉柒,而是出現(xiàn)了大的網(wǎng)絡(luò)延遲误褪,也有可能導(dǎo)致類似的執(zhí)行序列發(fā)生。



基于以上的考慮碾褂,其實(shí)redis的作者也考慮到這個(gè)問題兽间,他提出了一個(gè)RedLock算法

假設(shè)redis的部署模式是redis cluster,總共有5個(gè)master節(jié)點(diǎn)斋扰,通過以下步驟獲取一把鎖:

  • 獲取當(dāng)前時(shí)間戳,單位是毫秒

  • 它嘗試按順序獲取所有5個(gè)實(shí)例中的鎖,在所有實(shí)例中使用相同的鍵名和隨機(jī)值传货,并設(shè)置一個(gè)鎖的過期時(shí)間屎鳍。在步驟2期間,客戶端使用與鎖的過期時(shí)間相比較小的時(shí)間以獲取鎖问裕,這個(gè)時(shí)間叫做的鎖的獲取時(shí)間逮壁。例如,如果過期時(shí)間是10秒粮宛,則鎖的獲取時(shí)間可以在~5-50毫秒范圍內(nèi)(如果超時(shí)則獲取失敗窥淆,從而嘗試獲取下個(gè)鎖)。這可以防止客戶端長(zhǎng)時(shí)間保持阻塞巍杈,試圖與Redis節(jié)點(diǎn)進(jìn)行通信忧饭,如果實(shí)例不可用,我們應(yīng)該嘗試盡快與下一個(gè)實(shí)例通話筷畦。

  • 客戶端通過從當(dāng)前時(shí)間中減去在步驟1中獲得的時(shí)間戳來計(jì)算鎖的獲取時(shí)間词裤。當(dāng)且僅當(dāng)客戶端能夠在大多數(shù)實(shí)例中獲取鎖時(shí)(至少3個(gè))并且獲取鎖時(shí)間小于鎖定鎖的過期時(shí)間,認(rèn)為鎖定被獲取鳖宾。

  • 如果獲得了鎖吼砂,則其有效時(shí)間被認(rèn)為是鎖的過期時(shí)間減去獲取鎖時(shí)間,如步驟3中計(jì)算的鼎文。

  • 如果客戶端由于某種原因(無法鎖定5 / 2 + 1實(shí)例或有效時(shí)間為負(fù))未能獲取鎖定渔肩,它將嘗試解鎖所有實(shí)例(即使它認(rèn)為不是能夠鎖定)。

  • 只要?jiǎng)e人建立了一把分布式鎖拇惋,你就得不斷輪詢?nèi)L試獲取鎖

當(dāng)然周偎,上面描述的只是獲取鎖的過程,而釋放鎖的過程比較簡(jiǎn)單:客戶端向所有Redis節(jié)點(diǎn)發(fā)起釋放鎖的操作蚤假,不管這些節(jié)點(diǎn)當(dāng)時(shí)在獲取鎖的時(shí)候成功與否栏饮。
具體步驟可參考redis 官網(wǎng)

但是這樣的這種算法還是頗具爭(zhēng)議的,可能還會(huì)存在不少的問題磷仰,無法保證加鎖的過程一定正確袍嬉。

image

redLock鎖真的萬無一失嗎?下面是需要思考的幾個(gè)問題

  • 未解決執(zhí)行時(shí)間大于過期時(shí)間問題
    單節(jié)點(diǎn)時(shí)存在如果線程A的執(zhí)行時(shí)間或者發(fā)生阻塞以至于大于鎖的加鎖時(shí)間灶平,這時(shí)線程A還沒有執(zhí)行完伺通,由于鎖已經(jīng)自動(dòng)釋放,這時(shí)線程B獲得鎖逢享。同一資源被倆線程執(zhí)行罐监,出現(xiàn)安全問題。redLock同樣未解決該問題瞒爬,所以鎖的過期時(shí)間時(shí)一個(gè)很關(guān)鍵的問題弓柱。

  • RedLock性能及崩潰恢復(fù)的相關(guān)解決方法
    假設(shè)一共有5個(gè)Redis節(jié)點(diǎn):A, B, C, D, E沟堡。設(shè)想發(fā)生了如下的事件序列:

    1. 客戶端1成功鎖住了A, B, C,獲取鎖成功(但D和E沒有鎖资缚铡)航罗。

    2. 節(jié)點(diǎn)C崩潰重啟了,但客戶端1在C上加的鎖沒有持久化下來屁药,丟失了粥血。

    3. 節(jié)點(diǎn)C重啟后,客戶端2鎖住了C, D, E酿箭,獲取鎖成功复亏。

    這樣,客戶端1和客戶端2同時(shí)獲得了鎖(針對(duì)同一資源)缭嫡。在默認(rèn)情況下缔御,Redis的AOF持久化方式是每秒寫一次磁盤(即執(zhí)行fsync),因此最壞情況下可能丟失1秒的數(shù)據(jù)械巡。為了盡可能不丟數(shù)據(jù)刹淌,Redis允許設(shè)置成每次修改數(shù)據(jù)都進(jìn)行fsync,但這會(huì)降低性能讥耗。當(dāng)然有勾,即使執(zhí)行了fsync也仍然有可能丟失數(shù)據(jù)(這取決于系統(tǒng)而不是Redis的實(shí)現(xiàn))。所以古程,上面分析的由于節(jié)點(diǎn)重啟引發(fā)的鎖失效問題蔼卡,總是有可能出現(xiàn)的。為了應(yīng)對(duì)這一問題挣磨,antirez又提出了延遲重啟(delayed restarts)的概念雇逞。也就是說,一個(gè)節(jié)點(diǎn)崩潰后茁裙,先不立即重啟它塘砸,而是等待一段時(shí)間再重啟,這段時(shí)間應(yīng)該大于鎖的有效時(shí)間(lock validity time)晤锥。這樣的話掉蔬,這個(gè)節(jié)點(diǎn)在重啟前所參與的鎖都會(huì)過期,它在重啟后就不會(huì)對(duì)現(xiàn)有的鎖造成影響矾瘾。

  • 基于故障轉(zhuǎn)移實(shí)現(xiàn)的redis主從無法真正實(shí)現(xiàn)Redlock

    假設(shè)redis cluster同樣有5個(gè)master節(jié)點(diǎn)女轿,A, B, C, D, E。設(shè)想發(fā)生了如下的事件序列:

    1. 客戶端1成功鎖住了A, B, C壕翩,獲取鎖成功(但D和E沒有鎖昨燃!)。

    2. 節(jié)點(diǎn)C崩潰,并且崩潰前鎖未同步到slave節(jié)點(diǎn)鎖放妈,slave節(jié)點(diǎn)升級(jí)為master節(jié)點(diǎn)北救,這時(shí)該master節(jié)點(diǎn)并沒有加鎖荐操。

    3. 這時(shí)客戶端2鎖住了C, D, E,獲取鎖成功珍策。

    這樣淀零,客戶端1和客戶端2同時(shí)獲得了鎖(針對(duì)同一資源),導(dǎo)致資源共享膛壹,沒有進(jìn)行互斥。這個(gè)失敗的原因是因?yàn)閺膔edis立刻升級(jí)為主redis唉堪,如果能夠過TTL時(shí)間再升級(jí)為主redis(延遲升級(jí))后模聋,或者立刻升級(jí)為主redis的節(jié)點(diǎn)只有過TTL的時(shí)間后再執(zhí)行獲取鎖的任務(wù),就能成功產(chǎn)生互斥效果唠亚;是不是這樣就能實(shí)現(xiàn)基于redis主從的Redlock了

RedLock算法是否是異步算法链方?

可以看成是同步算法;因?yàn)?即使進(jìn)程間(多個(gè)電腦間)沒有同步時(shí)鐘灶搜,但是每個(gè)進(jìn)程時(shí)間流速大致相同祟蚀;并且時(shí)鐘漂移相對(duì)于TTL叫小,可以忽略割卖,所以可以看成同步算法前酿;(不夠嚴(yán)謹(jǐn),算法上要算上時(shí)鐘漂移鹏溯,因?yàn)槿绻麅蓚€(gè)電腦在地球兩端罢维,則時(shí)鐘漂移非常大)

RedLock失敗重試

當(dāng)client不能獲取鎖時(shí),應(yīng)該在隨機(jī)時(shí)間后重試獲取鎖丙挽;并且最好在同一時(shí)刻并發(fā)的把set命令發(fā)送給所有redis實(shí)例肺孵;而且對(duì)于已經(jīng)獲取鎖的client在完成任務(wù)后要及時(shí)釋放鎖,這是為了節(jié)省時(shí)間颜阐;

RedLock釋放鎖

由于釋放鎖時(shí)會(huì)判斷這個(gè)鎖的value是不是自己設(shè)置的平窘,如果是才刪除;所以在釋放鎖時(shí)非常簡(jiǎn)單凳怨,只要向所有實(shí)例都發(fā)出釋放鎖的命令瑰艘,不用考慮能否成功釋放鎖;

RedLock注意點(diǎn)(Safety arguments):

1.先假設(shè)client獲取所有實(shí)例猿棉,所有實(shí)例包含相同的key和過期時(shí)間(TTL) ,但每個(gè)實(shí)例set命令時(shí)間不同導(dǎo)致不能同時(shí)過期磅叛,第一個(gè)set命令之前是T1,最后一個(gè)set命令后為T2,則此client有效獲取鎖的最小時(shí)間為TTL-(T2-T1)-時(shí)鐘漂移;

2.對(duì)于以N/2+ 1(也就是一半以 上)的方式判斷獲取鎖成功,是因?yàn)槿绻∮谝话肱袛酁槌晒Φ脑捜蓿锌赡艹霈F(xiàn)多個(gè)client都成功獲取鎖的情況弊琴, 從而使鎖失效

3.一個(gè)client鎖定大多數(shù)事例耗費(fèi)的時(shí)間大于或接近鎖的過期時(shí)間,就認(rèn)為鎖無效杖爽,并且解鎖這個(gè)redis實(shí)例(不執(zhí)行業(yè)務(wù)) ;只要在TTL時(shí)間內(nèi)成功獲取一半以上的鎖便是有效鎖;否則無效

總結(jié)

1.TTL時(shí)長(zhǎng) 要大于正常業(yè)務(wù)執(zhí)行的時(shí)間+獲取所有redis服務(wù)消耗時(shí)間+時(shí)鐘漂移

2.獲取redis所有服務(wù)消耗時(shí)間要 遠(yuǎn)小于TTL時(shí)間敲董,并且獲取成功的鎖個(gè)數(shù)要 在總數(shù)的一般以上:N/2+1

3.嘗試獲取每個(gè)redis實(shí)例鎖時(shí)的時(shí)間要 遠(yuǎn)小于TTL時(shí)間

4.嘗試獲取所有鎖失敗后 重新嘗試一定要有一定次數(shù)限制

5.在redis崩潰后(無論一個(gè)還是所有)紫皇,要延遲TTL時(shí)間重啟redis

6.在實(shí)現(xiàn)多redis節(jié)點(diǎn)時(shí)要結(jié)合單節(jié)點(diǎn)分布式鎖算法 共同實(shí)現(xiàn)

最后,Martin得出了如下的結(jié)論:

  • 如果是為了效率(efficiency)而使用分布式鎖腋寨,允許鎖的偶爾失效聪铺,那么使用單Redis節(jié)點(diǎn)的鎖方案就足夠了,簡(jiǎn)單而且效率高萄窜。Redlock則是個(gè)過重的實(shí)現(xiàn)(heavyweight)铃剔。

  • 如果是為了正確性(correctness)在很嚴(yán)肅的場(chǎng)合使用分布式鎖,那么不要使用Redlock查刻。它不是建立在異步模型上的一個(gè)足夠強(qiáng)的算法键兜,它對(duì)于系統(tǒng)模型的假設(shè)中包含很多危險(xiǎn)的成分(對(duì)于timing)。而且穗泵,它沒有一個(gè)機(jī)制能夠提供fencing token普气。那應(yīng)該使用什么技術(shù)呢?Martin認(rèn)為佃延,應(yīng)該考慮類似Zookeeper的方案现诀,或者支持事務(wù)的數(shù)據(jù)庫。

另一種方式:Redisson

此外履肃,實(shí)現(xiàn)Redis的分布式鎖仔沿,除了自己基于redis client原生api來實(shí)現(xiàn)之外,還可以使用開源框架:Redission

Redisson是一個(gè)企業(yè)級(jí)的開源Redis Client尺棋,也提供了分布式鎖的支持于未。我也非常推薦大家使用,為什么呢陡鹃?

回想一下上面說的烘浦,如果自己寫代碼來通過redis設(shè)置一個(gè)值,是通過下面這個(gè)命令設(shè)置的萍鲸。

  • SET anyLock unique_value NX PX 30000

這里設(shè)置的超時(shí)時(shí)間是30s闷叉,假如我超過30s都還沒有完成業(yè)務(wù)邏輯的情況下,key會(huì)過期脊阴,其他線程有可能會(huì)獲取到鎖握侧。

這樣一來的話,第一個(gè)線程還沒執(zhí)行完業(yè)務(wù)邏輯嘿期,第二個(gè)線程進(jìn)來了也會(huì)出現(xiàn)線程安全問題品擎。所以我們還需要額外的去維護(hù)這個(gè)過期時(shí)間,太麻煩了~

我們來看看redisson是怎么實(shí)現(xiàn)的备徐?先感受一下使用redission的爽:

        Config config = new Config();
        config . useClusterServers()
            . addNodeAddress("redis://192.168.31.101:7001")
            . addNodeAddress("redis://192.168.31.101:7002")
            . addNodeAddress("redis://192.168.31.101:7003")
            . addNodeAddress("redis://192.168.31.101:7004")
            . addNodeAddress("redis://192.168.31.101:7005")
            . addNodeAddress("redis://192.168.31.101:7006")
        RedissonClient redisson = Redisson.create(config)
        RLock lock=redisson.getLock("anyLock");
        lock.lock();
        lock.unlock(); 

就是這么簡(jiǎn)單萄传,我們只需要通過它的api中的lock和unlock即可完成分布式鎖,他幫我們考慮了很多細(xì)節(jié):

  • redisson所有指令都通過lua腳本執(zhí)行蜜猾,redis支持lua腳本原子性執(zhí)行

  • redisson設(shè)置一個(gè)key的默認(rèn)過期時(shí)間為30s,如果某個(gè)客戶端持有一個(gè)鎖超過了30s怎么辦秀菱?

    redisson中有一個(gè)watchdog的概念振诬,翻譯過來就是看門狗,它會(huì)在你獲取鎖之后衍菱,每隔10秒幫你把key的超時(shí)時(shí)間重新延后30s

    這樣的話赶么,就不會(huì)出現(xiàn)業(yè)務(wù)沒執(zhí)行完而key過期了,其他線程獲取到鎖的問題了脊串。

  • redisson的“看門狗”邏輯保證了沒有死鎖發(fā)生辫呻。

    (如果機(jī)器宕機(jī)了,看門狗也就沒了琼锋。此時(shí)就不會(huì)延長(zhǎng)key的過期時(shí)間印屁,到了30s之后就會(huì)自動(dòng)過期了,其他線程可以獲取到鎖)

image

這里稍微貼出來其實(shí)現(xiàn)代碼:

這里的代碼請(qǐng)看下面參考連接中的

另外斩例,redisson還提供了對(duì)redlock算法的支持,

它的用法也很簡(jiǎn)單:

這里的代碼請(qǐng)看下面參考連接中的

小結(jié):

本節(jié)分析了使用redis作為分布式鎖的具體落地方案

以及其一些局限性

然后介紹了一個(gè)redis的客戶端框架redisson,

這也是我推薦大家使用的从橘,

比自己寫代碼實(shí)現(xiàn)會(huì)少care很多細(xì)節(jié)念赶。

基于zookeeper實(shí)現(xiàn)分布式鎖

常見的分布式鎖實(shí)現(xiàn)方案里面,除了使用redis來實(shí)現(xiàn)之外恰力,使用zookeeper也可以實(shí)現(xiàn)分布式鎖叉谜。

在介紹zookeeper(下文用zk代替)實(shí)現(xiàn)分布式鎖的機(jī)制之前,先粗略介紹一下zk是什么東西:

Zookeeper是一種提供配置管理踩萎、分布式協(xié)同以及命名的中心化服務(wù)停局。

zk的模型是這樣的:zk包含一系列的節(jié)點(diǎn),叫做znode香府,就好像文件系統(tǒng)一樣每個(gè)znode表示一個(gè)目錄董栽,然后znode有一些特性:

  • 有序節(jié)點(diǎn):假如當(dāng)前有一個(gè)父節(jié)點(diǎn)為/lock,我們可以在這個(gè)父節(jié)點(diǎn)下面創(chuàng)建子節(jié)點(diǎn)企孩;

    zookeeper提供了一個(gè)可選的有序特性锭碳,例如我們可以創(chuàng)建子節(jié)點(diǎn)“/lock/node-”并且指明有序,那么zookeeper在生成子節(jié)點(diǎn)時(shí)會(huì)根據(jù)當(dāng)前的子節(jié)點(diǎn)數(shù)量自動(dòng)添加整數(shù)序號(hào)

    也就是說勿璃,如果是第一個(gè)創(chuàng)建的子節(jié)點(diǎn)擒抛,那么生成的子節(jié)點(diǎn)為/lock/node-0000000000,下一個(gè)節(jié)點(diǎn)則為/lock/node-0000000001补疑,依次類推歧沪。

  • 臨時(shí)節(jié)點(diǎn):客戶端可以建立一個(gè)臨時(shí)節(jié)點(diǎn),在會(huì)話結(jié)束或者會(huì)話超時(shí)后莲组,zookeeper會(huì)自動(dòng)刪除該節(jié)點(diǎn)诊胞。

  • 事件監(jiān)聽:在讀取數(shù)據(jù)時(shí),我們可以同時(shí)對(duì)節(jié)點(diǎn)設(shè)置事件監(jiān)聽锹杈,當(dāng)節(jié)點(diǎn)數(shù)據(jù)或結(jié)構(gòu)變化時(shí)厢钧,zookeeper會(huì)通知客戶端鳞尔。當(dāng)前zookeeper有如下四種事件:

  • 節(jié)點(diǎn)創(chuàng)建

  • 節(jié)點(diǎn)刪除

  • 節(jié)點(diǎn)數(shù)據(jù)修改

  • 子節(jié)點(diǎn)變更

基于以上的一些zk的特性,我們很容易得出使用zk實(shí)現(xiàn)分布式鎖的落地方案:

  1. 使用zk的臨時(shí)節(jié)點(diǎn)和有序節(jié)點(diǎn)早直,每個(gè)線程獲取鎖就是在zk創(chuàng)建一個(gè)臨時(shí)有序的節(jié)點(diǎn)寥假,比如在/lock/目錄下。

  2. 創(chuàng)建節(jié)點(diǎn)成功后霞扬,獲取/lock目錄下的所有臨時(shí)節(jié)點(diǎn)糕韧,再判斷當(dāng)前線程創(chuàng)建的節(jié)點(diǎn)是否是所有的節(jié)點(diǎn)的序號(hào)最小的節(jié)點(diǎn)

  3. 如果當(dāng)前線程創(chuàng)建的節(jié)點(diǎn)是所有節(jié)點(diǎn)序號(hào)最小的節(jié)點(diǎn),則認(rèn)為獲取鎖成功喻圃。

  4. 如果當(dāng)前線程創(chuàng)建的節(jié)點(diǎn)不是所有節(jié)點(diǎn)序號(hào)最小的節(jié)點(diǎn)萤彩,則對(duì)節(jié)點(diǎn)序號(hào)的前一個(gè)節(jié)點(diǎn)添加一個(gè)事件監(jiān)聽。

    比如當(dāng)前線程獲取到的節(jié)點(diǎn)序號(hào)為/lock/003,然后所有的節(jié)點(diǎn)列表為[/lock/001,/lock/002,/lock/003],則對(duì)/lock/002這個(gè)節(jié)點(diǎn)添加一個(gè)事件監(jiān)聽器斧拍。

如果鎖釋放了雀扶,會(huì)喚醒下一個(gè)序號(hào)的節(jié)點(diǎn),然后重新執(zhí)行第3步肆汹,判斷是否自己的節(jié)點(diǎn)序號(hào)是最小愚墓。

比如/lock/001釋放了,/lock/002監(jiān)聽到時(shí)間昂勉,此時(shí)節(jié)點(diǎn)集合為[/lock/002,/lock/003],則/lock/002為最小序號(hào)節(jié)點(diǎn)浪册,獲取到鎖。

整個(gè)過程如下:

image

具體的實(shí)現(xiàn)思路就是這樣岗照,至于代碼怎么寫,這里比較復(fù)雜就不貼出來了攒至。

Curator介紹

Curator是一個(gè)zookeeper的開源客戶端厚者,也提供了分布式鎖的實(shí)現(xiàn)。

他的使用方式也比較簡(jiǎn)單:

InterProcessMutex interProcessMutex =  new InterProcessMutex(client,"/anyLock");`
interProcessMutex.acquire();`
interProcessMutex.release();`

其實(shí)現(xiàn)分布式鎖的核心源碼如下:

這里的代碼請(qǐng)看下下面的連接

其實(shí)curator實(shí)現(xiàn)分布式鎖的底層原理和上面分析的是差不多的迫吐。這里我們用一張圖詳細(xì)描述其原理:

image

小結(jié):

本節(jié)介紹了zookeeperr實(shí)現(xiàn)分布式鎖的方案以及zk的開源客戶端的基本使用籍救,簡(jiǎn)要的介紹了其實(shí)現(xiàn)原理。

兩種方案的優(yōu)缺點(diǎn)比較

學(xué)完了兩種分布式鎖的實(shí)現(xiàn)方案之后渠抹,本節(jié)需要討論的是redis和zk的實(shí)現(xiàn)方案中各自的優(yōu)缺點(diǎn)蝙昙。

對(duì)于redis的分布式鎖而言,它有以下缺點(diǎn):

  • 它獲取鎖的方式簡(jiǎn)單粗暴梧却,獲取不到鎖直接不斷嘗試獲取鎖奇颠,比較消耗性能。

  • 另外來說的話放航,redis的設(shè)計(jì)定位決定了它的數(shù)據(jù)并不是強(qiáng)一致性的烈拒,在某些極端情況下,可能會(huì)出現(xiàn)問題。鎖的模型不夠健壯

  • 即便使用redlock算法來實(shí)現(xiàn)荆几,在某些復(fù)雜場(chǎng)景下吓妆,也無法保證其實(shí)現(xiàn)100%沒有問題,關(guān)于redlock的討論可以看How to do distributed locking

  • redis分布式鎖吨铸,其實(shí)需要自己不斷去嘗試獲取鎖行拢,比較消耗性能。

但是另一方面使用redis實(shí)現(xiàn)分布式鎖在很多企業(yè)中非常常見诞吱,而且大部分情況下都不會(huì)遇到所謂的“極端復(fù)雜場(chǎng)景”

所以使用redis作為分布式鎖也不失為一種好的方案舟奠,最重要的一點(diǎn)是redis的性能很高,可以支撐高并發(fā)的獲取房维、釋放鎖操作沼瘫。

對(duì)于zk分布式鎖而言:

  • zookeeper天生設(shè)計(jì)定位就是分布式協(xié)調(diào),強(qiáng)一致性咙俩。鎖的模型健壯耿戚、簡(jiǎn)單易用、適合做分布式鎖阿趁。

  • 如果獲取不到鎖膜蛔,只需要添加一個(gè)監(jiān)聽器就可以了,不用一直輪詢歌焦,性能消耗較小。

但是zk也有其缺點(diǎn):如果有較多的客戶端頻繁的申請(qǐng)加鎖砚哆、釋放鎖独撇,對(duì)于zk集群的壓力會(huì)比較大。

小結(jié):

綜上所述躁锁,redis和zookeeper都有其優(yōu)缺點(diǎn)纷铣。我們?cè)谧黾夹g(shù)選型的時(shí)候可以根據(jù)這些問題作為參考因素。

作者的一些建議

通過前面的分析战转,實(shí)現(xiàn)分布式鎖的兩種常見方案:redis和zookeeper搜立,他們各有千秋。應(yīng)該如何選型呢槐秧?

就個(gè)人而言的話啄踊,我比較推崇zk實(shí)現(xiàn)的鎖:

因?yàn)閞edis是有可能存在隱患的,可能會(huì)導(dǎo)致數(shù)據(jù)不對(duì)的情況刁标。但是颠通,怎么選用要看具體在公司的場(chǎng)景了。

如果公司里面有zk集群條件膀懈,優(yōu)先選用zk實(shí)現(xiàn)顿锰,但是如果說公司里面只有redis集群,沒有條件搭建zk集群。

那么其實(shí)用redis來實(shí)現(xiàn)也可以硼控,另外還可能是系統(tǒng)設(shè)計(jì)者考慮到了系統(tǒng)已經(jīng)有redis刘陶,但是又不希望再次引入一些外部依賴的情況下,可以選用redis牢撼。

這個(gè)是要系統(tǒng)設(shè)計(jì)者基于架構(gòu)的考慮了

思考

加鎖是為了資源不能同時(shí)被共享匙隔,那么隊(duì)列是單個(gè)消費(fèi)的,能保證資源不被共享浪默。 那么能使用隊(duì)列代替嗎牡直?

參考

分布式鎖
redLock
基于Redis的分布式鎖到底安全嗎(上)
Redlock(redis分布式鎖)原理分析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市纳决,隨后出現(xiàn)的幾起案子碰逸,更是在濱河造成了極大的恐慌,老刑警劉巖阔加,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饵史,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡胜榔,警方通過查閱死者的電腦和手機(jī)胳喷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來夭织,“玉大人吭露,你說我怎么就攤上這事∽鸲瑁” “怎么了讲竿?”我有些...
    開封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)弄屡。 經(jīng)常有香客問我题禀,道長(zhǎng),這世上最難降的妖魔是什么膀捷? 我笑而不...
    開封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任迈嘹,我火速辦了婚禮,結(jié)果婚禮上全庸,老公的妹妹穿的比我還像新娘秀仲。我一直安慰自己,他們只是感情好壶笼,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開白布啄育。 她就那樣靜靜地躺著,像睡著了一般拌消。 火紅的嫁衣襯著肌膚如雪挑豌。 梳的紋絲不亂的頭發(fā)上安券,一...
    開封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音氓英,去河邊找鬼侯勉。 笑死,一個(gè)胖子當(dāng)著我的面吹牛铝阐,可吹牛的內(nèi)容都是我干的址貌。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼徘键,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼练对!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起吹害,我...
    開封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤螟凭,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后它呀,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體螺男,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年纵穿,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了下隧。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡谓媒,死狀恐怖淆院,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情句惯,我是刑警寧澤土辩,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站宗弯,受9級(jí)特大地震影響脯燃,放射性物質(zhì)發(fā)生泄漏搂妻。R本人自食惡果不足惜蒙保,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望欲主。 院中可真熱鬧邓厕,春花似錦、人聲如沸扁瓢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽引几。三九已至昧互,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背敞掘。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工叽掘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玖雁。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓更扁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親赫冬。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浓镜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355

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