简介
- MySQL 本身是不支持事务嵌套的,Laravel 用了一些技巧进行支持。
DB 对象到底是谁
DB 的启动周期。
【1、绑定,在 App 启动的时候绑定的对象】
'db' => 'Illuminate\Database\DatabaseManager'
【2、获取 Connection 对象】
'DatabaseManager'->connection() => '\Illuminate\Database\Connection'
【3、Connection 类头部】
class Connection implements ConnectionInterface {
use DetectsConcurrencyErrors,
DetectsLostConnections,
Concerns\ManagesTransactions;
...
}
【4、事务相关特质类,包含了所有事务相关的操作】
'Illuminate\Database\Concerns\ManagesTransactions'
开启事务
当我开始执行 DB::beginTransactions()
1、 创建事务代码。
public function beginTransaction()
{
$this->createTransaction();
$this->transactions++; // 计数器 +1
$this->fireConnectionEvent('beganTransaction'); // 一个空的事件,不管它
}
protected function createTransaction()
{
if ($this->transactions == 0) {
try {
$this->getPdo()->beginTransaction();
} catch (Exception $e) {
$this->handleBeginTransactionException($e);
}
} elseif ($this->transactions >= 1 && $this->queryGrammar->supportsSavepoints()) {
$this->createSavepoint();
}
}
【如果$this->transactions == 0】我们将直接调用 \PDO 对象开启一个事务
【如果$this->transactions >= 1】表示当前这个请求周期里面已经开启过事务
此时将会调用
protected function createSavepoint()
{
$this->getPdo()->exec(
// 保存位置,'SAVEPOINT trans'.($this->transactions + 1) 执行保存的语句
$this->queryGrammar->compileSavepoint('trans'.($this->transactions + 1))
);
}
public function compileSavepoint($name)
{
return 'SAVEPOINT '.$name;
}
依次类推每叠加依次执行 DB::beginTransactions()
那么就会增加一个新的存储点
SAVEPOINT trans 1
$this->transactions ++ ;
SAVEPOINT trans 2
$this->transactions ++ ;
...
这个计数器是关键!
目前创建完成事务。
提交事务
public function commit()
{
if ($this->transactions == 1) {
$this->getPdo()->commit();
}
$this->transactions = max(0, $this->transactions - 1);
$this->fireConnectionEvent('committed'); // 一个空的事件不理会它
}
【如果 $this->transactions == 1】 当前只存在一个开启的事务,直接提交即可,重置 $this->transactions 为 0。
【如果 $this->transactions > 1】 当前存在多个开启的事务,将 $this->transactions 进行减 1 操作。
这里可以看出来提交事务逻辑还是很简单的,不存在嵌套就直接提交,否则只是修改计数器。
回滚事务
public function rollBack($toLevel = null)
{
$toLevel = is_null($toLevel)
? $this->transactions - 1
: $toLevel;
if ($toLevel < 0 || $toLevel >= $this->transactions) {
return; // 计数器运算之后数值异常将 return
}
try {
$this->performRollBack($toLevel);
} catch (Exception $e) {
$this->handleRollBackException($e);
}
$this->transactions = $toLevel;
$this->fireConnectionEvent('rollingBack'); // 不管它
}
这里的执行逻辑是
【未指定 rollBack 的层数时计数器值减 1 并且赋值给 $toLevel】
【执行 performRollBack($toLevel) 根据指定的或者当前的层数进行回滚】
protected function performRollBack($toLevel)
{
if ($toLevel == 0) {
// '如果当前以及是最顶层了,直接 rollBack() 事务'
$this->getPdo()->rollBack();
} elseif ($this->queryGrammar->supportsSavepoints()) {
// 不是最顶层时,执行 'ROLLBACK TO SAVEPOINT trans'.($this->transactions + 1) ,
// 这里最关键的部分就是返回之前设定的存储点
// 想象一下打游戏的时候那些存档的点,当你返回到指定位置时,那么后面所有的资料都将回滚!
$this->getPdo()->exec(
$this->queryGrammar->compileSavepointRollBack('trans'.($toLevel + 1))
);
}
}
【重置 $this->transactions = $toLevel 到当前回滚的位置层数】
变化
事务的嵌套其实逻辑上理解起来很简单,很巧妙的用了 MySQL 的 SAVEPOINT 。
我们举个例子来看一下内部的数值改变。
DB::beginTransactions();
Model::create(['tag'=>'top level']); // $this->transactions = 1
DB::beginTransactions();
Model::create(['tag'=>'trans2']); // $this->transactions = 2
DB::beginTransactions();
Model::create(['tag'=>'trans3']); // $this->transactions = 3
DB::beginTransactions();
Model::create(['tag'=>'trans4']); // $this->transactions = 4
DB::rollBack(1); // 返回到 $this->transactions = 1 的位置。
DB::commit(); // 插入了 top Level 的记录。
拓展
关于事务部分提交以及回滚到指定位置,MySQL 提供的解决方式。
Laravel 嵌套事务也是用此原理实现。MySQL SAVEPOINT