1. 程式人生 > 程式設計 >Yii2資料庫事務原始碼

Yii2資料庫事務原始碼

更多原始碼文章已釋出到https://github.com/Zhucola/php_frameworks_analysis

事務與事務巢狀

yii有兩種事務模式,一種是需要自己commit和捕獲異常rollback,還有一種令是使用回撥的方式,如控制器程式碼如下:

public function actionTest()
{
    //db元件配置
    $db = new \yii\db\Connection([
	    'dsn' => 'mysql:host=192.168.0.10;dbname=test','username' => 'root','password' => ''
,'charset' => 'utf8','enableSlaves'=>true,//可以使用從庫 'serverRetryInterval'=>600,//其中一個從庫配置不可用,將快取不可用狀態600秒 'enableProfiling'=>true,//預設配置,將記錄連線資料庫、執行語句等的效能分析日誌 'emulatePrepare'=>true,//true為開啟本地模擬prepare 'shuffleMasters'=>false,'serverStatusCache'=>false,'slaveConfig'=>[ //從庫slaves屬性通用配置 'username'
=> 'root','attributes' => [ PDO::ATTR_TIMEOUT => 1,],'slaves'=>[ //從庫列表 ["dsn"=>"mysql:host=192.168.0.10;dbname=test"],'masters'=>[ //主庫列表 ["dsn"=>"mysql:host=192.168.0.10;dbname=test"],'masterConfig'=>[ //主庫master屬性通用配置 'username' => 'root',]); //第一種事務模式,需要自己去commit和捕獲異常rollback $transaction
= $db->beginTransaction(); try { $command1 = $db->createCommand("insert into a([[age]]) value(:age)"); $res1 = $command1->bindValue(":age",111)->execute(); $command2 = $db->createCommand("update a set age = 1"); $res2 = $command2->execute(); $transaction->commit(); } catch (\Exception $e) { $transaction->rollback(); } //第二種事務模式,不用自己去捕獲異常然後去rollback $db->transaction(function($db){ $command1 = $db->createCommand("insert into a([[age]]) value(:age)"); $res1 = $command1->bindValue(":age",111)->execute(); $command2 = $db->createCommand("update a set age = 1"); $res2 = $command2->execute(); }); return 123; } 複製程式碼

我們先從第一種模式的原始碼開始看,第一個方法是beginTransaction
從原始碼分析可知,事務一定會去連線master,具體master和slave連線原始碼分析請看我的另一篇連線資料庫的原始碼分析文章

public function beginTransaction($isolationLevel = null)
{
    //事務一定會去連線master
    $this->open();
    //已經開啟了事務就不用再去例項化Transaction類了
    if (($transaction = $this->getTransaction()) === null) {
        $transaction = $this->_transaction = new Transaction(['db' => $this]);
    }
    $transaction->begin($isolationLevel);
    //返回Transaction物件
    return $transaction;
}
複製程式碼

可見事務的核心是一個單獨的Transaction類,該類繼承與BaseObject所以只有屬性注入,無行為behavior和事件Events

class Transaction extends \yii\base\BaseObject
{
    //以下幾個常量都是隔離級別
    const READ_UNCOMMITTED = 'READ UNCOMMITTED';
    const READ_COMMITTED = 'READ COMMITTED';
    const REPEATABLE_READ = 'REPEATABLE READ';
    const SERIALIZABLE = 'SERIALIZABLE';
    public $db;
    //事務層級,用來模擬多層級事務
    private $_level = 0;
複製程式碼

其實yii的事務是有幾個Events事件的,只不過事件標識是掛在Connection類的

class Connection extends Component
{
    const EVENT_AFTER_OPEN = 'afterOpen';
    //事務開啟事件標識
    const EVENT_BEGIN_TRANSACTION = 'beginTransaction';
    //事務提交事件標識
    const EVENT_COMMIT_TRANSACTION = 'commitTransaction';
    //事務回滾事件標識
    const EVENT_ROLLBACK_TRANSACTION = 'rollbackTransaction';
複製程式碼

回到Transaction類的begin方法
我們知道mysql是不支援多事務層級的,在第一次begin開啟事務之後,再次begin會commit第一次的事務,yii根據事務層級level和savepoint來模擬多事務層級
單事務原始碼邏輯:
1.第一次開啟事務,事務層級level是1(自增)
2.提交或者回滾,事務層級level為0(自減),會真正的commit或者rollback

多事務原始碼邏輯
1.第一次開啟事務,事務層級level是1(自增)
2.再次開啟事務,事務層級level是2(自增),savepoint名字是LEVEL2
3.再次開啟事務,事務層級level是3(自增),savepoint名字是LEVEL3
4.提交,事務層級level是2(自減),刪除名字是LEVEL3的savepint,不會真正的commit
5.回滾,事務層級level是1(自減),回滾名字是LEVEL2的savepoint,會真實rollback同時也會把LEVEL2的savepoint刪除
6.提交或者回滾,因為事務層級level是1自減為0,所以表示是最外層事務了,會真正的commit或者rollback

public function begin($isolationLevel = null)
{
    if ($this->db === null) {
        throw new InvalidConfigException('Transaction::db must be set.');
    }
    //連線master
    $this->db->open();
    //如果是第一次開啟事務
    if ($this->_level === 0) {
        if ($isolationLevel !== null) {
            //設定事務隔離級別
            $this->db->getSchema()->setTransactionIsolationLevel($isolationLevel);
        }
        //記錄debug級別日誌
        Yii::debug('Begin transaction' . ($isolationLevel ? ' with isolation level ' . $isolationLevel : ''),__METHOD__);
        //觸發開啟事件事件
        $this->db->trigger(Connection::EVENT_BEGIN_TRANSACTION);
        //pdo開啟事務
        $this->db->pdo->beginTransaction();
        //事務層級為1
        $this->_level = 1;

        return;
    }
    //如果不是第一次開啟事務(多層級事務)
    $schema = $this->db->getSchema();
    //判斷db元件配置是否支援使用多層級事務
    if ($schema->supportsSavepoint()) {
        //記錄debug級別日誌
        Yii::debug('Set savepoint ' . $this->_level,__METHOD__);
        //打一個savepoint點,注意savepoint點的名字是和事務層級有關
        $schema->createSavepoint('LEVEL' . $this->_level);
    } else {
        Yii::info('Transaction not started: nested transaction not supported',__METHOD__);
        throw new NotSupportedException('Transaction not started: nested transaction not supported.');
    }
    //多層級事務,自增事務層級
    $this->_level++;
}
複製程式碼

提交commit的原始碼

public function commit()
{
    if (!$this->getIsActive()) {
        throw new Exception('Failed to commit transaction: transaction was inactive.');
    }
    //自減
    $this->_level--;
    //只剩下了一個事務
    if ($this->_level === 0) {
        Yii::debug('Commit transaction',__METHOD__);
	//真實提交
        $this->db->pdo->commit();
	//觸發提交事件
        $this->db->trigger(Connection::EVENT_COMMIT_TRANSACTION);
        return;
    }
    //多層級事務
    $schema = $this->db->getSchema();
    //判斷是否支援多層級事務
    if ($schema->supportsSavepoint()) {
        Yii::debug('Release savepoint ' . $this->_level,__METHOD__);
	//刪除savepint,根據事務層級來刪
        $schema->releaseSavepoint('LEVEL' . $this->_level);
    } else {
        Yii::info('Transaction not committed: nested transaction not supported',__METHOD__);
    }
}
複製程式碼

回滾rollback原始碼

public function rollBack()
{
    if (!$this->getIsActive()) {
        // do nothing if transaction is not active: this could be the transaction is committed
        // but the event handler to "commitTransaction" throw an exception
        return;
    }
    //自減
    $this->_level--;
    //只剩下了一個事務
    if ($this->_level === 0) {
        Yii::debug('Roll back transaction',__METHOD__);
	//真實回滾
        $this->db->pdo->rollBack();
	//觸發回滾事件
        $this->db->trigger(Connection::EVENT_ROLLBACK_TRANSACTION);
        return;
    }

    $schema = $this->db->getSchema();
    //多層級事務
    //判斷是否支援多層級事務
    if ($schema->supportsSavepoint()) {
        Yii::debug('Roll back to savepoint ' . $this->_level,__METHOD__);
	//回滾savepoint,會真實回滾
        $schema->rollBackSavepoint('LEVEL' . $this->_level);
    } else {
        Yii::info('Transaction not rolled back: nested transaction not supported',__METHOD__);
    }
}
複製程式碼

yii的第二種事務方式是不用自己捕獲事務,實現的方案就是原始碼給封裝好了try,原始碼很簡單就不多做解釋

public function transaction(callable $callback,$isolationLevel = null)
{
    //開事務
    $transaction = $this->beginTransaction($isolationLevel);
    $level = $transaction->level;

    try {
        //執行回撥
        $result = call_user_func($callback,$this);
	//成功的話就commit
        if ($transaction->isActive && $transaction->level === $level) {
            $transaction->commit();
        }
    } catch (\Exception $e) {
        //失敗就回滾
        $this->rollbackTransactionOnLevel($transaction,$level);
        throw $e;
    } catch (\Throwable $e) {
        //程式碼語法級別失敗就回滾
        $this->rollbackTransactionOnLevel($transaction,$level);
        throw $e;
    }

    return $result;
}
...
private function rollbackTransactionOnLevel($transaction,$level)
{
    if ($transaction->isActive && $transaction->level === $level) {
        // https://github.com/yiisoft/yii2/pull/13347
        try {
            $transaction->rollBack();
        } catch (\Exception $e) {
            \Yii::error($e,__METHOD__);
            // hide this exception to be able to continue throwing original exception outside
        }
    }
}
複製程式碼