數據庫加鎖,樂觀鎖、悲觀鎖

一、概念

悲觀鎖(Pessimistic Lock)

  • 每次獲取數據的時候,都會擔心數據被修改,所以每次獲取數據的時候都會進行加鎖(讀鎖、寫鎖、行鎖等),確保在自己使用的過程中數據不會被別人修改,使用完成後進行數據解鎖。由於數據進行加鎖,期間對該數據進行讀寫的其他線程都會進行等待。

樂觀鎖(Optimistic Lock)

  • 每次獲取數據的時候,都不會擔心數據被修改,所以每次獲取數據的時候都不會進行加鎖,但是在更新數據的時候需要判斷該數據是否被別人修改過。如果數據被其他線程修改,則不進行數據更新,如果數據沒有被其他線程修改,則進行數據更新。由於數據沒有進行加鎖,期間該數據可以被其他線程進行讀寫操作。一般會使用版本號機制或CAS操作實現

  • version方式實現樂觀鎖:一般是在數據表中加上一個數據版本號version字段,表示數據被修改的次數,當數據被修改時,version值會加一。當線程A要更新數據值時,在讀取數據的同時也會讀取version值,在提交更新時,若剛纔讀取到的version值爲當前數據庫中的version值相等時才更新,否則重試更新操作,直到更新成功。

    • sql代碼:
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

添加version字段,而不使用查到的目標值做判斷,應爲存在數值被改動,又被改回來的操作(如:同時有人買了某件商品,然後又退貨,這個對於商品剩餘的操作。),而用version每次加一,每次操作都是唯一的,,這樣的結果更準確。

對於數據庫的操作,是有返回值的(返回一個整數),可以根據返回值,判斷是否執行成功(影響到0行。因爲失敗並不一定是錯誤,只是不符合條件的更新失敗)

  • CAS操作方式:即compare and swap 或者 compare and set,涉及到三個操作數,數據所在的內存值,預期值,新值。當需要更新時,判斷當前內存值與之前取到的值是否相等,若相等,則用新值更新,若失敗則重試,一般情況下是一個自旋操作,即不斷的重試。

使用場景

  • 悲觀鎖:比較適合寫入操作比較頻繁的場景,如果出現大量的讀取操作,每次讀取的時候都會進行加鎖,這樣會增加大量的鎖的開銷,降低了系統的吞吐量。

  • 樂觀鎖:比較適合讀取操作比較頻繁的場景,如果出現大量的寫入操作,數據發生衝突的可能性就會增大,爲了保證數據的一致性,應用層需要不斷的重新獲取數據,這樣會增加大量的查詢操作,降低了系統的吞吐量。

  • 總結:兩種所各有優缺點,讀取頻繁使用樂觀鎖,寫入頻繁使用悲觀鎖。


二、具體實現方式

sql層面

例1:例如需要將用戶id爲1的用戶資產增加100元,mysql併發操作加鎖實現方式如下兩種:

  • 1.悲觀鎖
-- 查詢時直接加鎖(強制鎖定該條記錄的操作,直到自己操作完成)
-- SELECT FROM ... FOR UPDATE--
SELECT amount FROM user WHERE id = 1 FOR UPDATE;
-- 更新用戶資產
UPDATE user amout = amount+100 WHERE id = 1;

SELECT ... FOR UPDATE 的用法。由於InnoDB 預設是Row-Level Lock,所以只有「明確」的指定主鍵或者其他索引的鍵,MySQL 纔會執行Row lock (只鎖住被選取的數據) ,否則mysql 將會執行Table Lock (將整個數據表單給鎖住)。
只鎖住被選取的數據的好處是:多線程時,如果其他線程使用的是非鎖住的數據,則不會受影響,無需等待解鎖,更好的實現了併發。

  • 2.樂觀鎖

代碼爲展示sql邏輯

-- 查詢用戶資產
SELECT amount FROM user WHERE id = 1;
-- 將amount賦值給更新條件(這裏爲業務代碼內設置臨時變量,進行復制操作)
var amountTemp = amount;
-- 帶條件更新用戶資產
UPDATE user amout = amount+100 WHERE id = 1 AND amount = amountTemp;
  • 3.利用redis單線程模式加鎖

Django層面

可以在manager層面對錶數據設置鎖,在操作時進行上鎖、操作數據、解鎖操作。

class LockingManager(models.Manager):
    """ Add lock/unlock functionality to manager.
    1.將lock/unlock功能添加到manager
    2.在建表時對其上鎖(見下實例)

    """    

    def lock(self):
        """ Lock table. 
        鎖定對象模型表,以便可以進行原子更新。
        模擬數據庫訪問請求掛起,直到鎖解鎖()爲止。

        注意:如果需要鎖定多個表,則需要把它們
        所有鎖定在一個SQL子句,並且僅僅這樣是不夠的。 
        爲了避免死鎖,所有表必須以相同的順序鎖定。

        See http://dev.mysql.com/doc/refman/5.0/en/lock-tables.html
        """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        logger.debug("Locking table %s" % table)
        cursor.execute("LOCK TABLES %s WRITE" % table)
        row = cursor.fetchone()
        return row

    def unlock(self):
        """ Unlock the table. """
        cursor = connection.cursor()
        table = self.model._meta.db_table
        cursor.execute("UNLOCK TABLES")
        row = cursor.fetchone()
        return row 
        

應用實例:

class Job(models.Model):

    manager = LockingManager()  # 對錶進行加鎖

    counter = models.IntegerField(null=True, default=0)

    @staticmethod
    def do_atomic_update(job_id)
        ''' 更新job表下的counter這個integer, 保持它小於5 '''
        try:
            # 確保只有一個 HTTP 請求可以執行此次更新
            Job.objects.lock()

            job = Job.object.get(id=job_id)
            
            # 如果我們不鎖定表,則兩個同時發出的請求可能會使計數器超過5
            if job.counter < 5:
                job.counter += 1                                        
                job.save()

        finally:
            Job.objects.unlock()
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章