mysql事務原理以及鎖


一、Innodb事務原理

  1.什么是事務

    a.事務(Transaction)是數據庫區別於文件系統的重要特性之一,事務會把數據庫從一種一致性狀態轉換為另一種一致性狀態。

    b.在數據庫提交時,可以確保要么所有修改都已保存,要么所有修改都不保存。

  2.事務的特性:(ACID)

    a.原子性(Atomicity):事務中的全部操作在數據庫中是不可分割的,要么全部完成,要么均不執行。

    b.一致性(Consistency):幾個並行執行的事務,其執行結果必須與按某一順序串行執行的結果相一致。

    c.隔離性(Isolation):事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務必須是透明的。

    d.持久性(Durability):對於任意已提交事務,系統必須保證該事務對數據庫的改變不被丟失,即使數據庫出現故障.

   3.事務的分類:

    3.1扁平事務(Flat Transactions)

      a.扁平事務是事務類型中最簡單但使用最頻繁的事務。

      b.在扁平事務中,所有的操作都處於同一層次,由BEGIN/START TRANSACTION開始事務,由COMMIT/ROLLBACK結束,且都是原子的,要么都執行,要么都回滾

      c.扁平事務是應用程序成為原子操作的基本組成模塊。

    扁平事務一般有四種不同的結果:

      1.事務成功完成

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into student(name) value('wangwu');
Query OK, 1 row affected (0.00 sec)

mysql> update student set name = '王五' where id = 4;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from student;
+----+--------+
| id | name   |
+----+--------+
|  1 | 張三   |
|  2 | 李四   |
|  3 | wangwu |
|  4 | 王五   |
+----+--------+
4 rows in set (0.00 sec)
事務成功完成

      2.應用程序要求停止事務。比如應用程序在捕獲到異常時會回滾事務

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into student(name) value('joke');
Query OK, 1 row affected (0.02 sec)

mysql> select * from student;
+----+--------+
| id | name   |
+----+--------+
|  1 | 張三   |
|  2 | 李四   |
|  3 | wangwu |
|  4 | 王五   |
|  5 | joke   |
+----+--------+
5 rows in set (0.00 sec)

mysql> rollback;
Query OK, 0 rows affected (0.07 sec)

mysql> select * from student;
+----+--------+
| id | name   |
+----+--------+
|  1 | 張三   |
|  2 | 李四   |
|  3 | wangwu |
|  4 | 王五   |
+----+--------+
4 rows in set (0.00 sec)
應用程序要求停止事務

      3.外界因素強制終止事務。如連接超時或連接斷開

mysql> begin;
Query OK, 0 rows affected (0.07 sec)

mysql> insert into student(name) value('田七');
Query OK, 1 row affected (0.01 sec)

mysql> # 此時,我將MySQL的服務停止掉了,去執行刪除操作
mysql> delete from student where id=7;
ERROR 2013 (HY000): Lost connection to MySQL server during query

msyql> # 此時,我將MySQL的服務重新啟動,去執行刪除操作
mysql> delete from student where id=7;
ERROR 2006 (HY000): MySQL server has gone away
No connection. Trying to reconnect...
Connection id:    3
Current database: mytest

Query OK, 0 rows affected (0.03 sec)

mysql> select * from student;
+----+--------+
| id | name   |
+----+--------+
|  1 | 張三   |
|  2 | 李四   |
|  3 | wangwu |
|  4 | 王五   |
|  6 | 趙六   |
+----+--------+
5 rows in set (0.00 sec)
外界因素強制終止事務

      4.帶有保存節點的扁平事務

        a.帶有保存節點的扁平事務允許事務在執行過程中回滾到較早的一個狀態,而不是回滾所有的操作。

        b.保存點用來通知系統應該記住事務當前的狀態,以便當之后發生錯誤時,事務能回到保存點當時的狀態。

        c.對於扁平事務來說,在事務開始時隱式地設置了一個保存點,回滾時只能回滾到事務開始時的狀態。

#帶有保存節點的扁平事務
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from student;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 張三 |  18 |
|  2 | 李四 |  19 |
+----+------+-----+
2 rows in set (0.00 sec)

mysql> update student set age=28 where id=1;
Query OK, 1 row affected (0.04 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> savepoint sp1;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into student(name,age) value ('王五',20);
Query OK, 1 row affected (0.00 sec)

mysql> rollback to sp1;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from student;
+----+------+-----+
| id | name | age |
+----+------+-----+
|  1 | 張三 |  28 |
|  2 | 李四 |  19 |
+----+------+-----+
2 rows in set (0.00 sec)
帶有保存節點的扁平事務

    3.2、鏈事務

      什么是鏈事務: 

        a.鏈事務(Chained Transaction)是指一個事務由多個子事務鏈式組成

        b.前一個子事務的提交操作和下一個子事務的開始操作合並成一個原子操作,這意味着下一個事務將看到上一個事務的結果,就好像在一個事務中進行的一樣。

        c.在提交子事務時就可以釋放不需要的數據對象,而不必等到整個事務完成后才釋放。

      鏈事務工作示意圖:

      鏈事務與扁平事務的區別:

        a.鏈事務中的回滾僅限於當前事務,相當於只能恢復到最近的一個保存節點

        b.帶保存節點的扁平事務能回滾到任意正確的保存點

        c.帶有保存節點的扁平事務中的保存點是易失的,當發生系統崩潰是,所有的保存點都將消失,這意味着當進行恢復時,事務需要從開始處重新執行。

     3.3、嵌套事務

      什么是嵌套事務?

        a.嵌套事務(Nested Transaction)是一個層次結構框架。

        b.由一個頂層事務(top-level transaction)控制着各個層次的事務。

        c.頂層事務之下嵌套的事務成為子事務(subtransaction),其控制着每一個局部的操作,子事務本身也可以是嵌套事務.

        d.嵌套事務的層次結構可以看成是一顆樹.

      嵌套事務結構如下圖所示:

  4.事務的隔離級別

     SQL標准定義的四個隔離級別:

      a.READ UNCOMMITTED: 未提交讀,作用域為(global)全局 、(session)當前會話

mysql> set @@global.tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)

mysql> set @@session.tx_isolation='READ-UNCOMMITTED';
Query OK, 0 rows affected (0.00 sec)
未提交讀

      b.READ COMMITTED: 提交讀

mysql> set @@global.tx_isolation='READ-COMMITTED';
Query OK, 0 rows affected (0.00 sec)
提交讀

      c.REPEATABLE READ: 可重復讀

mysql> set @@global.tx_isolation='REPEATABLE-READ';
Query OK, 0 rows affected (0.00 sec)
可重復讀

      d.SERIALIZABLE: 可串行讀

mysql> set @@global.tx_isolation='SERIALIZABLE';
Query OK, 0 rows affected (0.00 sec)
可串行讀

     4.1、Read uncommitted 

      讀取未提交內容。在該隔離級別下,所有事務都可以看到其它未提交事務的執行結果。

      a.事務2查詢到的數據是事務1中修改但未提交的數據

      b.但因為事務1回滾了數據,所以事務2查詢的數據是不正確的

      c.因此出現了臟讀的問題

    4.2、Read committed

      1.讀取提交內容。在該隔離級別下,一個事務從開始到提交之前對數據所做的改變對其它事務是不可見的,這樣就解決在READ-UNCOMMITTED級別下的臟讀問題。

      2.但如果一個事務在執行過程中,其它事務的提交對該事物中的數據發生改變,那么該事務中的一個查詢語句在兩次執行過程中會返回不一樣的結果

 

      a.事務2執行update語句但未提交前,事務1的前兩個select操作返回結果是相同的。

      b.但事務2執行commit操作后,事務1的第三個select操作就讀取到事務2對數據的改變,導致與前兩次select操作返回不同的數據

      c.因此出現了不可重復讀的問題。

    4.3、Repeatable read

      1.可重復讀。這是MySQL的默認事務隔離級別,能確保事務在並發讀取數據時會看到同樣的數據行,解決了READ-COMMITTED隔離級別下的不可重復讀問題

        2.MySQL的InnoDB存儲引擎通過多版本並發控制(Multi_Version Concurrency Control, MVCC)機制來解決該問題

      3.在該機制下,事務每開啟一個實例,都會分配一個版本號給它,如果讀取的數據行正在被其它事務執行DELETE或UPDATE操作(即該行上有排他鎖),這時該事物的讀取操作不會等待行上的鎖釋放,而是根據版本號去讀取行的快照數據(記錄在undo log中)

      4.這樣,事務中的查詢操作返回的都是同一版本下的數據,解決了不可重復讀問題

      a.雖然該隔離級別下解決了不可重復讀問題,但理論上會導致另一個問題:幻讀(Phantom Read)

        b.正如上面所講,一個事務在執行過程中,另一個事物對已有數據行的更改

      c.MVCC機制可保障該事物讀取到的原有數據行的內容相同,但並不能阻止另一個事務插入新的數據行,這就會導致該事物中憑空多出數據行,像出現了幻讀一樣,這便是幻讀問題

      1)事務2對id=1的行內容進行了修改並且執行了commit操作

      2)事務1中的第二個select操作在MVCC機制的作用下返回的仍是v=1的數據

      3)事務3執行了insert操作

      4) 事務1第三次執行select操作時便返回了id=2的數據行,與前兩次的select操作返回的值不一樣

    需要說明的是,REPEATABLE-READ隔離級別下的幻讀問題是SQL標准定義下理論上會導致的問題,MySQL的InnoDB存儲引擎在該隔離級別下,采用了Next-Key Locking鎖機制避免了幻讀問題。Next-Key Locking鎖機制將在后面的鎖章節中講到。

     4.4 Serializable

      1.可串行化。這是事務的最高隔離級別

      2.通過強制事務排序,使之不可能相互沖突,就是在每個讀的數據行加上共享鎖來實現

      3.在該隔離級別下,可以解決前面出現的臟讀、不可重復讀和幻讀問題

      4.但也會導致大量的超時和鎖競爭現象,一般不推薦使用。

二、Mysql數據庫中的鎖  

  1、MyISAM和InnoDB支持的鎖類型

      1. 相對其他數據庫而言,MySQL的鎖機制比較簡單,其最顯著的特點是不同的存儲引擎支持不同的鎖機制。

      2. MyISAM和MEMORY存儲引擎采用的是表級鎖(table-level locking)。

      3. InnoDB存儲引擎既支持行級鎖(row-level locking),也支持表級鎖,但默認情況下是采用行級鎖。

  2、MySQL這3種鎖的特性

    1)行級鎖

        1. 行級鎖分為共享鎖和排它鎖,行級鎖是Mysql中鎖定粒度最細的鎖。

        2. InnoDB引擎支持行級鎖和表級鎖,只有在通過索引條件檢索數據的時候,才使用行級鎖,否就使用表級鎖。

        3. 行級鎖開銷大,加鎖慢,鎖定粒度最小,發生鎖沖突概率最低,並發度最高

        舉例: 只根據主鍵進行查詢,並且查詢到數據,主鍵字段產生行鎖。

#### 行鎖
'''
client1中執行:
    select * from shop where id=1 for update;
clenet2中執行:
    select * from shop where id=2 for update;   # 可以正常放回數據
    select * from shop where id=1 for update;   # 阻塞
'''
# 可以看到:id是主鍵,當在client1上查詢id=1的數據時候,在client2上查詢id=2的數據沒問題
# 但在client2上查詢id=1的數據時阻塞,說明此時的鎖時行鎖。
# 當client1執行commit時,clinet2查詢的id=1的命令立即返回數據。
產生行鎖

    2)表級鎖

        1. 表級鎖分為表共享鎖和表獨占鎖。

        2. 表級鎖開銷小,加鎖快,鎖定粒度大、發生鎖沖突最高,並發度最低

        舉例:根據非主鍵不含索引(name)進行查詢,並且查詢到數據,name字段產生表鎖。

#### 表鎖
# 可以看到,client1通過非索引的name字段查詢到prod11的數據后,在client2查prod**的數據會阻塞,產生表鎖。
'''
client1中執行:
    select * from shop where name="prod11" for update;
clenet2中執行:
    select * from shop where name="prod**" for update;
'''
產生表鎖

    3)頁級鎖

        1. 頁級鎖是MySQL中鎖定粒度介於行級鎖和表級鎖中間的一種鎖。

        2. 表級鎖速度快,但沖突多,行級沖突少,但速度慢。

        3. 所以取了折衷的頁級,一次鎖定相鄰的一組記錄,BDB支持頁級鎖。

        4. 開銷和加鎖時間界於表鎖和行鎖之間;會出現死鎖;鎖定粒度界於表鎖和行鎖之間,並發度一般。

    總結:

        1. 表級鎖更適合於以查詢為主,只有少量按索引條件更新數據的應用,如Web應用;

        2. 而行級鎖則更適合於有大量按索引條件並發更新少量不同數據,同時又有並發查詢的應用,如一些在線事務處理(OLTP)系統。

  3、鎖分類

      1. 按操作划分:DML鎖,DDL鎖

      2. 按鎖的粒度划分:表級鎖、行級鎖、頁級鎖

      3. 按鎖級別划分:共享鎖、排他鎖

      4. 按加鎖方式划分:自動鎖、顯示鎖

      5. 按使用方式划分:樂觀鎖、悲觀鎖

  4、樂觀鎖悲觀鎖作用

      1. 在並發訪問情況下,很有可能出現不可重復讀等等讀現象。

      2. 為了更好的應對高並發,封鎖、時間戳、樂觀並發控制(樂觀鎖)、
          悲觀並發控制(悲觀鎖)都是並發控制采用的主要技術方式。

  5、悲觀鎖

      1. 悲觀鎖的實現,往往依靠數據庫提供的鎖機制

      2. MySQL會對查詢結果集中每行數據都添加排他鎖,其他線程對該記錄的更新與刪除操作都會阻塞,排他鎖包含行鎖、表鎖。

      3. 申請前提:沒有線程對該結果集中的任何行數據使用排他鎖或共享鎖,否則申請會阻塞。

      適用場景:悲觀鎖適合寫入頻繁的場景。

      注:

        首先我們需要set autocommit=0,即不允許自動提交
        用法:select * from tablename where id = 1 for update;

  6、樂觀鎖

      1. 在更新數據的時候需要比較程序中的庫存量與數據庫中的庫存量是否相等,如果相等則進行更新。

      2. 反之程序重新獲取庫存量,再次進行比較,直到兩個庫存量的數值相等才進行數據更新。

  7、舉例:對商品數量-1操作

    1)悲觀鎖實現方法

        1. 每次獲取商品時,對該商品加排他鎖。

        2. 也就是在用戶A獲取獲取 id=1 的商品信息時對該行記錄加鎖,期間其他用戶阻塞等待訪問該記錄。

#### 悲觀鎖實現加一操作代碼
# 我們可以看到,首先通過begin開啟一個事物,在獲得shop信息和修改數據的整個過程中都對數據加鎖,保證了數據的一致性。
'''
begin;
select id,name,stock as old_stock from shop  where id=1 for update;
update shop set stock=stock-1 where id=1 and stock=old_stock;
commit
'''
悲觀鎖   
SKU.objects.select_for_update().get(id=1)
python使用悲觀鎖

    2)樂觀鎖實現方法

        1. 每次獲取商品時,不對該商品加鎖。

        2. 在更新數據的時候需要比較程序中的庫存量與數據庫中的庫存量是否相等,如果相等則進行更新

        3. 反之程序重新獲取庫存量,再次進行比較,直到兩個庫存量的數值相等才進行數據更新。

#### 樂觀鎖實現加一操作代碼
# 我們可以看到,只有當對數量-1操作時才會加鎖,只有當程序中值和數據庫中的值相等時才正真執行。
'''
//不加鎖
select id,name,stock where id=1;
//業務處理
begin;
update shop set stock=stock-1 where id=1 and stock=stock;
commit;
'''
樂觀鎖
SKU.objects.filter(id=1, stock=7).update(stock=2)
python使用樂觀鎖  

  8、python適用樂觀鎖解決事物問題

      使用 django.db.transaction 模塊解決MySQL 事物管理 問題   

      1. 在事務當前啟動celery異步任務, 無法獲取未提交的改動.

      2. 在使用transaction當中, Model.save()都不做commit .

      3. 因此如果在transaction當中設置異步任務,使用get()查詢數據庫,將看不到對象在事務當中的改變.

      4. 這也是實現”可重復讀”的事務隔離級別,即同一個事務里面的多次查詢都應該保持結果不變.

# with語句用法

from django.db import transaction

def viewfunc(request):
    # 這部分代碼不在事務中,會被Django自動提交
    ...

    with transaction.atomic():
        # 這部分代碼會在事務中執行
        ...
'''
from django.db import transaction

# 創建保存點
save_id = transaction.savepoint()  

# 回滾到保存點
transaction.savepoint_rollback(save_id)

# 提交從保存點到當前狀態的所有數據庫事務操作
transaction.savepoint_commit(save_id)
'''
使用transaction模塊解決mysql事物問題
from django.db import transaction

def create(self, validated_data):
        """
        保存訂單
        """
        # 獲取當前下單用戶
        user = self.context['request'].user

        # 組織訂單編號 20170903153611+user.id
        # timezone.now() -> datetime
        order_id = timezone.now().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)

        address = validated_data['address']
        pay_method = validated_data['pay_method']

        # 生成訂單
        with transaction.atomic():
            # 創建一個保存點
            save_id = transaction.savepoint()

            try:
                 # 創建訂單信息
                order = OrderInfo.objects.create(
                    order_id=order_id,
                    user=user,
                    address=address,
                    total_count=0,
                    total_amount=Decimal(0),
                    freight=Decimal(10),
                    pay_method=pay_method,
                    status=OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID']
                )
                # 獲取購物車信息
                redis_conn = get_redis_connection("cart")
                redis_cart = redis_conn.hgetall("cart_%s" % user.id)
                cart_selected = redis_conn.smembers('cart_selected_%s' % user.id)

                # 將bytes類型轉換為int類型
                cart = {}
                for sku_id in cart_selected:
                    cart[int(sku_id)] = int(redis_cart[sku_id])

                # 一次查詢出所有商品數據
                skus = SKU.objects.filter(id__in=cart.keys())

                # 處理訂單商品
                for sku in skus:
                    sku_count = cart[sku.id]

                    # 判斷庫存
                    origin_stock = sku.stock  # 原始庫存
                    origin_sales = sku.sales  # 原始銷量

                    if sku_count > origin_stock:
                        transaction.savepoint_rollback(save_id)
                        raise serializers.ValidationError('商品庫存不足')

                    # 用於演示並發下單
                    # import time
                    # time.sleep(5)

                    # 減少庫存
                    new_stock = origin_stock - sku_count
                    new_sales = origin_sales + sku_count

                    sku.stock = new_stock
                    sku.sales = new_sales
                    sku.save()

                    # 累計商品的SPU 銷量信息
                    sku.goods.sales += sku_count
                    sku.goods.save()

                    # 累計訂單基本信息的數據
                    order.total_count += sku_count  # 累計總金額
                    order.total_amount += (sku.price * sku_count)  # 累計總額

                    # 保存訂單商品
                    OrderGoods.objects.create(
                        order=order,
                        sku=sku,
                        count=sku_count,
                        price=sku.price,
                    )

                # 更新訂單的金額數量信息
                order.total_amount += order.freight
                order.save()

            except ValidationError:
                raise
            except Exception as e:
                logger.error(e)
                transaction.savepoint_rollback(save_id)
                raise

            # 提交事務
            transaction.savepoint_commit(save_id)

            # 更新redis中保存的購物車數據
            pl = redis_conn.pipeline()
            pl.hdel('cart_%s' % user.id, *cart_selected)
            pl.srem('cart_selected_%s' % user.id, *cart_selected)
            pl.execute()
            return order
transaction使用實例

  9、MySQL中 共享鎖 和 排它鎖 

    1)排它鎖

        1. 排它鎖又叫寫鎖,如果事務T對A加上排它鎖,則其它事務都不能對A加任何類型的鎖。獲准排它鎖的事務既能讀數據,又能寫數據。

        2. 用法 :  SELECT … FOR UPDATE

    2)共享鎖(share lock)

        1. 共享鎖又叫讀鎖,如果事務T對A加上共享鎖,則其它事務只能對A再加共享鎖,不能加其它鎖。

        2. 獲准共享鎖的事務只能讀數據,不能寫數據。

        3. 用法: SELECT … LOCK IN SHARE MODE;

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM