Zhucola 2019-06-22 21:07:51 4097次浏览 0条评论 0 0 0

文章已经同步到 GitHub 仓库 https://github.com/Zhucola/yii_core_debug ,欢迎 star~~

事务与事务嵌套

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',
	    	'password' => '',
	    	'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',
	    	'password' => '',
	    	'attributes' => [
        		PDO::ATTR_TIMEOUT => 1,
    		],
	    ],
	]);
  //第一种事务模式,需要自己去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连接源码分析请看连接mysql的源码分析https://www.yiichina.com/code/1947

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
        }
    }
}
    没有找到数据。
您需要登录后才可以评论。登录 | 立即注册