【问题标题】:Total rollback on nested transactions嵌套事务的总回滚
【发布时间】:2013-06-12 09:53:20
【问题描述】:

我真的很喜欢这个适用于 Yii 的 NestedPDO 解决方案,但我需要一些不同的事务处理。

只有在所有嵌套事务都可以提交并且如果一个事务执行回滚所有事务都应该回滚时,我才想提交我的嵌套事务。

我该怎么做?

我尝试更改不起作用的回滚功能:

public function rollBack() {
    $this->transLevel--;

    if($this->transLevel == 0 || !$this->nestable()) {
        parent::rollBack();
    } else {

        $level = $this->transLevel;
        for($level; $level>1; $level--){
            $this->exec("ROLLBACK TO SAVEPOINT LEVEL{$this->constantlevel}");
        }
        //parent::rollBack();
    }
}

我正在考虑调整 NestedPDO:在函数 commit() 中仅在最外层事务上执行提交,在函数 rollBack() 中回滚到最外层事务,无论哪个子事务导致回滚。但我无法完成它......

我正在使用 MySQL 和 InnoDB 表,我不确定自动提交,但是当在事务中回显自动提交的值时,我总是得到值 1,这应该意味着自动提交已打开,但在事务中自动提交应该设置为0. 我不确定这是否是整个回滚对我不起作用的原因?

【问题讨论】:

  • 不,自动提交应该在事务中默认切换为 0。如果您能够回滚一步,那么您应该能够回滚所有内容。此外,如果commit()rollback() 都将提交和回滚最外面的事务(正如您在最后一段之前所说的那样),那么您将回到常规的非嵌套事务的第一方。还是我错过了什么?请发布您的无效代码。
  • 啊,好吧,我不知道事务中自动提交的行为。那么你会如何建议这样做或者我做错了什么?我将最初帖子中的示例更改为我在应用程序中使用的实际代码。
  • 取出内部事务,然后它会按照你的意愿工作 - 如果一个步骤失败,它会全部回滚。如果在其他情况下有时需要内部事务,可以将其设为方法参数$useTransactions,默认为 true。
  • 感谢@halfer,但我想避免这种方式,因为这将是一个很大的努力。我想全局更改多个事务的行为以仅在它是最外层事务时才进行提交,否则就忽略它。不知道哪个事务级别正在回滚,一切都应该回滚。所以如果有一个level0事务和一个level1事务:当level1事务提交时忽略它,当level1事务回滚时回滚level1和level0。
  • @sandro1111 在您发布的代码中,我没有看到尝试实现您寻求的行为。也许相关代码在NestedPDO 类中?

标签: mysql yii nested-transactions


【解决方案1】:

如果您希望在发生错误时立即自动回滚整个事务,您可以在从某些特定位置(例如,从 A())调用时重新从 B 的异常处理程序中抛出异常:罢工>

function A(){
   ...
   $this->B(true);
   ...
}

/*
* @param B boolean Throw an exception if the transaction is rolled back
*/
function B($rethrow) {
    $transaction=Yii::app()->db->beginTransaction();
    try {
        //do something
        $transaction->commit();
    } catch(Exception $e) {
        $transaction->rollBack();
        if ($rethrow) throw $e;
    }
}

现在我了解到您实际上只是希望您的包装器检测事务是否已经在进行中,并且在这种情况下不启动事务。

因此,您实际上并不需要 NestedPDO 类。你可以创建一个这样的类:

class SingleTransactionManager extends PDO {
    private $nestingDepth = 0;

    public function beginTransaction() {
        if(!$this->nestingDepth++ == 0) {
            parent::beginTransaction();
        } // else do nothing
    }
    public function commit() {
        $this->nestingDepth--;
        if (--$this->nestingDepth == 0) {
            parent::commit();
        } // else do nothing
    }

    public function rollback() {
        parent::rollback();
        if (--$this->nestingDepth > 0) {
            $this->nestingDepth = 0;
            throw new Exception(); // so as to interrupt outer the transaction ASAP, which has become pointless
        }

    }
}

【讨论】:

  • 感谢@YaK,但我想避免这种情况,因为为整个应用程序做这件事需要付出很大的努力。我真的更喜欢可以在 NestedPDO 类中实现的一些解决方案来全局更改行为,因为应该将整个应用程序更改为该行为。
  • 对不起,但这不起作用。我仍然需要处理回滚,因为只有在事务处于活动状态并且我仍然不知道如何处理提交时才能进行回滚,因为如果子调用函数调用提交但另一个子函数调用回滚,那么事务是已经关闭。所以我想我需要一些全局计数器来计算 beginTransactions 的调用次数和提交的计数。
  • @sandro1111 我明白了。这个呢?
  • 回滚不起作用,因为该函数被多次调用,但事务仅在第一次调用时存在。我更改了您的回滚功能以进行检查。但是在使用这个 SingleTransactionManager 时,数据库中仍然会创建记录......这可能是什么原因?
  • 您知道这还有什么问题吗,因为在使用这种事务管理器并进行回滚时,数据库中仍有记录?
【解决方案2】:

根据@RandomSeed 的回答,我为默认 Yii 事务处理创建了一个“插入”:

$connection = Yii::app()->db;
$transaction=$connection->beginTransaction();
try
{
   $connection->createCommand($sql1)->execute();
   $connection->createCommand($sql2)->execute();
   //.... other SQL executions
   $transaction->commit();
}
catch(Exception $e)
{
   $transaction->rollback();
}

这是我的 SingleTransactionManager 类:

class SingleTransactionManager extends CComponent 
{
    // The current transaction level.
    private $transLevel = 0;

    // The CDbConnection object that should be wrapped
    public $dbConnection;

    public function init()
    {
        if($this->dbConnection===null)
            throw new Exception('Property `dbConnection` must be set.');

        $this->dbConnection=$this->evaluateExpression($this->dbConnection);
    }
    // We only start a transaction if we're the first doing so
    public function beginTransaction() {
        if($this->transLevel == 0) {
            $transaction = parent::beginTransaction();
        } else {
            $transaction = new SingleTransactionManager_Transaction($this->dbConnection, false);
        }
        // always increase transaction level:
        $this->transLevel++;

        return $transaction;
    }

    public function __call($name, $parameters)
    {
        return call_user_func_array(array($this->dbConnection, $name), $parameters);
    }
}

class SingleTransactionManager_Transaction extends CDbTransaction
{
    // boolean, whether this instance 'really' started the transaction
    private $_startedTransaction;

    public function __construct(CDbConnection $connection, $startedTransaction = false)
    {
        $this->_startedTransaction = $startedTransaction;
        parent::__construct($connection);
        $this->setActive($startedTransaction);
    }

    // We only commit a transaction if we've started the transaction
    public function commit() {
        if($this->_startedTransaction)
            parent::commit();
    }

    // We only rollback a transaction if we've started the transaction
    // else throw an Exception to revert parent transactions/take adquate action
    public function rollback() {
        if($this->_startedTransaction)
            parent::rollback();
        else
            throw new Exception('Child transaction rolled back!');
    }
}

这个类“包装”了主数据库连接,你应该在你的配置中像这样声明它:

'components'=>array(

    // database
    'db'=>array(
        'class' => 'CDbConnection',
        // using mysql
        'connectionString'=>'....',
        'username'=>'...',
        'password'=>'....',
    ),

    // database
    'singleTransaction'=>array(
        'class' => 'pathToComponents.db.SingleTransactionManager',
        'dbConnection' => 'Yii::app()->db'
    )

请注意,dbConnection 属性应该是主数据库连接的表达式。 现在,当在嵌套的 try catch 块中嵌套事务时,您可以在嵌套事务 3 中创建错误,并且 1 和 2 上的那些也会回滚。

测试代码:

$connection = Yii::app()->singleTransaction;

$connection->createCommand('CREATE TABLE IF NOT EXISTS `test_transactions` (
  `number` int(10) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;')->execute();

$connection->createCommand('TRUNCATE TABLE `test_transactions`;')->execute();

testNesting(4, 3, 1);

echo '<br>';
echo 'Rows:';
echo '<br>';
$rows = $connection->createCommand('SELECT * FROM `test_transactions`')->queryAll();
if($rows)
{
    foreach($rows as $row)
    {
        print_r($row);
    }
}
else
    echo 'Table is empty!';

function testNesting(int $total, int $createErrorIn = null, int $current = 1)
{
    if($current>=$total)
        return;

    $connection = Yii::app()->singleTransaction;
    $indent = str_repeat('&nbsp;', ($current*4));

    echo $indent.'Transaction '.$current;
    echo '<br>';
    $transaction=$connection->beginTransaction();
    try
    {
        // create nonexisting columnname when we need to create an error in this nested transaction
        $columnname = 'number'.($createErrorIn===$current ? 'rr' : '');
        $connection->createCommand('INSERT INTO `test_transactions` (`'.$columnname.'`) VALUES ('.$current.')')->execute();

        testNesting($total, $createErrorIn, ($current+1));

        $transaction->commit();
    }
    catch(Exception $e)
    {
        echo $indent.'Exception';
        echo '<br>';
        echo $indent.$e->getMessage();
        echo '<br>';
        $transaction->rollback();
    }
}

结果如下:

    Transaction 1
        Transaction 2
            Transaction 3
            Exception
            CDbCommand failed to execute the SQL statement: SQLSTATE[42S22]: Column not found: 1054 Unknown column 'numberrr' in 'field list'. The SQL statement executed was: INSERT INTO `test_transactions` (`numberrr`) VALUES (3)
        Exception
        Child transaction rolled back!
    Exception
    Child transaction rolled back!

Rows:
Table is empty! 

【讨论】:

    【解决方案3】:

    恕我直言,在应用程序代码中模拟“嵌套事务”的想法是一种反模式。应用程序中有许多无法解决的异常情况(请参阅我对https://stackoverflow.com/a/319939/20860 的回答)。

    在 PHP 中,最好保持简单。工作自然地组织成请求,所以使用请求作为事务范围。

    • 在调用任何模型类之前在控制器级别启动事务。
    • 如果出现任何问题,让模型抛出异常。
    • 在控制器级别捕获异常,并在必要时回滚。
    • 如果没有捕获到异常,则提交。

    忘记所有关于事务级别的废话。模型不应启动、提交或回滚任何事务。

    【讨论】:

    • 有时可以直接调用包含事务的函数,但也可以是嵌套函数调用的一部分,每个函数调用都带有或不带有事务。在后者(嵌套函数调用)中,您需要最干净的事务管理解决方案,不知道哪个请求来自哪里或正在处理哪个用例。如果这些功能在模型、控制器或组件中定义并不重要。我认为处理这些用例的中心类可能会非常有帮助,当然这取决于应用程序逻辑!
    • 但是中心类导致错误的事务处理。请参阅我链接到的答案。 all 函数最好避免启动自己的事务,而是依赖调用者来启动和提交事务。
    • 恕我直言:场景 #1 是设计问题,场景 #2,如果这是真的,那么对于未嵌套的事务也是如此,如果代码错误,场景 #3 也是正确的,就像写作一样许多结束标签“}”或“)”我认为“嵌套事务”至少有两个用例:它们应该独立运行,或者全部“作为一个”。但是在这两种情况下(非常重要的要求),如果函数被直接调用或在另一个具有事务的函数中调用,代码不应改变。
    • 第 2 部分 :) 我在此页面上写的答案有助于管理“作为一个事务的事务”(单个事务?),而应用程序逻辑不会被更改。在这种情况下,事务处理由调用者处理。后续调用将被忽略。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-04-16
    • 1970-01-01
    • 1970-01-01
    • 2020-04-11
    • 1970-01-01
    • 2012-09-14
    相关资源
    最近更新 更多