PHP处理事务嵌套

5 篇文章 0 订阅

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是明确不支持事务嵌套的,虽然有savepointrollback 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容器,有兴趣自己去翻代码研究吧~

  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值