Mysql是不支持事务嵌套的
本来你定义了一个方法,如下
function method1() {
try {
//开启事务
// 代码逻辑
// 提交事务
} catch(Exception $e) {
// 回滚事务
}
}
上面的method1
方法完美的支持了你的业务场景,随着业务场景越来越复杂,你同事也写了类似的method2
,method3
,最后你的BOSS要求你在一个接口里面一次性完成method1
, method2
,method3
的功能,没有办法
你只能
function do () {
try {
$db->begin();
method1();
method2();
method3();
$db->commit();
} catch(Exception $e) {
$db->rollback();
}
}
这里面的业务都是需要原子执行的,必须里面的所有代码全部执行成功才能提交,这时候你懵逼了,因为上面的代码你发现事务根本控制不了了,如果method1
里面的方法执行成功了,但是method2
里面出现了错误,我们期望是会直接进行回滚,但是事实是method1
里面会直接修改掉对应表记录,而不是会回滚,这样就破坏了事务一致性的原则,即使每个method
里面都包含了事务,单个拿出来用也是没有问题的,可是多重嵌套以后就发生了让我们蛋疼的问题,上述这种情况我们称之为事务嵌套,而Mysql是明确不支持事务嵌套的,虽然有savepoint
和 rollback to
,但也不是真正意义上的事务,所以需要我们自己从代码上解决
下面直接提供解决方案,Laravel框架的代码比较简单粗暴,也比较简单易懂
<?php
namespace Support;
use Phalcon\Di;
use Support\Facades\DB;
use Phalcon\Db\AdapterInterface;
/**
* 当前类主要解决事务嵌套所产生的问题,通过门面的Facade DB类来访问当前类内容
*
* Class Database
* @package Support
*/
class Database
{
/**
* 事务计数器
*
* @var int
*/
protected $transactions;
/**
* @var AdapterInterface
*/
protected $db;
/**
* 自动包装事务并执行
*
* @param $callback
* @param int $attempts
* @return mixed
* @throws \Exception
* @author Vencenty <yanchengtian0536@163.com>
* @date 2021/1/29
*/
public function transaction($callback, $attempts = 1)
{
for($currentAttempt = 1; $currentAttempt <= $attempts; $currentAttempt++) {
try {
$this->beginTransaction();
$callbackResult = $callback($this);
$this->commit();
return $callbackResult;
} catch (\Throwable $exception) {
$this->rollback();
throw new \Exception($exception);
}
}
}
/**
* 设置DB类
*
* Database constructor.
*/
public function __construct()
{
$this->db = Di::getDefault()->get('db');
}
/**
* 开启事务
*
* @author Vencenty <yanchengtian0536@163.com>
* @date 2021/1/29
*/
public function beginTransaction()
{
++$this->transactions;
if ($this->transactions == 1) {
$this->db->begin();
}
}
/**
* 回滚
* @author Vencenty <yanchengtian0536@163.com>
* @date 2021/1/29
*/
public function rollback()
{
if ($this->transactions == 1) {
$this->transactions = 0;
$this->db->commit();
} else {
--$this->transactions;
}
}
/**
* 提交事务
*
* @author Vencenty <yanchengtian0536@163.com>
* @date 2021/1/29
*/
public function commit()
{
if ($this->transactions == 1) $this->db->commit();
--$this->transactions;
}
}
上面的方法非常简单,当出现嵌套的时候,我们的Database类会持续的给transactions + 1
,并不会重复开启事务提交,当出现commit
的时候也不会立马提交,而是查看我们之前一共有多少个transactions
,然后一层层的减下去,到最后减到1,说明我们嵌套的事务应该结束了,这时候进行提交,这种情况下无论你嵌套多少层,只要你能确保你的 begin 和 rollback commit是成对出现的,那么事务永远不会出错
可是总会有人粗心漏掉,比如只写了begin
没有写commit
,为了杜绝这种情况发生,请看上面的Database::transaction
方法,这个方法自动执行了事务,只需要将需要执行的方法传递给callback
参数即可,一键实现自动提交,自动回滚,不用担心会少写begin
或者commit
导致出现难以排查的系统出现BUG了
类写好了,调用起来却非常麻烦,每次都要
$db = new Database;
$db->beginTransaction();
$db->commit();
$db->rollback();
非常的残忍,那么再继续抄一下Laravel框架的门面模式,高级叫法:静态代理
其实非常非常简单,我把代码贴出来稍微解释一下
Facade.php
<?php
namespace Support\Facades;
use RuntimeException;
abstract class Facade
{
/**
* 获取门面类根类
*
* @return mixed
*/
public static function getFacadeRoot()
{
return static::getFacadeAccessor();
}
/**
* 不设置门面类不允许使用
*
* @return string
*
* @throws \RuntimeException
*/
protected static function getFacadeAccessor()
{
throw new RuntimeException('Facade does not implement getFacadeAccessor method.');
}
/**
* 静态方法直接访问类
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws RuntimeException
*/
public static function __callStatic(string $method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('目标类未被设置');
}
return $instance->$method(...$args);
}
}
DB.php
<?php
namespace Support\Facades;
use Respect\Validation\Validator;
use Support\Database;
use Closure;
/**
* Database 门面类,通过代理进行访问
* @method static mixed transaction(Closure $callback, int $attempts)
* @method static void beginTransaction()
* @method static void commit()
* @method static void rollback()
*/
class DB extends Facade
{
/**
* 代理类
*
* @return Database
* @author Vencenty <yanchengtian0536@163.com>
* @date 2021/1/29
*/
protected static function getFacadeAccessor(): Database
{
return new Database();
}
}
DB类作为我们的代理类,我们可以直接执行DB::beginTransaction();
如果我们这么执行会发生什么呢,
会直接调用DB类
的父类 Facade
的 __callStatic
方法,
/**
* 静态方法直接访问类
*
* @param string $method
* @param array $args
* @return mixed
*
* @throws RuntimeException
*/
public static function __callStatic(string $method, $args)
{
$instance = static::getFacadeRoot();
if (! $instance) {
throw new RuntimeException('目标类未被设置');
}
return $instance->$method(...$args);
}
而上面的 $instance = static::getFacadeRoot()
方法内部调用的getFacadeRoot
方法,getFacadeRoot
被我们的DB代理类重写掉了,返回了 new Database
,如下图,
到这里我们就明白了,每次我们使用静态方法的时候,__callStatic
方法都会帮助我们自动new
出 Database类,所以,所有在Database里面定义的公共属性的方法,我们都可以通过
DB::{方法名}的方式去调用,简化了new的操作
// 修改之前
$db = new Database;
$db->beginTransaction();
$db->commit();
$db->rollback();
// 修改之后
DB::beginTransaction();
DB::commit();
DB::rollback();
其实大部分代码都是简化版的Laravel的代码,非常的干净又卫生啊,细心的朋友们可能发现Laravel有的们面模式是直接返回了字符串名称,例如下图
那是因为Laravel在调用getFacadeAccessor处理的时候,是通过容器拿到了对应的名称的类,这里面又牵扯到Laravel的DI容器,有兴趣自己去翻代码研究吧~