關於 MySQL 的巢狀事務

cheer發表於2020-01-02

MySQL事務

事務


事務的概念就不介紹了, 它的目的是解決ACID問題.

  • MySQL中的事務必須是InnoDB、Berkeley DB引擎,myisam不支援, 新出了一個memory, 貌似也是不支援的, 大家可以查查。
  • MySQL預設是autocommit=1,也就是說預設是立即提交,如果想開啟事務,先設定autocommit=0,然後用START TRANSACTION、 COMMIT、 ROLLBACK來使用具體的事務。

    你可能會有一個疑問: 查詢會不會開事務?

    根據上面兩點可以得出, 如果你在INNODB事務引擎下, 並且autocommit=1 (預設值), 答案是會, 否則不會. 其他引擎不支援事務, 這個問題也不存在.

    這時候你可能又有另外一個疑問: 我只是執行單個查詢語句, 為什麼要開事務?

  • 如果你一次執行單條查詢語句,則沒有必要啟用事務支援,資料庫預設支援SQL執行期間的讀一致性;
  • 如果你一次執行多條查詢語句,例如統計查詢,報表查詢,在這種場景下,多條查詢SQL必須保證整體的讀一致性,否則,在前條SQL查詢之後,後條SQL查詢之前,資料被其他使用者改變,則該次整體的統計查詢將會出現讀資料不一致的狀態,此時,應該啟用事務支援。

    簡而言之, 因為你需要ACID中的CI: consistency && isolated

    巢狀事務

    Transactions cannot be nested. This is a consequence of the implicit commit performed for any current transaction when you issue a START TRANSACTION statement or one of its synonyms.

    Mysql是不支援巢狀事務的,開啟了一個事務的情況下,再開啟一個事務,會隱式的提交上一個事務. 所以我們就要在系統架構層面來支援事務的巢狀, 常見的做法就是SAVEPOINT.

    savePoint機制

==測試資料表結構==

id stock
1 109
START TRANSACTION;
SAVEPOINT a;
UPDATE `user` SET stock = 1 WHERE id = 1;
SAVEPOINT b;
UPDATE `user` SET stock = 2 WHERE id = 1;
ROLLBACK TO b;
commit;

執行的程式碼,會發有資料表中id=1的行stock值被修改為1。

解讀一下程式碼

  1. 開啟事物
  2. 建立儲存點a
  3. 更新資料stock為1
  4. 建立儲存點b(此時id=1的資料stock=1)
  5. 更新stock為2
  6. 回滾至savepoint b
  7. 提交事物

粗讀這段程式碼,即可以知道,savepoint是可以在事務進行中建立儲存點,類似遊戲中的存檔一樣,但是有個不一樣的地方就是savepoint的持有者是事務,當事務commit或者是完全rollback之後,savepoint就會釋放。

THINKPHP

PHP框架THINKPHP5的巢狀事務處理策略: 如果開啟了supportSavepoint, 則利用SAVEPOINT來等價子事務, 否則啥也不幹, MySQL驅動下預設開啟

這產生了一個很重要的結論: 子事務的回滾不會導致事務回滾, 只有第一個事務的回滾才是真正的ROLLBACK

==TP5.1關於巢狀事務的實現原始碼擷取==

    /**
     * 啟動事務
     * @access public
     * @return void
     * @throws \PDOException
     * @throws \Exception
     */
    public function startTrans()
    {
        $this->initConnect(true);
        if (!$this->linkID) {
            return false;
        }

        ++$this->transTimes;

        try {
            if (1 == $this->transTimes) {
                $this->linkID->beginTransaction();
            } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
                $this->linkID->exec(
                    $this->parseSavepoint('trans' . $this->transTimes)
                );
            }
        } catch (\Exception $e) {
            if ($this->isBreak($e)) {
                --$this->transTimes;
                return $this->close()->startTrans();
            }
            throw $e;
        }
    }

    /**
     * 事務回滾
     * @access public
     * @return void
     * @throws PDOException
     */
    public function rollback()
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->rollBack();
        } elseif ($this->transTimes > 1 && $this->supportSavepoint()) {
            $this->linkID->exec(
                $this->parseSavepointRollBack('trans' . $this->transTimes)
            );
        }

        $this->transTimes = max(0, $this->transTimes - 1);
    }

    /**
     * 用於非自動提交狀態下面的查詢提交
     * @access public
     * @return void
     * @throws PDOException
     */
    public function commit()
    {
        $this->initConnect(true);

        if (1 == $this->transTimes) {
            $this->linkID->commit();
        }

        --$this->transTimes;
    }

    /**
     * 是否支援事務巢狀
     * @return bool
     */
    protected function supportSavepoint()
    {
        return false;
    }

    /**
     * 生成定義儲存點的SQL
     * @access protected
     * @param  $name
     * @return string
     */
    protected function parseSavepoint($name)
    {
        return 'SAVEPOINT ' . $name;
    }

==從TP的實現原始碼上面可以知道,TP框架在出現巢狀事務時,處理方法就是採用savepoint的策略。startTrans 與 rollback方法都不是直接執行mysql的rollback。他執行分兩步==

  • 判斷是不是第一次開啟事務,如果是的話就開啟事務,否則這是單純的執行savepoint方法。
  • 回滾rollback的時候也是同理,只有當時最外層的事務執行rollback時,他才會執行mysql的rollback,否則都是rollback to savepoint。
本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章