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
}
}
}
複製程式碼