1. 程式人生 > >死磕 java同步系列之mysql分散式鎖

死磕 java同步系列之mysql分散式鎖

問題

(1)什麼是分散式鎖?

(2)為什麼需要分散式鎖?

(3)mysql如何實現分散式鎖?

(4)mysql分散式鎖的優點和缺點?

簡介

隨著併發量的不斷增加,單機的服務遲早要向多節點或者微服務進化,這時候原來單機模式下使用的synchronized或者ReentrantLock將不再適用,我們迫切地需要一種分散式環境下保證執行緒安全的解決方案,今天我們一起來學習一下mysql分散式鎖如何實現分散式執行緒安全。

基礎知識

mysql中提供了兩個函式——get_lock('key', timeout)release_lock('key')——來實現分散式鎖,可以根據key來加鎖,這是一個字串,可以設定超時時間(單位:秒),當呼叫release_lock('key')

或者客戶端斷線的時候釋放鎖。

它們的使用方法如下:

mysql> select get_lock('user_1', 10);
    -> 1
mysql> select release_lock('user_1');
    -> 1

get_lock('user_1', 10)如果10秒之內獲取到鎖則返回1,否則返回0;

release_lock('user_1')如果該鎖是當前客戶端持有的則返回1,如果該鎖被其它客戶端持有著則返回0,如果該鎖沒有被任何客戶端持有則返回null;

多客戶端案例

為了便於舉例【本篇文章由“彤哥讀原始碼”原創,請支援原創,謝謝!】,這裡的超時時間全部設定為0,也就是立即返回。

時刻 客戶端A 客戶端B
1 get_lock('user_1', 0) -> 1 -
2 - get_lock('user_1', 0) -> 0
3 - release_lock('user_1', 0) -> 0
4 release_lock('user_1', 0) -> 1 -
5 release_lock('user_2', 0) -> null -
6 - get_lock('user_1', 0) -> 1
7 - release_lock('user_1', 0) -> 1

Java實現

為了方便快速實現,這裡使用 springboot2.1 + mybatis 實現,並且省略spring的配置,只列舉主要的幾個類。

定義Locker介面

介面中只有一個方法,入參1為加鎖的key,入參2為執行的命令。

public interface Locker {
    void lock(String key, Runnable command);
}

mysql分散式鎖實現

mysql的實現中要注意以下兩點:

(1)加鎖、釋放鎖必須在同一個session(同一個客戶端)中,所以這裡不能使用Mapper介面的方式呼叫,因為Mapper介面有可能會導致不在同一個session。

(2)可重入性是通過ThreadLocal保證的;

@Slf4j
@Component
public class MysqlLocker implements Locker {

    private static final ThreadLocal<SqlSessionWrapper> localSession = new ThreadLocal<>();

    @Autowired
    private SqlSessionFactory sqlSessionFactory;

    @Override
    public void lock(String key, Runnable command) {
        // 加鎖、釋放鎖必須使用同一個session
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        if (sqlSessionWrapper == null) {
            // 第一次獲取鎖
            localSession.set(new SqlSessionWrapper(sqlSessionFactory.openSession()));
        }
        try {
            // 【本篇文章由“彤哥讀原始碼”原創,請支援原創,謝謝!】
            // -1表示沒獲取到鎖一直等待
            if (getLock(key, -1)) {
                command.run();
            }
        } catch (Exception e) {
            log.error("lock error", e);
        } finally {
            releaseLock(key);
        }
    }

    private boolean getLock(String key, long timeout) {
        Map<String, Object> param = new HashMap<>();
        param.put("key", key);
        param.put("timeout", timeout);
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.getLock", param);
        if (result != null && result.intValue() == 1) {
            // 獲取到了鎖,state加1
            sqlSessionWrapper.state++;
            return true;
        }
        return false;
    }

    private boolean releaseLock(String key) {
        SqlSessionWrapper sqlSessionWrapper = localSession.get();
        Integer result = sqlSessionWrapper.sqlSession.selectOne("LockerMapper.releaseLock", key);
        if (result != null && result.intValue() == 1) {
            // 釋放鎖成功,state減1
            sqlSessionWrapper.state--;
            // 當state減為0的時候說明當前執行緒獲取的鎖全部釋放了,則關閉session並從ThreadLocal中移除
            if (sqlSessionWrapper.state == 0) {
                sqlSessionWrapper.sqlSession.close();
                localSession.remove();
            }
            return true;
        }
        return false;
    }

    private static class SqlSessionWrapper {
        int state;
        SqlSession sqlSession;

        public SqlSessionWrapper(SqlSession sqlSession) {
            this.state = 0;
            this.sqlSession = sqlSession;
        }
    }
}

LockerMapper.xml

定義get_lock()、release_lock()的語句。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="LockerMapper">
    <select id="getLock" resultType="integer">
        select get_lock(#{key}, #{timeout});
    </select>

    <select id="releaseLock" resultType="integer">
        select release_lock(#{key})
    </select>
</mapper>

測試類

這裡啟動1000個執行緒,每個執行緒列印一句話並睡眠2秒鐘。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = Application.class)
public class MysqlLockerTest {

    @Autowired
    private Locker locker;

    @Test
    public void testMysqlLocker() throws IOException {
        for (int i = 0; i < 1000; i++) {
            // 多節點測試
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            new Thread(()->{
                locker.lock("lock", ()-> {
                    // 可重入性測試
                    locker.lock("lock", ()-> {
                        System.out.println(String.format("time: %d, threadName: %s", System.currentTimeMillis(), Thread.currentThread().getName()));
                        try {
                            Thread.sleep(2000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    });
                });
            }, "Thread-"+i).start();
        }

        System.in.read();
    }
}

執行結果

檢視執行結果發現每隔2秒列印一個執行緒的資訊,說明這個鎖是有效的,至於分散式環境下面的驗證也很簡單,起多個MysqlLockerTest例項即可。

time: 1568715905952, threadName: Thread-3
time: 1568715907955, threadName: Thread-4
time: 1568715909966, threadName: Thread-8
time: 1568715911967, threadName: Thread-0
time: 1568715913969, threadName: Thread-1
time: 1568715915972, threadName: Thread-9
time: 1568715917975, threadName: Thread-6
time: 1568715919997, threadName: Thread-5
time: 1568715921999, threadName: Thread-7
time: 1568715924001, threadName: Thread-2

總結

(1)分散式環境下需要使用分散式鎖,單機的鎖將無法保證執行緒安全;

(2)mysql分散式鎖是基於get_lock('key', timeout)release_lock('key')兩個函式實現的;

(3)mysql分散式鎖是可重入鎖;

彩蛋

使用mysql分散式鎖需要注意些什麼呢?

答:必須保證多個服務節點使用的是同一個mysql庫【本篇文章由“彤哥讀原始碼”原創,請支援原創,謝謝!】。

mysql分散式鎖具有哪些優點?

答:1)方便快捷,因為基本每個服務都會連線資料庫,但是不是每個服務都會使用redis或者zookeeper;

2)如果客戶端斷線了會自動釋放鎖,不會造成鎖一直被佔用;

3)mysql分散式鎖是可重入鎖,對於舊程式碼的改造成本低;

mysql分散式鎖具有哪些缺點?

答:1)加鎖直接打到資料庫,增加了資料庫的壓力;

2)加鎖的執行緒會佔用一個session,也就是一個連線數,如果併發量大可能會導致正常執行的sql語句獲取不到連線;

3)服務拆分後如果每個服務使用自己的資料庫,則不合適;

4)相對於redis或者zookeeper分散式鎖,效率相對要低一些;


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

相關推薦

java同步系列mysql分散式

問題 (1)什麼是分散式鎖? (2)為什麼需要分散式鎖? (3)mysql如何實現分散式鎖? (4)mysql分散式鎖的優點和缺點? 簡介 隨著併發量的不斷增加,單機的服務遲早要向多節點或者微服務進化,這時候原來單機模式下使用的synchronized或者ReentrantLock將不再適用,我們迫切地需要一

java同步系列zookeeper分散式

(2)zookeeper分散式鎖有哪些優點? (3)zookeeper分散式鎖有哪些缺點? 簡介 zooKeeper是一個分散式的,開放原始碼的分散式應用程式協調服務,它可以為分散式應用提供一致性服務,它是Hadoop和Hbase的重要元件,同時也可以作為配置中心、註冊中心運用在微服務體系中。 本章我們將介

java同步系列redis分散式進化史

(2)redis分散式鎖有哪些優點? (3)redis分散式鎖有哪些缺點? (4)redis實現分散式鎖有沒有現成的輪子可以使用? 簡介 Redis(全稱:Remote Dictionary Server 遠端字典服務)是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-

java同步系列開篇

討論 關註 使用 避免死鎖 更新數據 讀寫 上下文切換 monit 缺點 簡介 同步系列,這是彤哥想了好久的名字,本來是準備寫鎖相關的內容,但是java中的CountDownLatch、Semaphore、CyclicBarrier這些類又不屬於鎖,它們和鎖又有很多共同點,

java同步系列JMM(Java Memory Model)

簡介 Java記憶體模型是在硬體記憶體模型上的更高層的抽象,它遮蔽了各種硬體和作業系統訪問的差異性,保證了Java程式在各種平臺下對記憶體的訪問都能達到一致的效果。 硬體記憶體模型 在正式講解Java的記憶體模型之前,我們有必要先了解一下硬體層面的一些東西。 在現代計算機的硬體體系中,CPU的運算速度是非常快

java同步系列volatile解析

問題 (1)volatile是如何保證可見性的? (2)volatile是如何禁止重排序的? (3)volatile的實現原理? (4)volatile的缺陷? 簡介 volatile可以說是Java虛擬機器提供的最輕量級的同步機制了,但是它並不容易被正確地理解,以至於很多人不習慣使用它,遇到多執行緒問題一律

java同步系列synchronized解析

問題 (1)synchronized的特性? (2)synchronized的實現原理? (3)synchronized是否可重入? (4)synchronized是否是公平鎖? (5)synchronized的優化? (6)synchronized的五種使用方式? 簡介 synchronized關鍵字是Ja

java同步系列自己動手寫一個Lock

問題 (1)自己動手寫一個鎖需要哪些知識? (2)自己動手寫一個鎖到底有多簡單? (3)自己能不能寫出來一個完美的鎖? 簡介 本篇文章的目標一是自己動手寫一個鎖,這個鎖的功能很簡單,能進行正常的加鎖、解鎖操作。 本篇文章的目標二是通過自己動手寫一個鎖,能更好地理解後面章節將要學習的AQS及各種同步器實現的原理

java同步系列AQS起篇

問題 (1)AQS是什麼? (2)AQS的定位? (3)AQS的實現原理? (4)基於AQS實現自己的鎖? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 AQS是基於FIFO的佇列實現的,並且內部維護了一個狀態變數sta

java同步系列ReentrantLock原始碼解析(一)——公平、非公平

問題 (1)重入鎖是什麼? (2)ReentrantLock如何實現重入鎖? (3)ReentrantLock為什麼預設是非公平模式? (4)ReentrantLock除了可重入還有哪些特性? 簡介 Reentrant = Re + entrant,Re是重複、又、再的意思,entrant是enter的名詞或

java同步系列ReentrantLock原始碼解析(二)——條件

問題 (1)條件鎖是什麼? (2)條件鎖適用於什麼場景? (3)條件鎖的await()是在其它執行緒signal()的時候喚醒的嗎? 簡介 條件鎖,是指在獲取鎖之後發現當前業務場景自己無法處理,而需要等待某個條件的出現才可以繼續處理時使用的一種鎖。 比如,在阻塞佇列中,當佇列中沒有元素的時候是無法彈出一個元素

java同步系列ReentrantLock VS synchronized——結果可能跟你想的不一樣

問題 (1)ReentrantLock有哪些優點? (2)ReentrantLock有哪些缺點? (3)ReentrantLock

java同步系列ReentrantReadWriteLock原始碼解析

問題 (1)讀寫鎖是什麼? (2)讀寫鎖具有哪些特性? (3)ReentrantReadWriteLock是怎麼實現讀寫鎖的? (4)如何使用ReentrantReadWriteLock實現高效安全的TreeMap? 簡介 讀寫鎖是一種特殊的鎖,它把對共享資源的訪問分為讀訪問和寫訪問,多個執行緒可以同時對共享

java同步系列Semaphore原始碼解析

問題 (1)Semaphore是什麼? (2)Semaphore具有哪些特性? (3)Semaphore通常使用在什麼場景中? (

java同步系列AQS終篇(面試)

問題 (1)AQS的定位? (2)AQS的重要組成部分? (3)AQS運用的設計模式? (4)AQS的總體流程? 簡介 AQS的全稱是AbstractQueuedSynchronizer,它的定位是為Java中幾乎所有的鎖和同步器提供一個基礎框架。 在之前的章節中,我們一起學習了ReentrantLock、R

java同步系列StampedLock原始碼解析

問題 (1)StampedLock是什麼? (2)StampedLock具有什麼特性? (3)StampedLock是否支援可重入

java同步系列CyclicBarrier原始碼解析——有圖有真相

問題 (1)CyclicBarrier是什麼? (2)CyclicBarrier具有什麼特性? (3)CyclicBarrier與

java同步系列Phaser原始碼解析

問題 (1)Phaser是什麼? (2)Phaser具有哪些特性? (3)Phaser相對於CyclicBarrier和Count

java同步系列終結篇

腦圖 下面是關於同步系列的一份腦圖,列舉了主要的知識點和問題點,看過本系列文章的同學可以根據腦圖自行回顧所學的內容,也可以作為面試前的準備。 如果有需要高清無碼原圖的同學,可以關注公眾號“彤哥讀原始碼”,回覆“sync”領取。 總結 所謂同步,就是保證多執行緒(包括多程序)對共享資源的讀寫能夠安全有效的執