菜鳥帶你手撕分佈式鎖-redis,看完不會算我輸

背景

今天是五一,決定不出去,在家裏擼代碼,今天學習redis,因而準備寫個基於redis的分佈式鎖。因爲本人屬於菜鳥級別,在寫的過程當中遇到各類問題,功夫不負有心人,終於搞定,若是發現實現有問題,歡迎指導,感謝.git

在單機時代,雖然不須要分佈式鎖,但也面臨過相似的問題,只不過在單機的狀況下,若是有多個線程要同時訪問某個共享資源的時候,咱們能夠採用線程間加鎖的機制,即當某個線程獲取到這個資源後,就當即對這個資源進行加鎖,當使用完資源以後,再釋放鎖,其它線程就能夠接着使用了。JAVA中已提供相關工具類。可是到了分佈式系統的時代,這種線程或者進程之間的鎖機制,就可能沒做用了,系統可能會有多份而且部署在不一樣的機器上,這些資源已經不是在線程之間共享了,而是屬於進程之間共享的資源。所以,爲了解決這個問題,咱們就必須引入「分佈式鎖」。github

分佈式鎖,是指在分佈式的部署環境下,經過鎖機制來讓多客戶端互斥的對共享資源進行訪問。web

通常分佈式鎖要知足一下幾點要求:redis

  • 排他性:在同一時間只會有一個客戶端能獲取到鎖,其它客戶端沒法同時獲取
  • 避免死鎖:這把鎖在一段有限的時間以後,必定會被釋放(正常釋放或異常釋放)
  • 高可用:獲取或釋放鎖的機制必須高可用且性能佳

分佈式鎖實現方式

目前主流的分佈式鎖實現主要有如下幾種數據庫

  • 基於redis實現
  • 基於數據庫實現
  • 基於zookeeper的實現

今天主要將基於redis實現分佈式鎖緩存

redis分佈式鎖實現

redis分佈式鎖基礎知識

  • 緩存過時安全

    緩存能夠設置過時時間,redis根據時間自動進行清理。併發

  • setNx命令dom

將 key 的值設爲 value ,當且僅當 key 不存在。
若給定的 key 已經存在,則 SETNX 不作任何動做。
SETNX 是『SET if Not eXists』(若是不存在,則 SET)的簡寫。
複製代碼
  • lua腳本編輯器

    腳本語言,用於支持redis原子操做。

熟悉以上redis知識,實現redis分佈式鎖比較容易了。

關注點

  • 緩存過時

    最好給加鎖的key設置緩存過時時間,能夠有效的防止死鎖,好比某個進程加鎖後沒來得及釋放鎖,宕機,說來負責釋放鎖?

  • set值

    加鎖時,在redis中保存在各節點中惟一的值,防止不一樣進程誤解鎖

好比serviceA已經在redis中加鎖lock,通常serviceA執行時間爲1秒,則設置緩存過時時間2秒,某天因爲機器緣由serviceA執行了3秒,那麼對應的鎖已經失效,此期間B去加鎖,並加鎖成功, serviceA執行完會釋放鎖,致使serviceA會將B加的鎖釋放,因此產生誤刪鎖,採用惟一值,避免這種狀況產生。刪鎖會檢查值,若是加鎖與解鎖的值不相同則不容許解鎖。

  • 加鎖與失效時間必需要原子性

核心邏輯

一、利用SETNX命令加鎖

public static String set(String key, String value, long timeout) {
        Jedis jedis = getJedis();
        try {
            String ret = jedis.set(key, value, "NX", "PX", timeout);
            return ret;
        } finally {
            close(jedis);
        }
    }
複製代碼

二、實現阻塞加鎖和非阻塞加鎖

/** * 阻塞加鎖 */
    public void lock() {
        if (tryLock()) {
            return;
        }
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //lock
        lock();
 }  /** * 基於setNx實現非阻塞鎖 * * @return */ public boolean tryLock() { String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, DEFAULT_TIME_OUT); if ("OK".equals(ret)) { //lock success LOCAL.set(uuid); return true; } return false; }  public boolean tryLock(long time, TimeUnit unit){ String uuid = UUID.randomUUID().toString(); String ret = JedisUtis.set(LOCK_KEY, uuid, unit.toMillis(time)); if ("ok".equals(ret)) { LOCAL.set(uuid); //lock success return true; } return false; } 複製代碼

三、解鎖

解鎖的同時須要去檢查值是否與加鎖的值相同,不相同則不容許解鎖,這裏是經過ThreadLocal傳加鎖產生的uuid

/** * unlock * 執行lua腳本,保證原子性 */
    public void unlock() {
        release();
    }
 private void release() { String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"; JedisUtis.remove(LOCK_KEY, script, LOCAL.get()); } 複製代碼

場景驗證

12306售票是併發學習中經典案例,仍是拿這個舉例,好比有100 tickets,有多個售票窗口同時售票,怎麼保證不被重複賣

/** * 線程不安全示例 * * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicket implements Runnable {
 private int tickets = 100;  public void run() { for (; ; ) { sale(); if (tickets < 0) break; } } }  /** * 售票 */ private void sale() { if (tickets > 0) { tickets--; System.out.println(Thread.currentThread().getName() + " - 在售第" + (100 - tickets) + "票 :: 剩餘" + (tickets)); } try { Thread.sleep(100); } catch (InterruptedException e) {  } } } 複製代碼

測試類

/** * @author Qi.qingshan * @date 2020/5/1 */
public class SaleTicketTest {
 BlockingDeque queue = new LinkedBlockingDeque(100);  private ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 50, 100, TimeUnit.SECONDS, queue);  @Test public void testSaleTickets() throws IOException { SaleTicket saleTicket = new SaleTicket(); executor.execute(new Thread(saleTicket, "售票員001")); executor.execute(new Thread(saleTicket, "售票員002")); executor.execute(new Thread(saleTicket, "售票員003")); executor.execute(new Thread(saleTicket, "售票員004")); System.in.read(); } } 複製代碼

存在重複售票狀況,改用redisLock,調整核心代碼

public void run() {
        for (; ; ) {
            lock.lock();
            try {
                sale();
                if (tickets < 0) break;
            } finally {
                lock.unlock();
            }
        }
    }
複製代碼

執行結果以下

完整代碼已上傳github.com/qiqsa/distr…

相關文章
相關標籤/搜索