MySQL中的悲觀鎖和樂(lè)觀鎖

悲觀鎖(Pessimistic Lock)和樂(lè)觀鎖(Optimistic Lock)是數(shù)據(jù)庫(kù)系統(tǒng)中并發(fā)控制主要采用的技術(shù)手段。針對(duì)不同的業(yè)務(wù)場(chǎng)景,應(yīng)該選用不同的并發(fā)控制方式。不要把它們和數(shù)據(jù)庫(kù)中提供的鎖機(jī)制(行鎖、表鎖、排他鎖、共享鎖)混為一談。

Pessimistic Lock

概述

悲觀鎖(Pessimistic Lock),顧名思義,就是很悲觀,每次去拿數(shù)據(jù)的時(shí)候都認(rèn)為別人會(huì)修改,所以每次在拿數(shù)據(jù)的時(shí)候都會(huì)上鎖,這樣別人想拿這個(gè)數(shù)據(jù)就會(huì)block直到它拿到鎖。假定會(huì)發(fā)生并發(fā)沖突,屏蔽一切可能違反數(shù)據(jù)完整性的操作。

Java synchronized 就屬于悲觀鎖的一種實(shí)現(xiàn),每次線程要修改數(shù)據(jù)時(shí)都先獲得鎖,保證同一時(shí)刻只有一個(gè)線程能操作數(shù)據(jù),其他線程則會(huì)被block。

特性

  • 需要依靠數(shù)據(jù)庫(kù)中的鎖機(jī)制來(lái)實(shí)現(xiàn),即通過(guò)常用的select ... for update操作來(lái)實(shí)現(xiàn)悲觀鎖。
  • 需要開(kāi)啟事務(wù),在事務(wù)中實(shí)現(xiàn)鎖機(jī)制。
  • 可以最大程度的保證數(shù)據(jù)操作的獨(dú)占性。
  • select for update語(yǔ)句中所有掃描過(guò)的行都會(huì)被鎖上,這一點(diǎn)很容易造成問(wèn)題。如果用悲觀鎖請(qǐng)確保用到了索引,否則造成鎖表。
  • 長(zhǎng)事務(wù)中的鎖等待,會(huì)導(dǎo)致其他用戶長(zhǎng)時(shí)間無(wú)法操作。
  • 主要用于數(shù)據(jù)爭(zhēng)用激烈的環(huán)境,以及發(fā)生并發(fā)沖突時(shí)使用鎖保護(hù)數(shù)據(jù)的成本要低于回滾事務(wù)的成本的環(huán)境中。

Optimistic Lock

樂(lè)觀鎖,又稱樂(lè)觀并發(fā)控制(Optimistic Concurrency Control),樂(lè)觀地認(rèn)為不會(huì)發(fā)生并發(fā)問(wèn)題,只在提交更新操作時(shí)檢查是否違反數(shù)據(jù)的一致性。

概述

樂(lè)觀鎖在數(shù)據(jù)庫(kù)中的實(shí)現(xiàn)完全是邏輯性的,不需要數(shù)據(jù)庫(kù)提供特殊的支持。一般的做法是在數(shù)據(jù)表中增加一個(gè)字段(版本號(hào)或者時(shí)間戳),作為數(shù)據(jù)的版本標(biāo)識(shí)。讀取數(shù)據(jù)時(shí),將版本號(hào)一同讀出;之后更新數(shù)據(jù)時(shí),加入版本號(hào)條件,更新成功就將版本號(hào)加1。樂(lè)觀鎖的重點(diǎn)在于,更新數(shù)據(jù)時(shí),加入版本號(hào)匹配條件,將數(shù)據(jù)的版本與數(shù)據(jù)表中對(duì)應(yīng)記錄的當(dāng)前版本進(jìn)行匹配更新,如果數(shù)據(jù)的版本號(hào)等于數(shù)據(jù)表的當(dāng)前版本號(hào),則獲取鎖成功,也就是更新成功;否則,更新失敗,需要回滾整個(gè)業(yè)務(wù)操作。Java中的atomic包就是樂(lè)觀鎖的一種實(shí)現(xiàn),AtomicInteger 通過(guò)CAS(Compare And Set)操作實(shí)現(xiàn)線程安全的自增。

實(shí)現(xiàn)機(jī)制

在數(shù)據(jù)庫(kù)中,update同一行的情況是不允許并發(fā)的,即數(shù)據(jù)庫(kù)每次執(zhí)行一條update語(yǔ)句時(shí)會(huì)獲取被update行的寫鎖,直到這一行被成功更新后才釋放。因此在業(yè)務(wù)操作進(jìn)行前獲取需要鎖的數(shù)據(jù)的當(dāng)前版本號(hào),然后實(shí)際更新數(shù)據(jù)時(shí),以版本號(hào)作為條件,再次對(duì)比版本號(hào)確認(rèn)與之前獲取的相同,并更新版本號(hào),即可確認(rèn)沒(méi)有發(fā)生并發(fā)的修改。如果更新失敗即可認(rèn)為老版本的數(shù)據(jù)已經(jīng)被并發(fā)修改掉了,此時(shí)認(rèn)為獲取鎖失敗,需要回滾整個(gè)業(yè)務(wù)操作并可根據(jù)需要重試整個(gè)過(guò)程。

特性

  • 不需要依靠數(shù)據(jù)庫(kù)中的鎖機(jī)制來(lái)實(shí)現(xiàn),但需要在表中新增一個(gè)版本號(hào),在邏輯上實(shí)現(xiàn)。
  • 無(wú)論是否開(kāi)啟事務(wù),都可以在邏輯上實(shí)現(xiàn)樂(lè)觀鎖。
  • 樂(lè)觀鎖在不發(fā)生取鎖失敗的情況下開(kāi)銷比悲觀鎖小,但是一旦發(fā)生失敗回滾開(kāi)銷則比較大,因此適合用在取鎖失敗概率比較小的場(chǎng)景,可以提升系統(tǒng)并發(fā)性能。

示例

悲觀鎖

用數(shù)據(jù)庫(kù)來(lái)演示悲觀鎖,首先悲觀鎖是必須用到數(shù)據(jù)庫(kù)的事務(wù)機(jī)制,其次要注意查詢條件字段必須是索引字段,否則會(huì)造成鎖表。

  1. 開(kāi)啟事務(wù)

begin

  1. 執(zhí)行for update操作。

select * from t_logs where id = '2' for update;

  1. 不要執(zhí)行commit操作,為了模仿并發(fā)操作。

在Navicat中開(kāi)啟另一個(gè)會(huì)話窗口

  1. 開(kāi)啟事務(wù)

begin

  1. 執(zhí)行update操作

update t_logs set action = '測(cè)試用例' where id = '2';

  1. 如果不執(zhí)行上一個(gè)會(huì)話的commit操作,會(huì)發(fā)現(xiàn)此會(huì)話一直處于block狀態(tài)。
  2. 執(zhí)行上一個(gè)會(huì)話的commit操作,提交數(shù)據(jù)。
  3. 執(zhí)行此會(huì)話的commit操作。

樂(lè)觀鎖

商品的庫(kù)存量是固定的,保證商品數(shù)量不超賣, 需要保證數(shù)據(jù)一致性:用樂(lè)觀鎖來(lái)保證某個(gè)人點(diǎn)擊秒殺后系統(tǒng)中查出來(lái)的庫(kù)存量和實(shí)際扣減庫(kù)存時(shí)庫(kù)存量是一致的。

  1. 商品表
CREATE TABLE `tb_product_stock` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '自增ID',
  `product_id` bigint(32) NOT NULL COMMENT '商品ID',
  `number` INT(8) NOT NULL DEFAULT 0 COMMENT '庫(kù)存數(shù)量',
  `create_time` DATETIME NOT NULL COMMENT '創(chuàng)建時(shí)間',
  `modify_time` DATETIME NOT NULL COMMENT '更新時(shí)間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `index_pid` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品庫(kù)存表';
  1. POJO類
@Getter
@Setter
@ToString
public class ProductStock {

    private Long productId; //商品id

    private Integer number; //庫(kù)存量

}
  1. 鎖實(shí)現(xiàn)
public boolean updateStock(Long productId) {
        int updateCnt = 0;
        while (updateCnt == 0) {
            ProductStock product = query("SELECT * FROM tb_product_stock WHERE product_id=#{productId}", productId);
            if (product.getNumber() > 0) {
                // 確保庫(kù)存不會(huì)減為負(fù)數(shù)
                updateCnt = update("UPDATE tb_product_stock SET number=number-1 WHERE product_id=#{productId} AND number>=1", productId);
                if(updateCnt > 0){    //更新庫(kù)存成功
                    return true;
                }
            } else {    //賣完
                return false;
            }
        }
        return false;
    }

UPDATE 語(yǔ)句的WHERE 條件字句上需要建索引,避免全表掃描。

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

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