淺談併發加鎖

luler發表於2020-01-05

在我們的工作中,常常會出現一些對數量控制有精確要求的需求,比如商品庫存量、獎品數量、報名人數限制等等,這些應用場景往往都存在高併發可能,比較容易出現資料量超量問題。以下做一下示例探索:

首先設計一個存量表

CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_name` varchar(255) NOT NULL DEFAULT '',
  `count` int(10) NOT NULL DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

新增一行資料如下,設定基礎庫存量為10

淺談併發加鎖

問題程式碼如下:

        $process_num = 50; //開50個程式,模擬50個使用者
        for ($i = 0; $i < $process_num; $i++) {
            MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
                if (Db::name('product')->where('id', 1)->value('count') > 0) {
                    $res = Db::name('product')->where('id', 1)->setDec('count');
                    if ($res) {
                        dump('獲取到更新資源許可權:' . $i);
                    }
                }
            });
        }

執行結果,50個使用者都獲取到了更新資源的許可權

淺談併發加鎖

資料庫相應資料存量變成了-40

淺談併發加鎖

這顯然不是我們所期待的,這就是高併發帶來的問題,同一時刻有多個程式讀取同一條資料,同一時刻有多個程式更新同一條資料

資料庫自有的鎖機制解決

  1. 要進行DML層面的限制(最後關卡安全,報錯總比出現資料問題產生的影響小),主要的修改是將count的型別改成了無符號整數,這樣該值就不可能再出現負數值
    CREATE TABLE `product` (
    `id` int(11) NOT NULL AUTO_INCREMENT,
    `product_name` varchar(255) NOT NULL DEFAULT '',
    `count` int(10) unsigned NOT NULL DEFAULT '0',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

執行一下程式碼,當count值從10減到0時,就不能再減少了,再減就會出現資料庫報錯

淺談併發加鎖

  1. mysql提供的行級鎖select ... lock in share mode(阻塞寫),select ... for update(阻塞讀寫,悲觀鎖),所以for update機制能滿足我們的原子要求。編輯程式碼如下:
        $process_num = 50; //開50個程式,模擬50個使用者
        for ($i = 0; $i < $process_num; $i++) {
            MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
                Db::startTrans(); //行級鎖必須在事務中才能生效
                //設定for update,程式會阻塞在這裡,只能允許一個程式獲取到行鎖,其他等待獲取
                if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) { 
                    $res = Db::name('product')->where('id', 1)->setDec('count');
                    if ($res) {
                        dump('獲取到更新資源許可權:' . $i);
                    }
                }
                Db::commit();
            });
        }

只有十個程式獲取到了更新許可權,消費正常

淺談併發加鎖

淺談併發加鎖

  1. 將條件語句放到update上,保持語句執行的原子性,杜絕併發幻讀
    修改程式碼如下:
        $process_num = 50; //開50個程式,模擬50個使用者
        for ($i = 0; $i < $process_num; $i++) {
            MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
                    //合併兩條語句為一條更新語句
                    $res = Db::name('product')->where('id', 1)->where('count', '>', 0)->setDec('count');
                    if ($res) {
                        dump('獲取到更新資源許可權:' . $i);
                    }
            });
        }

只有十個程式獲取到了更新許可權,消費正常

淺談併發加鎖

淺談併發加鎖

檔案鎖機制解決

編輯程式碼如下:

        $process_num = 50; //開50個程式,模擬50個使用者
        for ($i = 0; $i < $process_num; $i++) {
            MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
                $filename = app()->getRootPath() . 'runtime/lock';
                $file = fopen($filename, 'w'); //開啟檔案
                $lock = flock($file, LOCK_EX);
                // $lock=flock($handle, LOCK_EX|LOCK_NB); (非同步非阻塞,所有程式如果出現獲取不到鎖,不等待跳過,加鎖失敗)
                //獲取檔案排他鎖:LOCK_EX(非同步阻塞,只有一個程式獲得鎖,其他競爭程式等待)
                //還有一種共享鎖:LOCK_SH(所有程式都可以獲取共享鎖,讀取檔案,當且只有一個鎖時,才允許寫操作,否則操作失敗,容易出現死鎖問題)
                if ($lock) {
                    try {
                        if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {
                            $res = Db::name('product')->where('id', 1)->setDec('count');
                            if ($res) {
                                dump('獲取到更新資源許可權:' . $i);
                            }
                        }
                    } catch (\Exception $e) {
                        dump($e->getMessage());
                    } finally {
                        flock($file, LOCK_UN); //無論如何都要釋放鎖
                    }
                }
                fclose($file); //關閉檔案控制程式碼
            });
        }

只有十個程式獲取到了更新許可權,消費正常

淺談併發加鎖

淺談併發加鎖

分散式鎖機制解決

以上檔案鎖,只適應於單體架構的需求,在叢集架構、分散式等多機聯網結構中就是掩耳盜鈴了,所以適應性更好地鎖機制還是要使用分散式鎖,分散式鎖最常用和最易用就是redis的setnx鎖了。
編輯程式碼如下:

        $process_num = 50; //開50個程式,模擬50個使用者
        for ($i = 0; $i < $process_num; $i++) {
            MultiProcessHelper::instance($process_num)->multiProcessTask(function () use ($i) {
                //獲取redis鎖
                //關於CacheHelper::getRedisLock是怎樣獲取鎖的,注意幾個點就行:1.如何避免死鎖;2.如何設定過期時間;3.如何設定搶佔條件;4.如何迴圈等待判斷。這些不在本文討論範圍,可自行研究,以後有空我也可以寫一篇博文
                $lock = CacheHelper::getRedisLock('redis_lock');
                if ($lock) {
                    try {
                        if (Db::name('product')->where('id', 1)->lock('for update')->value('count') > 0) {
                            $res = Db::name('product')->where('id', 1)->setDec('count');
                            if ($res) {
                                dump('獲取到更新資源許可權:' . $i);
                            }
                        }
                    } catch (\Exception $e) {
                        dump($e->getMessage());
                    }
                } else {
//                    dump('獲取redis鎖失敗');
                }
            });
        }

只有十個程式獲取到了更新許可權,消費正常

淺談併發加鎖

淺談併發加鎖

加鎖並不是最好的解決方案,只能達到資料安全的要求,同時效能會很大的損耗,可能出現死鎖或者請求長期等待返回5XX的問題,要根據場景和需求來做選擇和設計。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章