(十三)全面理解并發(fā)編程之分布式架構(gòu)下Redis、ZK分布式鎖的前世今生

引言

在前面的大部分文章中,我們反復(fù)圍繞著線程安全相關(guān)問題在對(duì)Java的并發(fā)編程進(jìn)行闡述,但前敘的文章中都是基于單體架構(gòu)的Java程序進(jìn)行分析的,而如今單體的程序遠(yuǎn)不足以滿足日益漸增的用戶需求,所以一般目前Java程序都是通過多機(jī)器、分布式的架構(gòu)模式進(jìn)行部署。那么在多部署環(huán)境下,之前我們分析的CAS無鎖、隱式鎖、顯式鎖等方案是否還有效呢?答案是無效。

一、單體架構(gòu)下的鎖遷移分布式架構(gòu)分析

在前面關(guān)于Synchronized關(guān)鍵字原理剖析以及AQS與ReetrantLock原理分析兩篇文章中,曾得知這兩種都是屬于可以解決線程安全問題的鎖,一種是Java原生的關(guān)鍵字,通過Java對(duì)象進(jìn)行加鎖的隱式鎖,另一種則是JUC提供的顯式鎖方案。在開發(fā)過程中使用它們都能夠保證多線程的安全問題。但把它們放在多機(jī)器/多應(yīng)用部署的環(huán)境下,也能保證相同的效果嗎?先來看一個(gè)案例:

兩個(gè)服務(wù):[訂單服務(wù):8000端口]、[商品服務(wù):8001、8002端口]
數(shù)據(jù)庫(kù):訂單庫(kù)[db_order]、商品庫(kù)[db_shopping]
訂單服務(wù)中提供了一個(gè)下單接口,用戶下單后會(huì)調(diào)用它,調(diào)用下單接口后,會(huì)通過restTemplate進(jìn)行RPC調(diào)用商品服務(wù)的扣庫(kù)存接口,實(shí)現(xiàn)庫(kù)存扣減下單操作。
源碼如下:

// 訂單服務(wù)
@RestController
@RequestMapping("/order")
public class OrderApi{
    // 庫(kù)存服務(wù)的RPC前綴
    private static final String REST_URL_PREFIX =
        "http://SHOPPING/inventory";
    
    @Autowired
    private OrderService orderService;
    @Autowired
    private RestTemplate restTemplate;
    
    // 下單接口
    @RequestMapping("/placeAnOrder")
    public String placeAnOrder(){
        // 模擬商品ID
        String inventoryId = "82391173-9dbc-49b6-821b-746a11dbbe5e";
        // 生成一個(gè)訂單ID(分布式架構(gòu)中要使用分布式ID生成策略,此處是模擬)
        String orderId = UUID.randomUUID().toString();
        // 模擬生成訂單記錄
        Order order = new
            Order(orderId,"黃金竹子","88888.88",inventoryId);
        
        // RPC調(diào)用庫(kù)存接口
        String responseResult = restTemplate.getForObject(
            REST_URL_PREFIX + "/minusInventory?inventoryId="
                + inventoryId, String.class);
        System.out.println("調(diào)用后庫(kù)存接口結(jié)果:" + responseResult);
        
        Integer n = orderService.insertSelective(order);
        
        if (n > 0) 
            return "下單成功....";
        return "下單失敗....";
    }
}

// 庫(kù)存服務(wù)
@RestController
@RequestMapping("/inventory")
public class InventoryApi{
    @Autowired
    private InventoryService inventoryService;
    @Value("${server.port}")
    private String port;
    
    // 扣庫(kù)存接口
    @RequestMapping("/minusInventory")
    public String minusInventory(Inventory inventory) {
        // 查詢庫(kù)存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "庫(kù)存不足,請(qǐng)聯(lián)系賣家....";
        }
        
        // 扣減庫(kù)存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        
        if (n > 0)
            return "端口-" + port + ",庫(kù)存扣減成功?。?!";
        return "端口-" + port + ",庫(kù)存扣減失敗?。?!";
    }
}

觀察上述源碼,存在什么問題?線程安全問題。按照之前的做法我們會(huì)對(duì)扣庫(kù)存的接口使用Synchronized或ReetrantLock加鎖,如下:

// 庫(kù)存服務(wù) → 扣庫(kù)存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    int n;
    synchronized(InventoryApi.class){
        // 查詢庫(kù)存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "庫(kù)存不足,請(qǐng)聯(lián)系賣家....";
        }
        
        // 扣減庫(kù)存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        System.out.println("庫(kù)存信息-剩余庫(kù)存數(shù)量:" +
                inventoryResult.getShopCount());
    }
    if (n > 0)
        return "端口:" + port + ",庫(kù)存扣減成功?。?!";
    return "端口:" + port + ",庫(kù)存扣減失敗?。。?;
}

是不是感覺沒問題了?看測(cè)試:

通過JMeter壓測(cè)工具,1秒內(nèi)對(duì)下單接口進(jìn)行八百次調(diào)用,此時(shí)出現(xiàn)了一個(gè)比較有意思的現(xiàn)象,測(cè)試結(jié)果如下:

訂單服務(wù)控制臺(tái)日志:[端口:8000]
    ......
    調(diào)用后庫(kù)存接口結(jié)果:端口-8001,庫(kù)存扣減成功!??!
    調(diào)用后庫(kù)存接口結(jié)果:端口-8002,庫(kù)存扣減成功?。?!
    ......
    調(diào)用后庫(kù)存接口結(jié)果:端口-8001,庫(kù)存扣減成功!!!
    調(diào)用后庫(kù)存接口結(jié)果:端口-8001,庫(kù)存扣減成功?。。?    調(diào)用后庫(kù)存接口結(jié)果:端口-8002,庫(kù)存扣減成功!??!
    ......

商品服務(wù)控制臺(tái)日志:[端口:8001]
    ......
    庫(kù)存信息-剩余庫(kù)存數(shù)量:999
    ......
    庫(kù)存信息-剩余庫(kù)存數(shù)量:788
    庫(kù)存信息-剩余庫(kù)存數(shù)量:787
    .....

商品服務(wù)控制臺(tái)日志:[端口:8002]
    ......
    庫(kù)存信息-剩余庫(kù)存數(shù)量:998
    庫(kù)存信息-剩余庫(kù)存數(shù)量:996
    庫(kù)存信息-剩余庫(kù)存數(shù)量:993
    ......
    庫(kù)存信息-剩余庫(kù)存數(shù)量:788
    .....

注意觀察如上日志,在兩個(gè)商品服務(wù)[8001/8002]中都出現(xiàn)了庫(kù)存信息-剩余庫(kù)存數(shù)量:788這么一條日志記錄,這代表第799個(gè)商品被賣了兩次,還是出現(xiàn)了線程安全問題,導(dǎo)致了庫(kù)存超賣。可我們不是已經(jīng)通過Class對(duì)象加鎖了嗎?為什么還會(huì)有這樣的問題出現(xiàn)呢?下面我們來依次分析。

問題分析

在關(guān)于Synchronized關(guān)鍵字原理剖析的文章中已經(jīng)得知:Synchronized關(guān)鍵字是依賴于對(duì)象做為鎖資源進(jìn)行加鎖操作的,每個(gè)對(duì)象都會(huì)存在一個(gè)伴生的Monitor監(jiān)視器,Synchronized則是通過它進(jìn)行上鎖,加鎖過程如下:

Synchronized執(zhí)行流程

OK~,有了上述基礎(chǔ)知識(shí)之后,再接著分析前面的問題。為什么會(huì)出現(xiàn)線程安全問題?

實(shí)際上這個(gè)問題也不難理解,前面的案例中,我們是通過InventoryApi.class類對(duì)象進(jìn)行上鎖的,如果在單體程序中確實(shí)沒有任何問題,因?yàn)?code>Class對(duì)象是唯一的,當(dāng)多條線程搶占一把Class鎖時(shí),同一時(shí)刻只會(huì)有一條線程獲取鎖成功,這樣自然而然也不存在線程安全問題了。但目前的問題就出在這里,InventoryApi.class對(duì)象在單體程序中確實(shí)只存在一個(gè),但目前商品服務(wù)是屬于多應(yīng)用/多機(jī)器部署的,目前商品服務(wù)有兩個(gè)進(jìn)程,那這就代表著存在兩個(gè)不同的Java堆,在兩個(gè)不同的堆空間中,都存在各自的InventoryApi.class對(duì)象,也就是代表著“此時(shí)Class對(duì)象并不是唯一的,此刻出現(xiàn)了兩把鎖”。而正是因?yàn)檫@個(gè)問題才最終導(dǎo)致了線程安全問題的出現(xiàn)。如下圖:

單體與分布式

還是回到最開始敘述線程安全的起點(diǎn),同一時(shí)刻滿足三要素:“多線程”、“共享資源”以及“非原子性操作”便會(huì)產(chǎn)生線程安全問題,而SynchronizedReetrantLock都是從破壞了第一個(gè)要素的角度出發(fā),以此解決了線程安全問題。但目前在分布式架構(gòu)中,SynchronizedReetrantLock因?yàn)槎噙M(jìn)程導(dǎo)致的多個(gè)Java空間堆出現(xiàn),所以不管是Synchronized還是ReetrantLock都已經(jīng)無法保證同一時(shí)刻只有一條線程對(duì)共享資源進(jìn)行非原子性操作,最終線程安全問題的出現(xiàn)已經(jīng)無法避免。

二、分布式鎖思想及其實(shí)現(xiàn)過程推導(dǎo)

經(jīng)過前面的分析,已經(jīng)得知了分布式架構(gòu)下的線程安全問題產(chǎn)生的根本原因,那目前又該如何解決這個(gè)問題呢?可能學(xué)習(xí)過分布式鎖的小伙伴會(huì)直接回答:分布式鎖。但此刻請(qǐng)你拋下之前的記憶,我們完全從零開始對(duì)分布式鎖進(jìn)行推導(dǎo)。

大家可以先代入一個(gè)角色:假設(shè)市面上還沒有成熟的解決方案,你是第一位遇到這個(gè)問題的人,那么你又該如何去解決這類問題呢?OK~,接下來我們一起逐步推導(dǎo)。

先思考思考前面的Synchronized、ReetrantLock是如何解決單體程序的線程安全問題的呢?

  • Synchronized:
    • 依賴堆中對(duì)象的Monitor監(jiān)視器
    • 通過操作監(jiān)視器中的_count字段實(shí)現(xiàn)互斥
  • ReetrantLock:
    • 依賴于AQS同步器
    • 通過操作AQS中volatile修飾的state成員實(shí)現(xiàn)互斥

它們有什么共同點(diǎn)呢?都存在一個(gè)互斥量,并且互斥量都是所有線程可見的。

OK~,明白了這兩點(diǎn)之后,再反過來思考一下,分布式環(huán)境中又是因?yàn)槭裁丛驅(qū)е碌陌踩珕栴}復(fù)發(fā)了呢?答案是:互斥量所在的區(qū)域?qū)τ谄渌M(jìn)程中的線程來說是不可見的。比如Synchronized關(guān)鍵字通過某個(gè)Class對(duì)象作為鎖對(duì)象,一個(gè)堆空間中的Class對(duì)象對(duì)于當(dāng)前進(jìn)程中的所有線程來說是可見的,但是對(duì)于其他進(jìn)程的線程是不可見的。ReetrantLock也是同理,volatile修飾的state變量,對(duì)于當(dāng)前進(jìn)程中的所有線程可見,但對(duì)于另外進(jìn)程中的線程是不可見的。

那么此時(shí)想要解決分布式情況下的線程安全問題的思路是不是明了啦?

我們只需要找一個(gè)多個(gè)進(jìn)程之間所有線程可見的區(qū)域?qū)崿F(xiàn)這個(gè)互斥量即可。
比如:在一臺(tái)服務(wù)器的同一路徑下創(chuàng)建一個(gè)文件夾。獲取鎖操作則是創(chuàng)建文件夾,反之,釋放鎖的邏輯則是刪除文件夾,這樣可以很好的實(shí)現(xiàn)一把分布式鎖,因?yàn)镺S特性規(guī)定,在同一路徑下,相同名字的文件夾只能創(chuàng)建一個(gè)。所以當(dāng)兩條線程同時(shí)執(zhí)行獲取鎖邏輯時(shí),永遠(yuǎn)只會(huì)有一條線程創(chuàng)建成功,成功創(chuàng)建文件夾的那條線程則代表獲取鎖成功,那么可以去執(zhí)行業(yè)務(wù)邏輯。當(dāng)這條線程執(zhí)行完業(yè)務(wù)后,再刪除掉文件夾,代表釋放鎖,以便于其他線程可以再次獲取鎖。

上述的這種方式確實(shí)可以實(shí)現(xiàn)一把最基本的分布式鎖,但問題在于:這樣實(shí)現(xiàn)的話一方面性能會(huì)比較差,第二個(gè)也不能具備鎖重入的功能,第三方面也沒有具備合理的鎖失效機(jī)制以及阻塞機(jī)制。而一個(gè)優(yōu)秀的分布式鎖的實(shí)現(xiàn)方案應(yīng)該滿足如下幾個(gè)特性:

  • ①在分布式環(huán)境中,可以保證不同進(jìn)程之間的線程互斥
  • ②在同一時(shí)刻,同時(shí)只允許一條線程成功獲取到鎖資源
  • ③保存互斥量的地方需要保證高可用性
  • ④要保證可以高性能的獲取鎖與釋放鎖
  • ⑤可以支持同一線程的鎖重入性
  • ⑥具備合理的阻塞機(jī)制,競(jìng)爭(zhēng)鎖失敗的線程也有處理方案
  • ⑦支持非阻塞式獲取鎖,獲取鎖失敗的線程可以直接返回
  • ⑧具備合理的鎖失效機(jī)制,如超時(shí)失效等,可以確保避免死鎖情況出現(xiàn)

那么目前市面上對(duì)于分布式鎖的成熟方案有哪些呢?

  • ①基于DB實(shí)現(xiàn)
  • ②基于Redis實(shí)現(xiàn)
  • ③基于Zookeeper實(shí)現(xiàn)

對(duì)于第一種方式的實(shí)現(xiàn)并不難,無非是在數(shù)據(jù)庫(kù)中創(chuàng)建一張lock表,表中設(shè)置方法名、線程ID等字段。并為方法名字段建立唯一索引,當(dāng)線程執(zhí)行某個(gè)方法需要獲取鎖時(shí),就以這個(gè)方法名作為數(shù)據(jù)向表中插入,如果插入成功代表獲取鎖成功,如果插入失敗,代表有其他線程已經(jīng)在此之前持有鎖了,當(dāng)前線程可以阻塞等待或直接返回。同時(shí),也可以基于表中的線程ID字段為鎖重入提供支持。當(dāng)然,當(dāng)持有鎖的線程業(yè)務(wù)邏輯執(zhí)行完成后,應(yīng)當(dāng)刪除對(duì)應(yīng)的數(shù)據(jù)行,以此達(dá)到釋放鎖的目的。

這種方式依靠于數(shù)據(jù)庫(kù)的唯一索引,所以實(shí)現(xiàn)起來比較簡(jiǎn)單,但是問題在于:因?yàn)槭腔跀?shù)據(jù)庫(kù)實(shí)現(xiàn)的,所以獲取鎖、釋放鎖等操作都要涉及到數(shù)據(jù)落盤、刪盤等磁盤IO操作,性能方面值得考慮。同時(shí)也對(duì)于超時(shí)失效機(jī)制很難提供支持,在實(shí)現(xiàn)過程中也會(huì)出現(xiàn)很多其他問題,為了確保解決各類問題,實(shí)現(xiàn)的方式也會(huì)越發(fā)復(fù)雜。

OK~,那么接下來再看看其他兩種主流方案的實(shí)現(xiàn),redis以及ZK實(shí)現(xiàn)分布式鎖,也是目前應(yīng)用最廣泛的方式。

三、Redis實(shí)現(xiàn)分布式鎖及其細(xì)節(jié)問題分析

Redis實(shí)現(xiàn)分布式鎖是目前使用最廣泛的方式之一,因?yàn)镽edis屬于中間件,獨(dú)立部署在外,不附屬于任何一個(gè)Java程序,對(duì)于不同的Java進(jìn)程來說,都是可見的,同時(shí)它的性能也非常可觀,可以依賴于其本身提供的指令setnx key value實(shí)現(xiàn)分布式鎖。

setnx key value:往Redis中寫入一個(gè)K-V值。不過與普通的set指令不同的是:setnx只有當(dāng)key不存在時(shí)才會(huì)設(shè)置成功,當(dāng)key已存在時(shí),會(huì)返回設(shè)置失敗。同時(shí)因?yàn)閞edis對(duì)于客戶端的指令請(qǐng)求處理時(shí),是使用epoll多路復(fù)用模型的,所以當(dāng)同時(shí)多條線程一起向redis服務(wù)端發(fā)送setnx指令時(shí),只會(huì)有一條線程設(shè)置成功。最終可以依賴于redis這些特性實(shí)現(xiàn)分布式鎖。

OK~,下面通過setnx key value實(shí)現(xiàn)最基本的分布式鎖,如下:

// 庫(kù)存服務(wù)
@RestController
@RequestMapping("/inventory")
public class InventoryApi{
    @Autowired
    private InventoryService inventoryService;
    @Value("${server.port}")
    private String port;
    // 注入Redis客戶端
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    
    // 扣庫(kù)存接口
    @RequestMapping("/minusInventory")
    public String minusInventory(Inventory inventory) {
        // 獲取鎖
        String lockKey = "lock-" + inventory.getInventoryId();
        Boolean flag = stringRedisTemplate.opsForValue()
                .setIfAbsent(lockKey, "竹子-熊貓");
        
        if(!flag){
            // 非阻塞式實(shí)現(xiàn)
            return "服務(wù)器繁忙...請(qǐng)稍后重試?。?!";
            // 自旋式實(shí)現(xiàn)(這種實(shí)現(xiàn)比較耗性能,
            // 實(shí)際開發(fā)過程中需配合阻塞時(shí)間配合使用)
            // return minusInventory(inventory);
        }
        
        // ----只有獲取鎖成功才能執(zhí)行下述的減庫(kù)存業(yè)務(wù)----        
        try{
            // 查詢庫(kù)存信息
            Inventory inventoryResult =
                inventoryService.selectByPrimaryKey(inventory.getInventoryId());
            
            if (inventoryResult.getShopCount() <= 0) {
                return "庫(kù)存不足,請(qǐng)聯(lián)系賣家....";
            }
            
            // 扣減庫(kù)存
            inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
            int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
        } catch (Exception e) { // 確保業(yè)務(wù)出現(xiàn)異常也可以釋放鎖,避免死鎖
            // 釋放鎖
            stringRedisTemplate.delete(lockKey);
        }
        
        if (n > 0)
            return "端口-" + port + ",庫(kù)存扣減成功?。。?;
        return "端口-" + port + ",庫(kù)存扣減失?。。。?;
    }
}

如上源碼,實(shí)現(xiàn)了一把最基本的分布式鎖,使用setnx指令往redis中寫入一條數(shù)據(jù),以當(dāng)前商品ID作為key值,這樣可以確保鎖粒度得到控制。同時(shí)也使用try-catch保證業(yè)務(wù)執(zhí)行出錯(cuò)時(shí)也能釋放鎖,可以有效避免死鎖問題出現(xiàn)。

一條線程(一個(gè)請(qǐng)求)想要執(zhí)行扣庫(kù)存業(yè)務(wù)時(shí),需要先往redis寫入數(shù)據(jù),當(dāng)寫入成功時(shí)代表獲取鎖成功,獲取鎖成功的線程可以執(zhí)行業(yè)務(wù)。反之,寫入失敗的線程代表已經(jīng)有線程在之前已經(jīng)獲取鎖了,可以自己處理獲取鎖失敗的邏輯,如上源碼實(shí)現(xiàn)了非阻塞式獲取鎖(可自行實(shí)現(xiàn)阻塞+重試+次數(shù)控制)。

3.1、宕機(jī)/重啟死鎖問題分析

前面已經(jīng)通過Redis實(shí)現(xiàn)了一把最基本的分布式鎖,但問題在于:假設(shè)8001機(jī)器的線程T1剛剛獲取鎖成功,但不巧的是:8001所在的服務(wù)器宕機(jī)或斷電重啟了。那此時(shí)又會(huì)出現(xiàn)問題:獲取到鎖的T1線程因?yàn)樗谶M(jìn)程/服務(wù)器掛了,所以T1線程也會(huì)被迫死亡,那此時(shí)try-catch也無法保證鎖的釋放,T1線程不釋放鎖,其他線程嘗試setnx獲取鎖時(shí)也不會(huì)成功,最終導(dǎo)致了死鎖現(xiàn)象的出現(xiàn)。那這個(gè)問題又該如何解決呢?加上Key過期時(shí)間即可。如下:

// 扣庫(kù)存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // 獲取鎖
    String lockKey = "lock-" + inventory.getInventoryId();
    int timeOut = 100;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "竹子-熊貓");
    // 加上過期時(shí)間,可以保證死鎖也會(huì)在一定時(shí)間內(nèi)釋放鎖
    stringRedisTemplate.expire(lockKey,timeOut,TimeUnit.SECONDS);
    
    if(!flag){
        // 非阻塞式實(shí)現(xiàn)
        return "服務(wù)器繁忙...請(qǐng)稍后重試?。?!";
        // 自旋式實(shí)現(xiàn)(這種實(shí)現(xiàn)比較耗性能,
        // 實(shí)際開發(fā)過程中需配合阻塞時(shí)間配合使用)
        // return minusInventory(inventory);
    }
    
    // ----只有獲取鎖成功才能執(zhí)行下述的減庫(kù)存業(yè)務(wù)----        
    try{
        // 查詢庫(kù)存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "庫(kù)存不足,請(qǐng)聯(lián)系賣家....";
        }
        
        // 扣減庫(kù)存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
    } catch (Exception e) { // 確保業(yè)務(wù)出現(xiàn)異常也可以釋放鎖,避免死鎖
        // 釋放鎖
        stringRedisTemplate.delete(lockKey);
    }
    
    if (n > 0)
        return "端口-" + port + ",庫(kù)存扣減成功?。。?;
    return "端口-" + port + ",庫(kù)存扣減失敗?。。?;
}

如上,在基礎(chǔ)版的分布式鎖中,再加上一個(gè)超時(shí)失效的機(jī)制,這樣可以有效避免死鎖的情況出現(xiàn)。就算獲取到分布式鎖的那條線程所在機(jī)器不小心宕機(jī)或重啟了,導(dǎo)致無法釋放鎖,那也不會(huì)產(chǎn)生死鎖情況,因?yàn)镵ey設(shè)置了過期時(shí)間,在設(shè)定時(shí)間內(nèi),如果沒有釋放鎖,那么時(shí)間一到,Redis會(huì)自動(dòng)釋放鎖,以確保其他程序的線程可以獲取到鎖。

OK,解決掉宕機(jī)死鎖的問題后,再來看看我們自己實(shí)現(xiàn)的這個(gè)分布式鎖是否還有缺陷呢?

3.2、加鎖與過期時(shí)間原子性問題分析

從上一步的敘述中,通過設(shè)定過期時(shí)間的方式解決了宕機(jī)死鎖問題,但問題在于:前面分析過,Redis處理客戶端指令時(shí)采用的是單線程多路復(fù)用的模型,這就代表著只會(huì)有一條線程在處理所有客戶端的請(qǐng)求,因?yàn)閷?shí)際開發(fā)過程中,往往會(huì)有多處同時(shí)操作redis,而前面的加鎖與設(shè)置過期時(shí)間兩條指令對(duì)于redis是分開的,這兩條指令在執(zhí)行時(shí)不一定可以確保同時(shí)執(zhí)行,如下:

Redis處理客戶端指令模型

從上圖可以看到,加鎖與設(shè)置過期時(shí)間這兩條指令不一定會(huì)隨時(shí)執(zhí)行,那會(huì)出現(xiàn)什么問題呢?因?yàn)橹噶钍欠珠_執(zhí)行的,所以原子性沒有保證,有可能導(dǎo)致時(shí)間和小幾率死鎖問題出現(xiàn)。所以加鎖和設(shè)置過期時(shí)間這兩條指令需保證原子性,怎么操作呢??jī)煞N方式:

  • ①通過Lua語(yǔ)言編寫腳本執(zhí)行:
    • redis是支持lua腳本執(zhí)行的,會(huì)把lua編譯成sha指令,支持同時(shí)執(zhí)行多條指令
  • ②通過redis提供的原子性指令執(zhí)行:
    • 在redis2.7版本后提供了一些原子性指令,其中就包括set指令,如下:
      • set key value ex 100 nx
    • 可以通過這條指令代替之前的setnx與expire兩條指令

那在Java程序中又該如何修改代碼呢?實(shí)則非常簡(jiǎn)單,在SpringBoot整合Redis的模板中,只需要把如上代碼稍微修改一下即可。如下:

Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "竹子-熊貓",timeOut,TimeUnit.SECONDS);

3.3、過期時(shí)間的合理性分析

前面關(guān)于死鎖的問題已經(jīng)通過合理的鎖過期失效機(jī)制解決了,那再來思考一下這個(gè)過期時(shí)間的設(shè)定是否合理呢?前面案例中,設(shè)置的是100s過期時(shí)間,乍一看感覺沒啥問題,但仔細(xì)往深處一想:如果一條線程獲取鎖之后死亡了,其他線程想要獲取鎖其不是需要等到100s之后?因?yàn)樾枰鹊絩edis過期刪除后,釋放了鎖才可獲取。這明顯不合理,100s的時(shí)間,如果分布式鎖設(shè)計(jì)成了阻塞可重試類型的,那么可能會(huì)讓當(dāng)前程序在100s內(nèi)堆積大量請(qǐng)求,同時(shí)對(duì)于用戶體驗(yàn)感也并不好,在這一百秒內(nèi),用戶無論怎么重試,都不會(huì)下單成功,所以時(shí)間太長(zhǎng)顯然不合理。

那可能有小伙伴會(huì)說:“簡(jiǎn)單!時(shí)間設(shè)置的短一些不就好了嘛”,比如設(shè)置成5s。

確實(shí),一聽也沒啥毛病,但再仔細(xì)一想:分布式系統(tǒng)中,業(yè)務(wù)執(zhí)行往往會(huì)設(shè)計(jì)多個(gè)系統(tǒng)的RPC遠(yuǎn)程調(diào)用,因?yàn)槠渲猩婕熬W(wǎng)絡(luò)調(diào)用,網(wǎng)絡(luò)存在不穩(wěn)定因素,所以往往一個(gè)業(yè)務(wù)執(zhí)行的時(shí)間是很難具體的。這樣下來,如果設(shè)置的時(shí)間太短,最終可能會(huì)造成一個(gè)問題出現(xiàn):業(yè)務(wù)執(zhí)行時(shí)長(zhǎng)超過鎖過期時(shí)長(zhǎng),導(dǎo)致redis中key過期,最終redis釋放了鎖。而這個(gè)問題則會(huì)引發(fā)分布式鎖的ABC問題,如下:

線程A:獲取鎖成功,設(shè)置過期時(shí)間為5s,執(zhí)行業(yè)務(wù)邏輯
線程B:獲取鎖失敗,阻塞等待并在一定時(shí)間后重試獲取鎖
線程C:獲取鎖失敗,阻塞等待并在一定時(shí)間后重試獲取鎖
假設(shè)此時(shí)線程A執(zhí)行業(yè)務(wù),因?yàn)榫W(wǎng)絡(luò)波動(dòng)等原因,執(zhí)行總時(shí)長(zhǎng)花費(fèi)了7s,那么redis會(huì)在第五秒時(shí)將key刪除,因?yàn)檫^期了。
而恰巧線程B正好在第六秒時(shí),重試獲取鎖,喲嚯~,線程B一試,發(fā)現(xiàn)key不在了,自然就獲取鎖成功了。
到了第七秒時(shí),線程A執(zhí)行完了業(yè)務(wù),直接執(zhí)行了del lock_商品ID的指令,刪除了key,但此時(shí)這個(gè)鎖已經(jīng)是線程B的了,線程A的鎖已經(jīng)被redis放掉了,所以線程A釋放掉了線程B的鎖。
最后,線程C醒了,去重試時(shí),又發(fā)現(xiàn)redis中沒有了Key,也獲取鎖成功,執(zhí)行......

通過如上案例分析,不難發(fā)現(xiàn)一個(gè)問題,過期時(shí)間設(shè)置的太短,會(huì)導(dǎo)致鎖資源錯(cuò)亂,出現(xiàn)ABC問題。如上問題主要是由于兩個(gè)原因?qū)е碌模孩冁i過期時(shí)間太短 ②非加鎖線程也可以釋放鎖。第二個(gè)問題待會(huì)兒再解決,目前先看看鎖時(shí)長(zhǎng)怎么才能設(shè)置合理這個(gè)問題。

經(jīng)過前面分析,我們發(fā)現(xiàn)這個(gè)時(shí)間設(shè)置長(zhǎng)了不合適,要是短了那更不行,此時(shí)進(jìn)退兩難,那怎么解決呢?實(shí)際上也比較容易,可以設(shè)置一條子線程,給當(dāng)前鎖資源續(xù)命。

開啟一條子線程,間隔2-3s去查詢一次Key是否過期,如果過期了則代表業(yè)務(wù)線程已經(jīng)釋放了鎖,如果未過期,代表業(yè)務(wù)線程還在執(zhí)行業(yè)務(wù),那么則對(duì)于key的過期時(shí)間再加上5S秒鐘。為了避免業(yè)務(wù)線程死亡,當(dāng)前子線程一直續(xù)命,造成“長(zhǎng)生鎖”導(dǎo)致死鎖的情況出現(xiàn),可以把子線程變?yōu)闃I(yè)務(wù)線程的守護(hù)線程,這樣可以有效避免這個(gè)問題的出現(xiàn),實(shí)現(xiàn)如下:

// 續(xù)命子線程
public class GuardThread extends Thread {
    // 原本的key和過期時(shí)間
    private String lockKey;
    private int timeOut;
    private StringRedisTemplate stringRedisTemplate;
    
    private static boolean flag = true;

    public GuardThread(String lockKey, 
        int timeOut, StringRedisTemplate stringRedisTemplate){
        this.lockKey = lockKey;
        this.timeOut = timeOut;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void run() {
        // 開啟循環(huán)續(xù)命
        while (flag){
            try {
                // 先休眠一半的時(shí)間
                Thread.sleep(timeOut / 2 * 1000);
            }catch (Exception e){
                e.printStackTrace();
            }
            // 時(shí)間過了一半之后再去續(xù)命
            // 先查看key是否過期
            Long expire = stringRedisTemplate.getExpire(
                lockKey, TimeUnit.SECONDS);
            // 如果過期了,代表主線程釋放了鎖
            if (expire <= 0){
                // 停止循環(huán)
                flag = false;
            }
            // 如果還未過期
            // 再為則續(xù)命一半的時(shí)間
            stringRedisTemplate.expire(lockKey,expire
                + timeOut/2,TimeUnit.SECONDS);
        }
    }
}

// 扣庫(kù)存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // 獲取鎖
    String lockKey = "lock-" + inventory.getInventoryId();
    int timeOut = 10;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey, "竹子-熊貓",timeOut,TimeUnit.SECONDS);
    
    if(!flag){
        // 非阻塞式實(shí)現(xiàn)
        return "服務(wù)器繁忙...請(qǐng)稍后重試!?。?;
        // 自旋式實(shí)現(xiàn)(這種實(shí)現(xiàn)比較耗性能,
        // 實(shí)際開發(fā)過程中需配合阻塞時(shí)間配合使用)
        // return minusInventory(inventory);
    }
    
    // 創(chuàng)建子線程為鎖續(xù)命
    GuardThread guardThread = new
        GuardThread(lockKey,timeOut,stringRedisTemplate);
    // 設(shè)置為當(dāng)前 業(yè)務(wù)線程 的守護(hù)線程
    guardThread.setDaemon(true);
    guardThread.start();
    
    // ----只有獲取鎖成功才能執(zhí)行下述的減庫(kù)存業(yè)務(wù)----        
    try{
        // 查詢庫(kù)存信息
        Inventory inventoryResult =
            inventoryService.selectByPrimaryKey(inventory.getInventoryId());
        
        if (inventoryResult.getShopCount() <= 0) {
            return "庫(kù)存不足,請(qǐng)聯(lián)系賣家....";
        }
        
        // 扣減庫(kù)存
        inventoryResult.setShopCount(inventoryResult.getShopCount() - 1);
        int n = inventoryService.updateByPrimaryKeySelective(inventoryResult);
    } catch (Exception e) { // 確保業(yè)務(wù)出現(xiàn)異常也可以釋放鎖,避免死鎖
        // 釋放鎖
        stringRedisTemplate.delete(lockKey);
    }
    
    if (n > 0)
        return "端口-" + port + ",庫(kù)存扣減成功?。。?;
    return "端口-" + port + ",庫(kù)存扣減失敗?。?!";
}

如上實(shí)現(xiàn),利用了一條子線程為分布式鎖續(xù)命,同時(shí)為了確保主線程意外死亡等問題造成一直續(xù)命,所以將子線程變?yōu)榱酥?業(yè)務(wù))線程的守護(hù)線程,主線程死亡那么作為守護(hù)線程的子線程也會(huì)跟著死亡,可以有效避免“長(zhǎng)生鎖”的現(xiàn)象出現(xiàn)。

3.4、獲取鎖與釋放鎖線程一致性分析

在上一個(gè)問題中,我們?cè)岬揭_保加鎖與釋放鎖的線程一致性,這個(gè)問題比較好解決,只需要把value的值換成一個(gè)唯一值即可,然后在釋放鎖時(shí)判斷一下是否相等即可,如下:

// 扣庫(kù)存接口
@RequestMapping("/minusInventory")
public String minusInventory(Inventory inventory) {
    // 獲取鎖
    String lockKey = "lock-" + inventory.getInventoryId();
    // value值變?yōu)殡S機(jī)的UUID值
    String lockValue = UUID.randomUUID().toString();
    int timeOut = 10;
    Boolean flag = stringRedisTemplate.opsForValue()
            .setIfAbsent(lockKey,lockValue,timeOut,TimeUnit.SECONDS);
    
    // 省略其他代碼.....
    
    // ----只有獲取鎖成功才能執(zhí)行下述的減庫(kù)存業(yè)務(wù)----        
    try{
        // 省略其他代碼.....
    } catch (Exception e) {
        // 先判斷是否是當(dāng)前線程加的鎖
        if(lockVlue!=stringRedisTemplate.opsForValue().get(lockKey)){
            // 不是則拋出異常
            throw new RuntimeException("非法釋放鎖....");
        }
        // 確實(shí)是再釋放鎖
        stringRedisTemplate.delete(lockKey);
    }
    // 省略其他代碼.....
}

在獲取鎖的時(shí)候,把寫入redis的value值換成一個(gè)隨機(jī)的UUID,然后在釋放鎖之前,先判斷一下是否為當(dāng)前線程加的鎖,確實(shí)為當(dāng)前線程加的鎖那么則釋放,反之拋出異常。

3.5、Redis主從架構(gòu)鎖失效問題分析

在一般開發(fā)過程中,為了保證Redis的高可用,都會(huì)采用主從復(fù)制架構(gòu)做讀寫分離,從而提升Redis整體的吞吐量以及可用性。但問題在于:Redis的主從架構(gòu)下,實(shí)現(xiàn)分布式鎖時(shí)有可能會(huì)導(dǎo)致鎖失效,為什么呢?

因?yàn)閞edis主從架構(gòu)中的數(shù)據(jù)不是實(shí)時(shí)復(fù)制的,而是定時(shí)/定量復(fù)制。也就代表著一條數(shù)據(jù)寫入了redis主機(jī)后,并不會(huì)同時(shí)同步給所有的從機(jī),寫入的指令只要在主機(jī)上寫入成功會(huì)立即返回寫入成功,數(shù)據(jù)同步則是在一定時(shí)間或一定量之后才同步給從機(jī)。

這樣聽著感覺也沒啥問題,但再仔細(xì)一思考,如果8001的線程A剛剛往主機(jī)中寫入了Key,成功獲取到了分布式鎖,但redis主機(jī)還沒來得及把新數(shù)據(jù)同步給從機(jī),正巧因?yàn)橐馔忮礄C(jī)了,此時(shí)發(fā)生主從切換,而推選出來的新主也就是原本的舊從,因?yàn)榍懊驽礄C(jī)的主機(jī)還有部分?jǐn)?shù)據(jù)未復(fù)制過來,所以新主上不會(huì)有線程A的鎖記錄,此時(shí)8002的線程T1來嘗試獲取鎖,發(fā)生新主上沒有鎖記錄,那么則獲取鎖成功,此時(shí)又存在了兩條線程同時(shí)獲取到了鎖資源,同時(shí)執(zhí)行業(yè)務(wù)邏輯了。

OK~,如上描述的便是Redis主從架構(gòu)導(dǎo)致的分布式鎖失效問題,此時(shí)這個(gè)問題又該如何解決呢?方案如下:

  • ①紅鎖算法:多臺(tái)獨(dú)立的Redis同時(shí)寫入數(shù)據(jù),鎖失效時(shí)間之內(nèi),一半以上的機(jī)器寫成功則返回獲取鎖成功,否則返回獲取鎖失敗,失敗時(shí)會(huì)釋放掉那些成功的機(jī)器上的鎖。
    • 優(yōu)點(diǎn):可以完美解決掉主從架構(gòu)帶來的鎖失效問題
    • 缺點(diǎn):成本高,需要線上部署多臺(tái)獨(dú)立的Redis節(jié)點(diǎn)
    • 這種算法是Redis官方提出的解決方案:紅鎖算法
  • ②額外記錄鎖狀態(tài):再通過額外的中間件等獨(dú)立部署的節(jié)點(diǎn)記錄鎖狀態(tài),比如在DB中記錄鎖狀態(tài),在嘗試獲取分布式鎖之前需先查詢DB中的鎖持有記錄,如果還在持有則繼續(xù)阻塞,只有當(dāng)狀態(tài)為未持有時(shí)再嘗試獲取分布式鎖。
    • 優(yōu)點(diǎn):可以依賴于項(xiàng)目中現(xiàn)有的節(jié)點(diǎn)實(shí)現(xiàn),節(jié)約部署成本
    • 缺點(diǎn):
      • 實(shí)現(xiàn)需要配合定時(shí)器實(shí)現(xiàn)過期失效,保證鎖的合理失效機(jī)制
      • 獲取鎖的性能方面堪憂,會(huì)大大增加獲取鎖的性能開銷
      • 所有過程都需自己實(shí)現(xiàn),實(shí)現(xiàn)難度比較復(fù)雜
    • 總結(jié):這種方式類似于兩把分布式鎖疊加實(shí)現(xiàn),先獲取一把后再獲取另一把
  • ③Zookeeper實(shí)現(xiàn):使用Zookeeper代替Redis實(shí)現(xiàn),因?yàn)閆ookeeper追求的是高穩(wěn)定,所以Zookeeper實(shí)現(xiàn)分布式鎖時(shí),不會(huì)出現(xiàn)這個(gè)問題(稍后分析)

3.6、Redisson框架中的分布式鎖

在上述的內(nèi)容中,曾從分布式鎖的引出到自己實(shí)現(xiàn)的每個(gè)細(xì)節(jié)問題進(jìn)行了分析,但實(shí)際開發(fā)過程中并不需要我們自己去實(shí)現(xiàn),因?yàn)樽约簩?shí)現(xiàn)的分布式鎖多多少少會(huì)存在一些隱患問題。而這些工作實(shí)際已經(jīng)有框架封裝了,比如:Redisson框架,其內(nèi)部已經(jīng)基于redis為我們封裝好了分布式鎖,開發(fā)過程中屏蔽了底層處理,讓我們能夠像使用ReetrantLock一樣使用分布式鎖,如下:

/* ---------pom.xml文件-------- */
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.8.2</version>
</dependency>
/* ---------application.yml文件-------- */
spring:
    redis:
      database: 0
      host: 192.168.12.130
      port: 6379
      password: 123456
      timeout: 2m
// 注入redisson的客戶端
@Autowired
private RedissonClient redisson;

// 寫入redis的key值
String lockKey = "lock-" + inventory.getInventoryId();
// 獲取一個(gè)Rlock鎖對(duì)象
RLock lock = redisson.getLock(lockKey);
// 獲取鎖,并為其設(shè)置過期時(shí)間為10s
lock.lock(10,TimeUnit.SECONDS);
try{
    // 執(zhí)行業(yè)務(wù)邏輯....
} finally {
    // 釋放鎖
    lock.unlock();
}
/* ---------RedissonClient配置類-------- */      
@Configuration
public class RedissonConfig {
    // 讀取配置文件中的配置信息
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private String port;
    @Value("${spring.redis.password}")
    private String password;

    // 注入RedissonClient的Bean交由Spring管理
    @Bean
    public RedissonClient redisson() {
        //單機(jī)模式
        Config config = new Config();
        config.useSingleServer().
            setAddress("redis://" + host + ":" + port).
            setPassword(password).setDatabase(0);
        return Redisson.create(config);
    }
}

如上源碼,即可獲得一把最基本的分布式鎖,同時(shí)除開最基本的加鎖方法外,還支持其他形式的獲取鎖:

  • lock.tryLock(20,10,TimeUnit.SECONDS):非阻塞式獲取鎖,在獲取鎖失敗后的20s內(nèi)一直嘗試重新獲取鎖,超出20s則直接返回獲取鎖失敗
  • lock.lockAsync(10,TimeUnit.SECONDS):異步阻塞式獲取鎖,可以支持異步獲取加鎖的結(jié)果,該方法會(huì)返回一個(gè)Future對(duì)象,可通過Future對(duì)象異步獲取加鎖結(jié)果
  • lock.tryLockAsync(20,10,TimeUnit.SECONDS):異步非阻塞式獲取鎖,比上面那個(gè)多了一個(gè)超時(shí)時(shí)間

同時(shí)Redisson框架中的鎖實(shí)現(xiàn)還不僅僅只有一種,如下:

  • FairLock公平鎖:與ReetrantLock一樣,支持創(chuàng)建公平鎖,即先到的線程一定優(yōu)化獲取鎖
  • MultiLock連鎖:多個(gè)RLock對(duì)象組成一把鎖,也就是幾把鎖組成的一把鎖,可以用來實(shí)現(xiàn)紅鎖算法,因?yàn)?code>RLock對(duì)象可以不是一個(gè)Redisson創(chuàng)建出來的,也就是可以使用多個(gè)Redis客戶端的連接對(duì)象獲取多把鎖組成連鎖,只有當(dāng)所有個(gè)鎖獲取成功后,才能返回獲取鎖成功,如果獲取一把個(gè)鎖失敗,則返回獲取鎖失敗
  • RedLock紅鎖:和前面分析的Redis官方給出的紅鎖算法實(shí)現(xiàn)一致,繼承了連鎖,主要用于解決主從架構(gòu)鎖失效的問題

3.7、Redisson框架中的連鎖分析

連鎖向上繼承了RLock,向下為RedLock提供了實(shí)現(xiàn),所以它是Redisson框架中最為關(guān)鍵的一種鎖,先來看看它的使用方式:

// 獲取多個(gè)RLock鎖對(duì)象(redisson可以是不同的客戶端)
RLock lock1 = redisson.getLock("lock-1");
RLock lock2 = redisson.getLock("lock-2");
RLock lock3 = redisson.getLock("lock-3");

// 將多把鎖組合成一把連鎖,通過連鎖進(jìn)行獲取鎖與釋放鎖操作
RedissonMultiLock lock = new RedissonMultiLock(lock1,lock2,lock3);
// 獲取鎖:一半以上的鎖獲取成功才能成功,反之刪除寫入成功的節(jié)點(diǎn)數(shù)據(jù)
lock.lock();
// 釋放鎖
lock.unlock();

使用方式并不難理解,只需要?jiǎng)?chuàng)建多個(gè)RLock鎖對(duì)象后,再通過多個(gè)鎖對(duì)象組和成一把連鎖,通過連鎖對(duì)象進(jìn)行獲取鎖與釋放鎖的操作即可。

3.8、Redisson框架中的連鎖源碼實(shí)現(xiàn)分析

OK~,上面簡(jiǎn)單的給出了MultiLock連鎖的使用方式,接下來重點(diǎn)分析一下它的源碼實(shí)現(xiàn),源碼如下:

// RedissonMultiLock類 → lock()方法
public void lock() {
    try {
        // 調(diào)用了lockInterruptibly獲取鎖
        this.lockInterruptibly();
    } catch (InterruptedException var2) {
        // 如果出現(xiàn)異常則中斷當(dāng)前線程
        Thread.currentThread().interrupt();
    }
}

// RedissonMultiLock類 → lockInterruptibly()方法
public void lockInterruptibly() throws InterruptedException {
    // 這里傳入了-1
    this.lockInterruptibly(-1L, (TimeUnit)null);
}

// RedissonMultiLock類 → lockInterruptibly()重載方法
public void lockInterruptibly(long leaseTime, TimeUnit unit)
                throws InterruptedException {
    // 計(jì)算基礎(chǔ)阻塞時(shí)間:使用鎖的個(gè)數(shù)*1500ms。
    // 比如之前的案例:3*1500=4500ms
    long baseWaitTime = (long)(this.locks.size() * 1500);
    long waitTime = -1L;
    // 前面?zhèn)魅肓?1,所以進(jìn)入的是if分支
    if (leaseTime == -1L) {
        // 掛起時(shí)間為4500,單位毫秒(MS)
        waitTime = baseWaitTime;
        unit = TimeUnit.MILLISECONDS;
    } 
    // 這里是對(duì)于外部獲取鎖時(shí),指定了時(shí)間情況時(shí)的處理邏輯
    else {
        // 將外部傳入的時(shí)間轉(zhuǎn)換為毫秒值
        waitTime = unit.toMillis(leaseTime);
        // 如果外部給定的時(shí)間小于2000ms,那么賦值為2s
        if (waitTime <= 2000L) {
            waitTime = 2000L;
        } 
        // 如果傳入的時(shí)間小于前面計(jì)算出的基礎(chǔ)時(shí)間
        else if (waitTime <= baseWaitTime) {
            // 獲取基礎(chǔ)時(shí)間的一半,如baseWaitTime=4500ms,waitTime=2250ms
            waitTime = ThreadLocalRandom.current().
                nextLong(waitTime / 2L, waitTime);
        } else {
            // 如果外部給定的時(shí)間大于前面計(jì)算出的基礎(chǔ)時(shí)間會(huì)進(jìn)這里
            // 將基礎(chǔ)時(shí)間作為阻塞時(shí)長(zhǎng)
            waitTime = ThreadLocalRandom.current().
                nextLong(baseWaitTime, waitTime);
        }
        // 最終計(jì)算出掛起的時(shí)間
        waitTime = unit.convert(waitTime, TimeUnit.MILLISECONDS);
    }
    // 自旋嘗試獲取鎖,直至獲取鎖成功
    while(!this.tryLock(waitTime, leaseTime, unit)) {
        ;
    }
}

上述源碼中,實(shí)際上不難理解,比之前文章中分析的JUC的源碼可讀性強(qiáng)很多,上述代碼中,簡(jiǎn)單的計(jì)算了一下時(shí)間后,最終自旋調(diào)用了tryLock獲取鎖的方法一直嘗試獲取鎖。接著來看看tryLock方法:

// RedissonMultiLock類 → tryLock()方法
public boolean tryLock(long waitTime, long leaseTime,
        TimeUnit unit) throws InterruptedException {
    long newLeaseTime = -1L;
    // 如果外部獲取鎖時(shí),給定了過期時(shí)間
    if (leaseTime != -1L) {
        // 將newLeaseTime變?yōu)榻o定時(shí)間的兩倍
        newLeaseTime = unit.toMillis(waitTime) * 2L;
    }
    
    // 獲取當(dāng)前時(shí)間
    long time = System.currentTimeMillis();
    long remainTime = -1L;
    // 如果不是非阻塞式獲取鎖
    if (waitTime != -1L) {
        // 將過期時(shí)間改為用戶給定的時(shí)間
        remainTime = unit.toMillis(waitTime);
    }
    // 該方法是空實(shí)現(xiàn),留下的拓展接口,直接返回了傳入的值
    long lockWaitTime = this.calcLockWaitTime(remainTime);
    // 返回0,也是拓展接口,留給子類拓展的,紅鎖中就拓展了這兩方法
    // 這個(gè)變量是允許失敗的最大次數(shù),紅鎖中為個(gè)數(shù)的一半
    int failedLocksLimit = this.failedLocksLimit();
    // 獲取組成連鎖的所有RLock鎖集合
    List<RLock> acquiredLocks = new ArrayList(this.locks.size());
    // 獲取list的迭代器對(duì)象
    ListIterator iterator = this.locks.listIterator();
    
    // 通過List的迭代器遍歷整個(gè)連鎖集合
    while(iterator.hasNext()) {
        RLock lock = (RLock)iterator.next();
        
        boolean lockAcquired;
        // 嘗試獲取鎖
        try {
            // 如果是非阻塞式獲取鎖
            if (waitTime == -1L && leaseTime == -1L) {
                // 直接嘗試獲取鎖
                lockAcquired = lock.tryLock();
            } else {
                // 比較阻塞時(shí)間和過期時(shí)間的大小
                long awaitTime = Math.min(lockWaitTime, remainTime);
                // 嘗試重新獲取鎖
                lockAcquired = lock.tryLock(awaitTime, 
                    newLeaseTime, TimeUnit.MILLISECONDS);
            }
        // 如果redis連接中斷/關(guān)閉了
        } catch (RedisConnectionClosedException var21) {
            // 回滾獲取成功的鎖(刪除寫入成功的key)
            this.unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        // 如果在給定時(shí)間內(nèi)未獲取到鎖
        } catch (RedisResponseTimeoutException var22) {
            // 也回滾所有獲取成功的個(gè)鎖
            this.unlockInner(Arrays.asList(lock));
            lockAcquired = false;
        } catch (Exception var23) {
            // 如果是其他原因?qū)е碌?,則直接返回獲取鎖失敗
            lockAcquired = false;
        }
        
        // 如果獲取一把個(gè)鎖成功
        if (lockAcquired) {
            // 那么則記錄獲取成功的個(gè)鎖
            acquiredLocks.add(lock);
        } else {
            // 如果獲取一把個(gè)鎖失敗,此次失敗的次數(shù)已經(jīng)達(dá)到了
            // 最大的失敗次數(shù),那么直接退出循環(huán),放棄加鎖操作
            if (this.locks.size() - acquiredLocks.size() 
                == this.failedLocksLimit()) {
                break;
            }
            // 允許失敗的次數(shù)未0,獲取一個(gè)個(gè)鎖失敗則回滾
            if (failedLocksLimit == 0) {
                // 回滾所有成功的鎖 
                this.unlockInner(acquiredLocks);
                // 如果是非阻塞式獲取鎖,則直接返回獲取鎖失敗
                if (waitTime == -1L && leaseTime == -1L) {
                    return false;
                }
                // 獲取最新的失敗鎖的個(gè)數(shù)
                failedLocksLimit = this.failedLocksLimit();
                acquiredLocks.clear();
                // 移動(dòng)迭代器的指針位置到上一個(gè)
                while(iterator.hasPrevious()) {
                    iterator.previous();
                }
            
            // 如果允許失敗的次數(shù)不為0
            } else {
                // 每獲取個(gè)鎖失敗一次就減少一個(gè)數(shù)
                --failedLocksLimit;
            }
        }
        // 如果不是非阻塞式獲取鎖
        if (remainTime != -1L) {
            // 計(jì)算本次獲取鎖的所耗時(shí)長(zhǎng)
            remainTime -= System.currentTimeMillis() - time;
            time = System.currentTimeMillis();
            // 如果已經(jīng)超出了給定時(shí)間,則回滾所有成功的鎖
            if (remainTime <= 0L) {
                this.unlockInner(acquiredLocks);
                // 返回獲取鎖失敗
                return false;
            }
        }
    }
    
    // 能執(zhí)行到這里肯定是已經(jīng)獲取鎖成功了
    // 判斷是否設(shè)置了過期時(shí)間,如果設(shè)置了
    if (leaseTime != -1L) {
        // 獲取加鎖成功的個(gè)鎖集合
        List<RFuture<Boolean>> futures = new ArrayList(acquiredLocks.size());
        Iterator var25 = acquiredLocks.iterator();

        // 迭代為每個(gè)獲取成功的個(gè)鎖創(chuàng)建異步任務(wù)對(duì)象
        while(var25.hasNext()) {
            RLock rLock = (RLock)var25.next();
            RFuture<Boolean> future =
                rLock.expireAsync(unit.toMillis(leaseTime),
                TimeUnit.MILLISECONDS);
            futures.add(future);
        }
        // 獲取Future的個(gè)鎖集合迭代器對(duì)象
        var25 = futures.iterator();
        
        // 迭代每個(gè)Futrue對(duì)象
        while(var25.hasNext()) {
            RFuture<Boolean> rFuture = (RFuture)var25.next();
            // 異步為每個(gè)獲取個(gè)鎖成功的對(duì)象設(shè)置過期時(shí)間
            rFuture.syncUninterruptibly();
        }
    }
    // 返回獲取鎖成功
    return true;
}

如上源碼,流程先不分析,先感慨一句:雖然看著長(zhǎng),但?。?!真心的比JUC中的源碼可讀性和易讀性高N倍,每句代碼都容易弄懂,閱讀起來并不算費(fèi)勁。
OK~,感慨完之后來總結(jié)一下tryLock加鎖方法的總體邏輯:

  • ①計(jì)算出阻塞時(shí)間、最大失敗數(shù)以及過期時(shí)間,然后獲取所有組成連鎖的個(gè)鎖集合
  • ②迭代每把個(gè)鎖,嘗試對(duì)每把個(gè)鎖進(jìn)行加鎖,加鎖是也會(huì)判斷獲取鎖的方式是否為非阻塞式的:
    • 是:直接獲取鎖
    • 否:阻塞式獲取鎖,在給定時(shí)間內(nèi)會(huì)不斷嘗試獲取鎖
  • ③判斷個(gè)鎖是否獲取成功:
    • 成功:將獲取成功的個(gè)鎖添加到加鎖成功的集合acquiredLocks集合中
    • 失?。号袛啻舜潍@取鎖失敗的次數(shù)是否已經(jīng)達(dá)到了允許的最大失敗次數(shù):
      • 是:放棄獲取鎖,回滾所有獲取成功的鎖,返回獲取鎖失敗
      • 否:允許失敗次數(shù)自減,繼續(xù)嘗試獲取下一把個(gè)鎖
      • 注意:連鎖模式下最大失敗次數(shù)=0,紅鎖模式下為個(gè)鎖數(shù)量的一半
  • ④判斷目前獲取鎖過程的耗時(shí)是否超出了給定的阻塞時(shí)長(zhǎng):
    • 是:回滾所有獲取成功的鎖,然后返回獲取鎖失敗
    • 否:繼續(xù)獲取下把個(gè)鎖
  • ⑤如果連鎖獲取成功(代表所有個(gè)都鎖獲取成功),判斷是否指定了過期時(shí)間:
    • 是:異步為每個(gè)加鎖成功的個(gè)鎖設(shè)置過期時(shí)間并返回獲取鎖成功
    • 否:直接返回獲取鎖成功

雖然獲取鎖的代碼看著長(zhǎng),但其邏輯并不算復(fù)雜,上述過程是連鎖的實(shí)現(xiàn),而紅鎖則是依賴于連鎖實(shí)現(xiàn)的,也比較簡(jiǎn)單,只是重寫failedLocksLimit()獲取允許失敗次數(shù)的方法,允許獲取鎖失敗的次數(shù)變?yōu)榱藗€(gè)鎖數(shù)量的一半以及略微加了一些小拓展,感興趣的可以自己去分析其實(shí)現(xiàn)。

接著來看看釋放鎖的源碼實(shí)現(xiàn):

// RedissonMultiLock類 → unlock()方法
@Override
public void unlock() {
    // 創(chuàng)建為沒把個(gè)鎖創(chuàng)建一個(gè)Future
    List<RFuture<Void>> futures = new
        ArrayList<RFuture<Void>>(locks.size());
    // 遍歷所有個(gè)鎖
    for (RLock lock : locks) {
        // 釋放鎖
        futures.add(lock.unlockAsync());
    }
    // 阻塞等待所有鎖釋放成功后再返回
    for (RFuture<Void> future : futures) {
        future.syncUninterruptibly();
    }
}

// RedissonMultiLock類 → unlockInnerAsync()方法
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    // 獲取個(gè)鎖的Key名稱并通過Lua腳本釋放鎖(確保原子性)
    return commandExecutor.evalWriteAsync(getName(), 
        LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
        "if (redis.call('exists', KEYS[1]) == 0) then " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; " +
        "end;" +
        "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
            "return nil;" +
        "end; " +
        "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
        "if (counter > 0) then " +
            "redis.call('pexpire', KEYS[1], ARGV[2]); " +
            "return 0; " +
        "else " +
            "redis.call('del', KEYS[1]); " +
            "redis.call('publish', KEYS[2], ARGV[1]); " +
            "return 1; "+
        "end; " +
        "return nil;",
        Arrays.<Object>asList(getName(), getChannelName()),
        LockPubSub.unlockMessage, internalLockLeaseTime,
        getLockName(threadId));
}

釋放鎖的邏輯更加簡(jiǎn)單,遍歷所有的個(gè)鎖,然后異步通過Lua腳本刪除所有的key,在連鎖的釋放代碼中會(huì)同步阻塞等待所有鎖的Key刪除后再返回。

四、Zookeeper實(shí)現(xiàn)分布式鎖剖析

Zookeeper分布式鎖是依賴于其內(nèi)部的順序臨時(shí)節(jié)點(diǎn)實(shí)現(xiàn)的,其原理就類似于最開始舉例的那個(gè)文件夾分布式鎖,因?yàn)閆ookeeper實(shí)際上就類似于一個(gè)文件系統(tǒng)的結(jié)構(gòu)。我們可以通過Curator框架封裝的API操作Zookeeper,完成分布式鎖的實(shí)現(xiàn)。如下:

// 創(chuàng)建分布式鎖對(duì)象
InterProcessMutex lock = InterProcessMutex(client,
    "/locks/distributed_商品ID");
lock.acquire(); // 獲取鎖/加鎖

// 執(zhí)行業(yè)務(wù)邏輯...

lock.release(); // 釋放鎖

如上,通過Curator實(shí)現(xiàn)分布式鎖非常簡(jiǎn)單,因?yàn)橐呀?jīng)封裝好了API,所以應(yīng)用起來也非常方便,同時(shí)Zookeeper也可以實(shí)現(xiàn)公平鎖與非公平鎖兩種方案,如下:

  • 公平鎖:先請(qǐng)求鎖的線程一定先獲取鎖
    • 實(shí)現(xiàn)方式:通過臨時(shí)順序節(jié)點(diǎn)實(shí)現(xiàn),每條線程請(qǐng)求鎖時(shí)為其創(chuàng)建一個(gè)有序節(jié)點(diǎn),創(chuàng)建完成之后判斷自己創(chuàng)建的節(jié)點(diǎn)是不是最小的,如果是則直接獲取鎖成功,反之獲取鎖失敗,創(chuàng)建一個(gè)監(jiān)聽器,監(jiān)聽自己節(jié)點(diǎn)的前一個(gè)節(jié)點(diǎn)狀態(tài),當(dāng)前一個(gè)節(jié)點(diǎn)被刪除(代表前一個(gè)節(jié)點(diǎn)的創(chuàng)建線程釋放了鎖)自己嘗試獲取鎖
    • 優(yōu)劣勢(shì):可以保證請(qǐng)求獲取鎖的有序性,但性能方面比非公平鎖低
  • 非公平鎖:先請(qǐng)求鎖的線程不一定先獲取鎖
    • 實(shí)現(xiàn)方式:多條線程在同一目錄下,同時(shí)創(chuàng)建一個(gè)名字相同的節(jié)點(diǎn),誰創(chuàng)建成功代表獲取鎖成功,反之則代表獲取鎖失敗
    • 優(yōu)劣勢(shì):性能良好,但無法保證請(qǐng)求獲取鎖時(shí)的有序性

對(duì)于這兩種實(shí)現(xiàn)方式,非公平鎖的方案與前面的Redis實(shí)現(xiàn)差不多,所以不再分析。下面重點(diǎn)來分析一下Zookeeper實(shí)現(xiàn)分布式的公平鎖的大致原理。但在分析之前先簡(jiǎn)單說明一些Zookeeper中會(huì)用到的概念。如下:

  • 節(jié)點(diǎn)類型:
    • ①持久節(jié)點(diǎn):被創(chuàng)建后會(huì)一直存在的節(jié)點(diǎn)信息,除非有刪除操作主動(dòng)清楚才會(huì)銷毀
    • ②持久順序節(jié)點(diǎn):持久節(jié)點(diǎn)的有序版本,每個(gè)新創(chuàng)建的節(jié)點(diǎn)會(huì)在后面維護(hù)自增值保持先后順序,可以用于實(shí)現(xiàn)分布式全局唯一ID
    • ③臨時(shí)節(jié)點(diǎn):被創(chuàng)建后與客戶端的會(huì)話生命周期保持一致,連接斷開則自動(dòng)銷毀
    • ④臨時(shí)順序節(jié)點(diǎn):臨時(shí)節(jié)點(diǎn)的有序版本,與其多了一個(gè)有序性。分布式鎖則依賴這種類型實(shí)現(xiàn)
  • 監(jiān)視器:當(dāng)zookeeper創(chuàng)建一個(gè)節(jié)點(diǎn)時(shí),會(huì)為該節(jié)點(diǎn)注冊(cè)一個(gè)監(jiān)視器,當(dāng)節(jié)點(diǎn)狀態(tài)發(fā)生改變時(shí),watch會(huì)被觸發(fā),zooKeeper將會(huì)向客戶端發(fā)送一條通知。不過值得注意的是watch只能被觸發(fā)一次

ok~,假設(shè)目前8001服務(wù)中的線程T1嘗試獲取鎖,那么會(huì)T1會(huì)在Zookeeper/locks/distributed_商品ID目錄下創(chuàng)建一個(gè)臨時(shí)節(jié)點(diǎn),Zookeeper內(nèi)部會(huì)生成一個(gè)名字為xxx....-0000001臨時(shí)順序節(jié)點(diǎn)。當(dāng)?shù)诙l線程來嘗試獲取鎖時(shí),也會(huì)在相同位置創(chuàng)建一個(gè)臨時(shí)順序節(jié)點(diǎn),名字為xxx....-0000002。值得注意的是最后的數(shù)字是一個(gè)遞增的狀態(tài),從1開始自增,Zookeeper會(huì)維護(hù)這個(gè)先后順序。如下圖:

創(chuàng)建臨時(shí)節(jié)點(diǎn)

當(dāng)線程創(chuàng)建節(jié)點(diǎn)完成后,會(huì)查詢/locks/distributed_商品ID目錄下所有的子節(jié)點(diǎn),然后會(huì)判斷自己創(chuàng)建的節(jié)點(diǎn)是不是在所有節(jié)點(diǎn)的第一個(gè),也就是判斷自己的節(jié)點(diǎn)是否為最小的子節(jié)點(diǎn),如果是的話則獲取鎖成功,因?yàn)楫?dāng)前線程是第一個(gè)來獲取分布式鎖的線程,在它之前是沒有線程獲取鎖的,所以當(dāng)然可以加鎖成功,然后開始執(zhí)行業(yè)務(wù)邏輯。如下:
8001線程T1獲取分布式鎖

而第二條線程創(chuàng)建節(jié)點(diǎn)成功后,也會(huì)去判斷自己是否是最小的節(jié)點(diǎn)。哦豁!第二條線程判斷的時(shí)候會(huì)發(fā)現(xiàn),在自己的節(jié)點(diǎn)之前還有一個(gè)xxx...-0001節(jié)點(diǎn),所以代表在自己前面已經(jīng)有線程持有了分布式鎖,所以會(huì)對(duì)上個(gè)節(jié)點(diǎn)加上一個(gè)監(jiān)視器,監(jiān)視上個(gè)節(jié)點(diǎn)的狀態(tài)變化。如下:
Zookeeper實(shí)現(xiàn)分布式公平鎖

此時(shí),第一條線程T1執(zhí)行完了業(yè)務(wù)代碼,準(zhǔn)備釋放鎖,也就是刪除自己創(chuàng)建的xxx...-0001臨時(shí)順序節(jié)點(diǎn)。而第二條線程創(chuàng)建的監(jiān)視器會(huì)監(jiān)視著前面一個(gè)節(jié)點(diǎn)的狀態(tài),當(dāng)發(fā)現(xiàn)前面的節(jié)點(diǎn)已經(jīng)被刪除時(shí),就知道前面一條線程已經(jīng)執(zhí)行完了業(yè)務(wù),釋放了鎖資源,所以再次嘗試獲取鎖。如下:
Zookeeper實(shí)現(xiàn)分布式鎖完整流程

第二條線程重新嘗試獲取鎖時(shí),拿到當(dāng)前目錄下的所有節(jié)點(diǎn)判斷發(fā)現(xiàn),喲!自己是第一個(gè)(最小的那個(gè))節(jié)點(diǎn)啊?然后獲取鎖成功,開始執(zhí)行業(yè)務(wù)邏輯,后續(xù)再來新的線程則依次類推.....

至此,整個(gè)Zookeeper實(shí)現(xiàn)分布式鎖的過程分析完畢,關(guān)于自己動(dòng)手基于Zookeeper實(shí)現(xiàn)一遍我這邊就不再寫了,大家可以自習(xí)了解。實(shí)際開發(fā)過程中,Curator框架自帶的分布式鎖實(shí)現(xiàn)便已經(jīng)夠用了,同時(shí)使用也非常的方便。

五、分布式鎖性能優(yōu)化

經(jīng)過前述的分析,大家對(duì)分布式鎖應(yīng)該都有一個(gè)全面認(rèn)知了,但是請(qǐng)思考:如果對(duì)于類似于搶購(gòu)、秒殺業(yè)務(wù),又該如何處理呢?因?yàn)樵谶@種場(chǎng)景下,往往在一段時(shí)間內(nèi)會(huì)有大量用戶去請(qǐng)求同一個(gè)商品。從技術(shù)角度出發(fā),這樣會(huì)導(dǎo)致在同一時(shí)間內(nèi)會(huì)有大量的線程去請(qǐng)求同一把鎖。這會(huì)有何種隱患呢?會(huì)出現(xiàn)的問題是:雖然并發(fā)能抗住,但是對(duì)于用戶體驗(yàn)感不好,同時(shí)大量的用戶點(diǎn)擊搶購(gòu),但是只能有一個(gè)用戶搶購(gòu)成功,明顯不合理,這又該如何優(yōu)化?

參考并發(fā)容器中的分段容器,可以將共享資源(商品庫(kù)存)做提前預(yù)熱,分段分散到redis中。舉個(gè)例子:

1000個(gè)庫(kù)存商品,10W個(gè)用戶等待搶購(gòu),搶購(gòu)開始時(shí)間為下午15:00
提前預(yù)熱:兩點(diǎn)半左右開始將商品數(shù)量拆成10份或N份,如:[shopping_01;0-100]、[shopping_02;101-200]、[shopping_03;201-300]、[......]
也就是往redis中寫入十個(gè)key,值為100,在搶購(gòu)時(shí),過來的請(qǐng)求隨機(jī)分散到某個(gè)key上去,但是在扣減庫(kù)存之前,需要先獲取鎖,這樣就同時(shí)有了十把鎖,性能自然就上去了。

六、分布式鎖總結(jié)

本篇中從單機(jī)鎖的隱患 -> 分布式架構(gòu)下的安全問題引出 -> 分布式鎖的實(shí)現(xiàn)推導(dǎo) -> redis實(shí)現(xiàn)分布式鎖 -> redis實(shí)現(xiàn)分布式鎖的細(xì)節(jié)問題分析 -> redisson框架實(shí)現(xiàn)及其連鎖應(yīng)用與源碼分析 -> zookeeper實(shí)現(xiàn)分布式鎖 -> zookeeper實(shí)現(xiàn)原理這條思路依次剖析了分布式鎖的前世今生,總的一句話概括分布式鎖的核心原理就是:在多個(gè)進(jìn)程中所有線程都可見的區(qū)域?qū)崿F(xiàn)了互斥量而已。

最后再來說說Redis與Zookeeper實(shí)現(xiàn)的區(qū)別與項(xiàng)目中如何抉擇?

Redis數(shù)據(jù)不是實(shí)時(shí)同步的,主機(jī)寫入成功后會(huì)立即返回,存在主從架構(gòu)鎖失效問題。
Zookeeper數(shù)據(jù)是實(shí)時(shí)同步的,主機(jī)寫入后需一半節(jié)點(diǎn)以上寫入成功才會(huì)返回。
所以如果你的項(xiàng)目追求高性能,可以放棄一定的穩(wěn)定性,那么推薦使用Redis實(shí)現(xiàn)。比如電商、線上教育等類型的項(xiàng)目。
但如果你的項(xiàng)目追求高穩(wěn)定,愿意犧牲一部分性能換取穩(wěn)定性,那么推薦使用Zookeeper實(shí)現(xiàn)。比如金融、銀行、政府等類型的項(xiàng)目。

當(dāng)然,如果你的項(xiàng)目是基于SpringCloud開發(fā)的,也可以考慮使用SpringCloud的全局鎖,但是不推薦,一般還是優(yōu)先考慮Redis和Zookeeper。

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

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