3、分布式鎖

對于分布式鎖可以從幾個(gè)問題入手:

  1. 分布式鎖應(yīng)用的場景是什么樣的呢?
  2. 分布式鎖的實(shí)現(xiàn)是怎么樣的呢?
  3. 分布式鎖的實(shí)現(xiàn)方案應(yīng)用有什么優(yōu)劣勢?
  4. 分布式鎖的方案如何選擇?等

一、分布式鎖概念

1、什么是分布式鎖
  • 分布式鎖在分布式環(huán)境中,針對多個(gè)進(jìn)程訪問共享資源而提供的同步方案。相對于單進(jìn)程的鎖,分布式鎖更加復(fù)雜,分布式的不可靠性,需要分布式鎖考慮更多的東西。
  • 分布式鎖作用:保證共享資源的正確性,某一時(shí)刻鎖定全局資源,讓進(jìn)程串行化執(zhí)行,能有效避免共享資源被多個(gè)進(jìn)程同時(shí)訪問時(shí)候出現(xiàn)的數(shù)據(jù)正確性問題。比如防止重復(fù)下單、解決業(yè)務(wù)層冪等問題、解決消息隊(duì)列重復(fù)消費(fèi)問題、解決數(shù)據(jù)不一致問題等等
2、分布式鎖的設(shè)計(jì)目標(biāo)

1、可以保證在分布式部署的應(yīng)用集群中,同一個(gè)方法在同一時(shí)間只能被一臺(tái)機(jī)器上的一個(gè)線程執(zhí)行。
2、這把鎖要是一把可重入鎖(避免死鎖)
3、這把鎖最好是一把阻塞鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
4、這把鎖最好是一把公平鎖(根據(jù)業(yè)務(wù)需求考慮要不要這條)
5、有高可用的獲取鎖和釋放鎖功能(服務(wù)高可用,系統(tǒng)穩(wěn)健
6、獲取鎖和釋放鎖的性能要好
7、鎖的自動(dòng)續(xù)約與自動(dòng)釋放
8、代碼高度抽象,業(yè)務(wù)接入非常簡單
9、可視化的管理后臺(tái),監(jiān)控與管理

3、常見分布式鎖解決方案

MySql
Zk
Redis
自研分布式鎖:如谷歌的Chubby。
etcd

二、基于mysql實(shí)現(xiàn)分布式鎖
1、基于表主鍵唯一做分布式鎖

原理:利用主鍵唯一的特性,如果有多個(gè)請求同時(shí)提交到數(shù)據(jù)庫的話,數(shù)據(jù)庫會(huì)保證只有一個(gè)操作可以成功,那么我們就可以認(rèn)為操作成功的那個(gè)線程獲得了該方法的鎖,當(dāng)方法執(zhí)行完畢之后,想要釋放鎖的話,刪除這條數(shù)據(jù)庫記錄即可。(聯(lián)合主鍵或者唯一主鍵但是不能使自增主鍵)

  • 存在的問題

1、這把鎖強(qiáng)依賴數(shù)據(jù)庫的可用性,數(shù)據(jù)庫是一個(gè)單點(diǎn),一旦數(shù)據(jù)庫掛掉,會(huì)導(dǎo)致業(yè)務(wù)系統(tǒng)不可用。
2、這把鎖沒有失效時(shí)間,一旦解鎖操作失敗,就會(huì)導(dǎo)致鎖記錄一直在數(shù)據(jù)庫中,其他線程無法再獲得到鎖。
3、這把鎖只能是非阻塞的,因?yàn)閿?shù)據(jù)的insert操作,一旦插入失敗就會(huì)直接報(bào)錯(cuò)。沒有獲得鎖的線程并不會(huì)進(jìn)入排隊(duì)隊(duì)列,要想再次獲得鎖就要再次觸發(fā)獲得鎖操作。
4、這把鎖是非重入的,同一個(gè)線程在沒有釋放鎖之前無法再次獲得該鎖。因?yàn)閿?shù)據(jù)中數(shù)據(jù)已經(jīng)存在了。
5、這把鎖是非公平鎖,所有等待鎖的線程憑運(yùn)氣去爭奪鎖。

  • 當(dāng)然,我們也可以有其他方式解決上面的問題。

1、數(shù)據(jù)庫是單點(diǎn)?搞兩個(gè)數(shù)據(jù)庫,數(shù)據(jù)之前雙向同步。一旦掛掉快速切換到備庫上。
2、沒有失效時(shí)間?只要做一個(gè)定時(shí)任務(wù),每隔一定時(shí)間把數(shù)據(jù)庫中的超時(shí)數(shù)據(jù)清理一遍。
3、非阻塞的?搞一個(gè)while循環(huán),直到insert成功再返回成功。
4、非重入的?在數(shù)據(jù)庫表中加個(gè)字段,記錄當(dāng)前獲得鎖的機(jī)器的主機(jī)信息和線程信息,那么下次再獲取鎖的時(shí)候先查詢數(shù)據(jù)庫,如果當(dāng)前機(jī)器的主機(jī)信息和線程信息在數(shù)據(jù)庫可以查到的話,直接把鎖分配給他就可以了。
5、非公平的?再建一張中間表,將等待鎖的線程全記錄下來,并根據(jù)創(chuàng)建時(shí)間排序,只有最先創(chuàng)建的允許獲取鎖

  • 這種方案總體來說性能依靠于mysql,且會(huì)產(chǎn)生鎖表等現(xiàn)象,在高并發(fā)場景是不適用的。
2、基于數(shù)據(jù)庫排他鎖做分布式鎖

在查詢語句后面增加for update,數(shù)據(jù)庫會(huì)在查詢過程中給數(shù)據(jù)庫表增加排他鎖 (注意: InnoDB 引擎在加鎖的時(shí)候,只有通過索引進(jìn)行檢索的時(shí)候才會(huì)使用行級(jí)鎖,否則會(huì)使用表級(jí)鎖。這里我們希望使用行級(jí)鎖,就要給要執(zhí)行的方法字段名添加索引,值得注意的是,這個(gè)索引一定要?jiǎng)?chuàng)建成唯一索引,否則會(huì)出現(xiàn)多個(gè)重載方法之間無法同時(shí)被訪問的問題。重載方法的話建議把參數(shù)類型也加上。)。當(dāng)某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
思路:我們可以認(rèn)為獲得排他鎖的線程即可獲得分布式鎖,當(dāng)獲取到鎖之后,可以執(zhí)行方法的業(yè)務(wù)邏輯,執(zhí)行完方法之后,通過connection.commit()操作來釋放鎖。
示例:

BEGIN;(確保以下2步驟在一個(gè)事務(wù)中:)
SELECT * FROM tb_product_stock WHERE product_id=1 FOR UPDATE--->product_id有索引,鎖行,無索引,表鎖(加鎖階段)
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1--->更新庫存
COMMIT;( 解鎖階段)

  • 存在的問題

性能依賴于mysql,同時(shí)雖然使用了索引,在查詢優(yōu)化的時(shí)候未必使用行鎖有可能使用了表鎖。也有可能引發(fā)死鎖等其他意外發(fā)生

3、基于版本控制實(shí)現(xiàn)的樂觀鎖
  • 這個(gè)策略源于 mysql 的 mvcc 機(jī)制,使用這個(gè)策略其實(shí)本身沒有什么問題,唯一的問題就是對數(shù)據(jù)表侵入較大,我們要為每個(gè)表設(shè)計(jì)一個(gè)版本號(hào)字段,然后寫一條判斷 sql 每次進(jìn)行判斷,增加了數(shù)據(jù)庫操作的次數(shù),在高并發(fā)的要求下,對數(shù)據(jù)庫連接的開銷也是無法忍受的。
    思路:我們可以對我們的表加一個(gè)版本號(hào)字段,那么我們查詢出來一個(gè)版本號(hào)之后,update或者delete的時(shí)候需要依賴我們查詢出來的版本號(hào),判斷當(dāng)前數(shù)據(jù)庫和查詢出來的版本號(hào)是否相等,如果相等那么就可以執(zhí)行,如果不等那么就不能執(zhí)行。這樣的一個(gè)策略很像我們的CAS(Compare And Swap),比較并交換是一個(gè)原子操作
    示例:

BEGIN;(確保以下2步驟在一個(gè)事務(wù)中:)
SELECT number FROM tb_product_stock WHERE product_id=1--》查詢庫存總數(shù),不加鎖
UPDATE tb_product_stock SET number=number-1 WHERE product_id=1 AND number=第一步查詢到的庫存數(shù)--》number字段作為版本控制字段(這里版本的控制可以根據(jù)業(yè)務(wù)調(diào)整)
COMMIT;

4、mysql作為分布式鎖總結(jié)
  • 優(yōu)點(diǎn):直接借助數(shù)據(jù)庫,容易理解。缺點(diǎn):會(huì)有各種各樣的問題,在解決問題的過程中會(huì)使整個(gè)方案變得越來越復(fù)雜。操作數(shù)據(jù)庫需要一定的開銷,性能問題需要考慮。也就是說在高并發(fā)場景數(shù)據(jù)庫作為分布式鎖并不適用。
三、redis作為分布式鎖
  • redis實(shí)現(xiàn)分布式鎖原理主要是利用redis中setNX命令的原子性操作來實(shí)現(xiàn)的。
1、基于jedis實(shí)現(xiàn)分布式鎖
/**
 * 基于jedis實(shí)現(xiàn)的分布式鎖
 */
public class RedisDistributeLockUtils {

    private static final String LOCK_SUCCESS = "OK";
    private static final String SET_IF_NOT_EXIST = "NX";
    private static final String SET_WITH_EXPIRE_TIME = "PX";

    private static final Long RELEASE_SUCCESS = 1L;

    /**
     * @param jedis
     * @param lockKey
     * @param requestId   通過給value賦值為requestId,我們就知道這把鎖是哪個(gè)請求加的了,在解鎖的時(shí)候就可以有依據(jù)。
     *                    requestId可以使用UUID.randomUUID().toString()方法生成。也就是鎖誰加的鎖誰來釋放
     * @param expiredTime
     * @return
     */
    public boolean getDistributeLock(Jedis jedis, String lockKey, String requestId, Long expiredTime) {
        String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expiredTime);
        if (LOCK_SUCCESS.equalsIgnoreCase(result)) {
            return true;
        }
        return false;
    }

    public boolean releaseDistributeLock(Jedis jedis, String lockKey, String requestId) {
        /**
         * 1、過程:將Lua代碼傳到j(luò)edis.eval()方法里,并使參數(shù)KEYS[1]賦值為lockKey,
         * ARGV[1]賦值為requestId。eval()方法是將Lua代碼交給Redis服務(wù)端執(zhí)行。
         * 2、原理:首先獲取鎖對應(yīng)的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。
         * 那么為什么要使用Lua語言來實(shí)現(xiàn)呢?因?yàn)橐_保上述操作是原子性的。
         */
        String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));

        if (RELEASE_SUCCESS.equals(result)) {
            return true;
        }
        return false;
    }
}
  • 上面沒有考慮使用高可用情況,在通常的互聯(lián)網(wǎng)架構(gòu)中都是使用主從或者集群模式來實(shí)現(xiàn)redis的高可用。而集群模式是基于redis分片技術(shù),并不適合作為redis分布式鎖高可用方案,因此常用的是redis主從模式,在通常99.9999%的情況下面,redis是沒問題,但是當(dāng)主redis掛掉時(shí)候,而主從數(shù)據(jù)異步進(jìn)行時(shí)候,訪問此時(shí)從服務(wù)器中還沒有數(shù)據(jù),就會(huì)存在鎖失效問題。也就是說在極端情況下面會(huì)存在數(shù)據(jù)不一致問題。這也和redis集群是AP模型吻合。
  • 在redis官方文檔中,推薦使用redlock來保證一致性問題。但是至少需要三個(gè)redis主從實(shí)例來完成,下面說說redlock算法實(shí)現(xiàn)。
2、redlock算法實(shí)現(xiàn)分布式鎖
  • 在java中,實(shí)現(xiàn)了redlock鎖的開源框架是redisson,redLock實(shí)現(xiàn)分布式鎖的原理是:假設(shè)我們有N個(gè)Redis master節(jié)點(diǎn),這些節(jié)點(diǎn)都是完全獨(dú)立的,我們不用任何復(fù)制或者其他隱含的分布式協(xié)調(diào)算法。我們已經(jīng)描述了如何在單節(jié)點(diǎn)環(huán)境下安全地獲取和釋放鎖。因此我們理所當(dāng)然地應(yīng)當(dāng)用這個(gè)方法在每個(gè)單節(jié)點(diǎn)里來獲取和釋放鎖。在我們的例子里面我們把N設(shè)成5,這個(gè)數(shù)字是一個(gè)相對比較合理的數(shù)值,因此我們需要在不同的計(jì)算機(jī)或者虛擬機(jī)上運(yùn)行5個(gè)master節(jié)點(diǎn)來保證他們大多數(shù)情況下都不會(huì)同時(shí)宕機(jī)。一個(gè)客戶端需要做如下操作來獲取鎖:

1、獲取當(dāng)前時(shí)間(單位是毫秒)。
2、輪流用相同的key和隨機(jī)值在N個(gè)節(jié)點(diǎn)上請求鎖,在這一步里,客戶端在每個(gè)master上請求鎖時(shí),會(huì)有一個(gè)和總的鎖釋放時(shí)間相比小的多的超時(shí)時(shí)間。比如如果鎖自動(dòng)釋放時(shí)間是10秒鐘,那每個(gè)節(jié)點(diǎn)鎖請求的超時(shí)時(shí)間可能是5-50毫秒的范圍,這個(gè)可以防止一個(gè)客戶端在某個(gè)宕掉的master節(jié)點(diǎn)上阻塞過長時(shí)間,如果一個(gè)master節(jié)點(diǎn)不可用了,我們應(yīng)該盡快嘗試下一個(gè)master節(jié)點(diǎn)。
3、客戶端計(jì)算第二步中獲取鎖所花的時(shí)間,只有當(dāng)客戶端在大多數(shù)master節(jié)點(diǎn)上成功獲取了鎖(在這里是3個(gè)),而且總共消耗的時(shí)間不超過鎖釋放時(shí)間,這個(gè)鎖就認(rèn)為是獲取成功了。
4、如果鎖獲取成功了,那現(xiàn)在鎖自動(dòng)釋放時(shí)間就是最初的鎖釋放時(shí)間減去之前獲取鎖所消耗的時(shí)間。
5、如果鎖獲取失敗了,不管是因?yàn)楂@取成功的鎖不超過一半(N/2+1)還是因?yàn)榭傁臅r(shí)間超過了鎖釋放時(shí)間,客戶端都會(huì)到每個(gè)master節(jié)點(diǎn)上釋放鎖,即便是那些他認(rèn)為沒有獲取成功的鎖。

  • springboot+redisson實(shí)現(xiàn)分布式鎖:
    (1)、導(dǎo)入包
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.5.0</version>
        </dependency>

(2)、配置文件

spring.redisson.password=123456
spring.redisson.clusters=127.0.0.1:6370,127.0.0.1:6371,127.0.0.1:6372

(3)配置redisson

@Configuration
public class RedissonConfig {
    @Value("${spring.redisson.clusters}")
    private  String cluster;
    @Value("${spring.redisson.password}")
    private String password;
    
    @Bean
    public RedissonClient getRedisson(){
        String[] nodes = cluster.split(",");
        //redisson版本是3.5,集群的ip前面要加上“redis://”,不然會(huì)報(bào)錯(cuò),3.2版本可不加
        for(int i=0;i<nodes.length;i++){
            nodes[i] = "redis://"+nodes[i];
        }
        RedissonClient redisson = null;
        Config config = new Config();
        config.useClusterServers() //這是用的集群server
        .setScanInterval(2000) //設(shè)置集群狀態(tài)掃描時(shí)間 
        .addNodeAddress(nodes)
        .setPassword(password);
        redisson = Redisson.create(config);
        return redisson;
    }
}

(4)分布式鎖使用

@Service
public class DistributeLockServiceImpl{
    @Autowired
    private RedissonClient redissonClient;

private final static String LOCK_KEYP_PREFIX="redisson:lock:test:";
    @overiide
    public void redissonTest(Long userId) {
       //獲取key
        String redisLockKey=LOCK_KEYP_PREFIX.concat(String.ValueOf(userId));
       //獲取redisson鎖
        RLock rlock = redissonClient.getLock(redisLockKey);
  //設(shè)置鎖超時(shí)時(shí)間,防止異常造成死鎖
        rlock.lock(20, TimeUnit.SECONDS);
        try{
           //todo 執(zhí)行業(yè)務(wù)邏輯
        } catch(Exception e){
            //異常處理
        }finally{
            //釋放鎖
            rlock.unlock();
        }  
    }
}
5、redLock算法實(shí)現(xiàn)的分布式鎖總結(jié)

三、zookeeper實(shí)現(xiàn)的分布式鎖

四、基于etcd實(shí)現(xiàn)的分布式鎖

  • etcd和zk一樣作為分布式一致性的解決方案,具有zk的所有功能,就是說zk能做的etcd也能做。etcd是基于raft協(xié)議的一致性框架,相對paxos算法,更加容易理解,而且etcd提供了http+json的調(diào)用格式,更加符合restful風(fēng)格,還有一點(diǎn)重點(diǎn)的,基于etcd相對zk,性能會(huì)更加好。
  • 由于對于etcd不是很熟悉,后續(xù)學(xué)習(xí)了在動(dòng)手寫個(gè)demo。

五、幾種主流的分布鎖對比

  • 在生產(chǎn)上面對高并發(fā)基本不會(huì)使用數(shù)據(jù)作為分布式鎖的解決方法,絕大數(shù)情況是用redis作為分布式鎖的解決方案,但是對于一致性要求特別高的比如交易等系統(tǒng)內(nèi)就要考慮使用zk或者etcd實(shí)現(xiàn)的分布式鎖。具體三者的比較如下:


    分布式鎖實(shí)現(xiàn)的主流方案對比

參考:
http://www.importnew.com/27477.html
https://juejin.im/post/59f592c65188255f5c5142d2
https://zhuanlan.zhihu.com/p/42056183
https://juejin.im/post/5bbb0d8df265da0abd3533a5
http://www.spring4all.com/question/158

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 最近看了極客時(shí)間左耳聽風(fēng)的專欄,對于分布式系統(tǒng)的設(shè)計(jì)有了更深的認(rèn)識(shí),準(zhǔn)備結(jié)合陳皓的總結(jié)加上自己看過的資料對于分布式...
    仰泳的雙魚閱讀 3,761評(píng)論 0 23
  • 在很多環(huán)境下,多個(gè)不同的進(jìn)程需要以排他的形式使用共享資源,這是使用分布式鎖機(jī)制是一種傳統(tǒng)但有效的方案。 有很多的庫...
    BigFish__閱讀 1,909評(píng)論 0 0
  • 窗外 一縷一縷的春風(fēng) 一樹一樹的花開 此時(shí)是人間最美的四月天 我的心里 一絲一絲的寒涼 一點(diǎn)一點(diǎn)的死去 此時(shí)是世間...
    遇見子魚閱讀 434評(píng)論 4 14
  • 一. 小時(shí)候,老家院里有顆核桃樹,也不知道這樹有多大了,只有粗糙而碩大的樹干能看出歲月滄桑,春天來了,外婆就催著外...
    xr安穩(wěn)閱讀 476評(píng)論 0 1

友情鏈接更多精彩內(nèi)容