溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java與Mysql鎖相關(guān)知識點有哪些

發(fā)布時間:2023-04-18 10:04:16 來源:億速云 閱讀:192 作者:iii 欄目:開發(fā)技術(shù)

本篇內(nèi)容主要講解“Java與Mysql鎖相關(guān)知識點有哪些”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學(xué)習(xí)“Java與Mysql鎖相關(guān)知識點有哪些”吧!

    鎖的定義

    在計算機程序中鎖用于獨占資源,獲取到鎖才可以操作對應(yīng)的資源。

    鎖的實現(xiàn)

    鎖在計算機底層的實現(xiàn),依賴于CPU提供的CAS指令(compare and swsp),對于一個內(nèi)存地址,會比較原值以及嘗試去修改的值,通過值是否修改成功,來表示是否強占到了這個鎖。

    JVM中的鎖

    jvm中,有2個常用的鎖

    synchronized

    synchronized是java提供的關(guān)鍵字鎖,可以鎖對象,類,方法。
    在JDK1.6以后,對synchronized進行了優(yōu)化,增加了偏向鎖和輕量鎖模式,現(xiàn)在synchronized鎖的運行邏輯如下:

    • 在初始加鎖時,會增加偏向鎖,即“偏向上一次獲取該鎖的線程”,在偏向鎖下,會直接CAS獲取該鎖。該模式大大提高了單線程反復(fù)獲取同一個鎖的吞吐情況,在Java官方看來,大部分鎖的爭搶都發(fā)生在同個線程上。

    • 如果偏向鎖CAS獲取失敗,說明當前線程與偏向鎖偏向的線程不同,偏向鎖就會升級成輕量鎖,輕量鎖的特點就是通過自旋CAS去獲取鎖。

    • 如果自旋獲取失敗,那么鎖就會升級成重量鎖,所有等待鎖的線程將被JVM掛起,在鎖釋放后,再由JVM統(tǒng)一通知喚醒,再去嘗試CAS鎖,如果失敗,繼續(xù)掛起。

    很顯然,偏向鎖設(shè)計的目的是“在Java官方看來,對同一個鎖的爭搶大部分都發(fā)生在同個線程上”。
    輕量鎖設(shè)計的目的是“在短期內(nèi),鎖的爭搶通過自旋CAS就可以獲取到,短時間內(nèi)的CPU自旋消耗小于線程掛起再喚醒的消耗”。
    重量鎖就是最初優(yōu)化前的synchronized的邏輯了。

    ReentrantLock

    說到ReentrantLock,就不得不說到JUC里的AQS了。
    AQS全稱AbstractQueueSynchronizer,幾乎JUC里所有的工具類,都依賴AQS實現(xiàn)。
    AQS在java里,是一個抽象類,但是本質(zhì)上是一種思路在java中的實現(xiàn)而已。
    AQS的實現(xiàn)邏輯如下:

    • 構(gòu)造一個隊列

    • 隊列中維護需要等待鎖的線程

    • 頭結(jié)點永遠是持有鎖(或持有資源)的節(jié)點,等待的節(jié)點在頭結(jié)點之后依次連接。

    • 頭結(jié)點釋放鎖后,會按照順序去喚醒那些等待的節(jié)點,然后那些節(jié)點會再次去嘗試獲取鎖。

    在synchronized鎖優(yōu)化以后,AQS的本質(zhì)與synchronized并沒有太大不同,兩者的性能也并沒有太大差距了,所以AQS現(xiàn)在的特點是:

    • 是在java api層面實現(xiàn)的鎖,所以可以實現(xiàn)各種并發(fā)工具類,操作也更加靈活

    • 因為提供了超時時間等機制,操作靈活,所以不易死鎖。(相同的,如果發(fā)生死鎖,將更難排查,因為jstack里將不會有deadlock標識)。

    • 可以實現(xiàn)公平鎖,而synchronized必定是非公平鎖。

    • 因為是JavaApi層實現(xiàn)的鎖,所以可以響應(yīng)中斷。

    到這里你會發(fā)現(xiàn),其實ReentrantLock可以說是synchronized在JavaApi層的實現(xiàn)。

    Mysql 鎖

    共享鎖(S) 與排它鎖(X)

    作用范圍

    這兩種鎖都包括行級鎖和表級鎖。
    獲取共享鎖時,如果該數(shù)據(jù)被其他事務(wù)的排它鎖鎖住,則無法獲取,需要等待排它鎖釋放。

    意向鎖

    作用范圍

    意向鎖為表鎖,在獲取表鎖之前,一定會檢查意向鎖。

    意圖鎖定協(xié)議如下:

    在事務(wù)獲得表中某行的共享鎖之前,它必須首先獲得表上的 IS 鎖或更強的鎖。

    在事務(wù)獲得表中行的排他鎖之前,它必須首先獲得表的 IX 鎖。

    在獲取任意表鎖的共享鎖或排它鎖之前,一定會檢查該表上的共享鎖。

    表鎖以及意向鎖的互斥規(guī)則如下:
    X IX S IS
    X Conflict Conflict Conflict Conflict
    IX Conflict Compatible Conflict Compatible
    S Conflict Conflict Compatible Compatible
    IS Conflict Compatible Compatible Compatible

    意向鎖的作用在于:在獲取表鎖時,可以通過意向鎖來快速判斷能否獲取。

    因為獲取行級鎖時,會先獲取對應(yīng)的意向鎖,這樣另外的事務(wù)在獲取表鎖時就可以通過意向鎖快速的判斷,而不需要每行去掃描。

    特別注意的是,意向鎖是可以疊加的,即會存在多個,如T1事務(wù)獲取了意向鎖IX1和行級鎖X1,T2事務(wù)依舊可以獲取意向鎖IX2和行級鎖X2,所以僅在獲取表級鎖之前,才會檢查意向鎖。

    記錄鎖

    記錄鎖生效在索引上,用以在SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE時保護該行數(shù)據(jù)不被其他事務(wù)更改。

    記錄鎖在沒有索引時依舊會生效,因為innodb會為每張表創(chuàng)建一個隱藏的索引。

    記錄鎖是最基本的行鎖。

    間隙鎖

    間隙鎖生效在索引上,用于鎖定索引值后的行,防止插入,在select from table where index=? for update時會生效,例如index=1,則會鎖住index=1索引節(jié)點相關(guān)的行,防止其他事務(wù)插入數(shù)據(jù)。

    但是并不會防止update語句,哪怕update的數(shù)據(jù)不存在。

    Next-Key Locks

    這個鎖是記錄鎖和間隙鎖的組合,簡而言之在select from table where index=? for update時,既會有間隙鎖防止insert,也會有記錄鎖在index上防止這一條數(shù)據(jù)的update和delete。這個Next-key只是對這兩種鎖的一種概括,因為這兩種鎖在select for update時通常會一起出現(xiàn)。

    Insert Intention Locks

    插入意向鎖,和意向鎖類似。不過是特殊的間隙鎖,并不發(fā)生在select for update,而是在同時發(fā)生insert時產(chǎn)生,例如在兩個事務(wù)同時insert索引區(qū)間為[4,7]時,同時獲得該區(qū)間的意向鎖,此時事務(wù)不會阻塞,例如A:insert-5,B:insert-7,此時不會阻塞兩個事務(wù)。

    插入意向鎖是一個特殊的間隙鎖,是為了防止正常間隙鎖鎖區(qū)間的情況下,insert頻繁阻塞而設(shè)計的,例如A:insert-5,B:insert-7,如果沒有插入意向鎖,那么5和7都要去嘗試獲取間隙鎖,此時第二個事務(wù)就會被阻塞,但是通過插入意向鎖,第二個事務(wù)就不會被阻塞,只有到插入的行確實沖突,才會被阻塞。

    AUTO-INC Locks

    自增鎖,這個鎖很明顯是表級insert鎖,為了保證自增主鍵的表的主鍵保持原子自增。

    對于鎖這個東西,大家應(yīng)該多去理解各種鎖設(shè)計運行的原理和模型,這樣在加深理解后,在使用起來才會更加深入和透徹。

    常見鎖使用的場景和用法

    double check

    眾所周知,mysql的事務(wù)對防止重復(fù)插入并沒有什么卵用,唯一索引又存在很多缺點,業(yè)務(wù)上最好不要使用,所以一般來說防止重復(fù)插入的通用做法就是使用分布式鎖,這就有一種比較常用的寫法。

    final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
    if (weekendNoticeReadCountDO == null) {
        final String lockKey = RedisConstant.LOCK_WEEKEND_READ_COUNT_INSERT + ":" + noticeRequestDTO.getNoticeId();
        ClusterLock lock = clusterLockFactory.getClusterLockRedis(
            RedisConstant.REDIS_KEY_PREFIX,
            lockKey
        );
        if (lock.acquire(RedisConstant.REDIS_LOCK_DEFAULT_TIMEOUT)) {
            //double check
            final WeekendNoticeReadCountDO weekendNoticeReadCountDO = weekendNoticeReadRepositoryService.selectByNoticeId(noticeRequestDTO.getNoticeId());
            if (weekendNoticeReadCountDO == null) {
                try {
                    lock.execute(() -> {
                        WeekendNoticeReadCountDO readCountDO = new WeekendNoticeReadCountDO();
                        readCountDO.setNoticeId(noticeRequestDTO.getNoticeId());
                        readCountDO.setReadCount(1L);
                        readCountDO.setCreateTime(new Date());
                        readCountDO.setUpdateTime(new Date());
                        weekendNoticeReadRepositoryService.insert(readCountDO);
                        return true;
                    });
                } catch (ApiException err) {
                    throw err;
                } catch (Exception e) {
                    log.error("插入", e);
                    throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服務(wù)端出錯");
                }
            } else {
                weekendNoticeReadRepositoryService.noticeCountAdd(weekendNoticeReadCountDO);
            }
        } else {
            log.warn("redis鎖獲取超時,key:{}", lockKey);
            throw new ApiException(ErrorEnum.SERVER_ERROR.getCode(), "服務(wù)器繁忙,請稍后重試");
        }
    }

    在獲取到鎖之后,可能是經(jīng)過等待才獲取到的鎖,此時上一個釋放鎖的線程可能已經(jīng)插入了數(shù)據(jù)了,所以在鎖內(nèi)部,依舊要再次校驗一下數(shù)據(jù)是否存在。
    這種寫法適合大多數(shù)需要唯一性的寫場景。

    避免死鎖

    如何避免死鎖?最簡單有效的方法就是:**不要在鎖里再去獲取鎖,簡而言之就是鎖最好單獨使用,不要套娃。
    也要注意一些隱性鎖,比如數(shù)據(jù)庫。
    事務(wù)A:

    • 插入[5,7],插入意向鎖。

    • select for update更新[100,150],間隙鎖。
      事務(wù)B:

    • select for update更新[90,120],間隙鎖。

    • 插入[4,6],插入意向鎖。

    此時在并發(fā)場景下,就可能會出現(xiàn)A持有了[5,7]的間隙鎖,在等待事務(wù)B[90,120]的間隙鎖,事務(wù)B也一樣,就死鎖了。
    **

    順帶談?wù)劜l(fā)場景下常見的問題

    讀寫混亂

    在寫業(yè)務(wù)代碼,定義一些工具類或者緩存類的時候,很容易疏忽而發(fā)生類似的問題。
    比如構(gòu)建一個static緩存,沒有使用ConcurrentHashMap中的putIfAbsent等方法,也沒有加鎖去構(gòu)建,導(dǎo)致上面的線程剛put了,下面的線程就刪掉了,或者重復(fù)構(gòu)建2次緩存。

    Redis或者一些并發(fā)操作釋放鎖或者資源,沒有檢查是否是當前線程持有

    這點在Redis鎖的示例代碼也講到了。
    線程A獲取到鎖,此時B,C在等待,然后A執(zhí)行時間過長,導(dǎo)致鎖超時被自動釋放了,此時B獲取到了鎖,在快樂的執(zhí)行,然后A執(zhí)行完了之后,釋放鎖時沒有判斷是否還是自己持有,導(dǎo)致B持有的鎖被刪除了,此時C又獲取到了鎖,BC同時在執(zhí)行。

    到此,相信大家對“Java與Mysql鎖相關(guān)知識點有哪些”有了更深的了解,不妨來實際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進入相關(guān)頻道進行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

    向AI問一下細節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI