Php 设计/体系结构问题:使用远程服务回滚

Php 设计/体系结构问题:使用远程服务回滚,php,transactions,service,design-patterns,Php,Transactions,Service,Design Patterns,例如,远程API具有以下调用: getGroupCapacity(group) setGroupCapacity(group, quantity) getNumberOfItemsInGroup(group) addItemToGroup(group, item) deleteItemFromGroup(group, item) 任务是将某些项目添加到某个组中。团体有能力。 所以首先我们应该检查组是否已满。如果是,增加容量,然后添加项目。类似这样的内容(例如,API是用SOAP公开的): 现在

例如,远程API具有以下调用:

getGroupCapacity(group)
setGroupCapacity(group, quantity)
getNumberOfItemsInGroup(group)
addItemToGroup(group, item)
deleteItemFromGroup(group, item)
任务是将某些项目添加到某个组中。团体有能力。 所以首先我们应该检查组是否已满。如果是,增加容量,然后添加项目。类似这样的内容(例如,API是用SOAP公开的):

现在,如果addItemToGroup失败(项目已损坏),该怎么办?我们需要回滚组的容量

现在想象一下,您必须向组中添加10个项目,然后使用一些属性设置添加的项目——所有这些都在一个事务中完成。这意味着如果在中间的某个地方失败,你必须把所有的东西都回滚到以前的状态。 如果没有一堆IF和意大利面代码,这可能吗?是否有任何库、框架、模式或体系结构决策可以简化这些操作(在PHP中)

UPD:SOAP只是一个例子。解决方案应该适合任何服务,即使是原始TCP。问题的要点是如何使用底层非事务性API组织事务性行为

UPD2:我想这个问题在所有编程语言中都是一样的。因此,任何答案都是受欢迎的,不仅仅是PHP

提前谢谢

您可以将各个SOAP查询封装在抛出适当异常的类中


更糟糕的解决方案是创建一个异常数组,手动将queryStatus=false或queryStatus=true添加到每个步骤中,然后检查提议的事务是否有效。如果是这样的话,您将调用一个final-commitTransaction方法。

理论上,“WS-DeathStar”协议家族中的一个,也就是精确地处理这个问题。但是,我不知道(虽然我不是PHP开发人员)该标准在PHP中的任何实现。

远程服务通常不支持事务。我不知道PHP,但在BPEL中有一种称为
补偿的东西

补偿或撤消业务流程中已成功完成的步骤是业务流程中最重要的概念之一。补偿的目标是扭转以前作为被放弃的业务流程的一部分而进行的活动的影响


也许你可以试试类似的东西。会有一些if/else。

听起来你需要事务和/或锁定,就像数据库一样。您的客户机代码如下所示:

<?php
//
// Obviously better if the service supports transactions but here's
// one possible solution using the Command pattern.
//
// tl;dr: Wrap all destructive API calls in IApiCommand objects and
// run them via an ApiTransaction instance.  The IApiCommand object
// provides a method to roll the command back.  You needn't wrap the
// non-destructive commands as there's no rolling those back anyway.
//
// There is one major outstanding issue: What do you want to do when
// an API command fails during a rollback? I've marked those areas
// with XXX.
//
// Barely tested but the idea is hopefully useful.
//

class ApiCommandFailedException extends Exception {}
class ApiCommandRollbackFailedException extends Exception {}
class ApiTransactionRollbackFailedException extends Exception {}

interface IApiCommand {
    public function execute();
    public function rollback();
}


// this tracks a history of executed commands and allows rollback    
class ApiTransaction {
    private $commandStack = array();

    public function execute(IApiCommand $command) {
        echo "EXECUTING " . get_class($command) . "\n";
        $result = $command->execute();
        $this->commandStack[] = $command;
        return $result;
    }

    public function rollback() {
        while ($command = array_pop($this->commandStack)) {
            try {
                echo "ROLLING BACK " . get_class($command) . "\n";
                $command->rollback();
            } catch (ApiCommandRollbackFailedException $rfe) {
                throw new ApiTransactionRollbackFailedException();
            }
        }
    }
}


// this groups all the api commands required to do your
// add_item function from the original post.  it demonstrates
// a nested transaction.
class AddItemToGroupTransactionCommand implements IApiCommand {
    private $soap;
    private $group;
    private $item;
    private $transaction;

    public function __construct($soap, $group, $item) {
        $this->soap = $soap;
        $this->group = $group;
        $this->item = $item;
    }

    public function execute() {
        try {
            $this->transaction = new ApiTransaction();
            $this->transaction->execute(new EnsureGroupAvailableSpaceCommand($this->soap, $this->group, 1));
            $this->transaction->execute(new AddItemToGroupCommand($this->soap, $this->group, $this->item));
        } catch (ApiCommandFailedException $ae) {
            throw new ApiCommandFailedException();
        }
    }

    public function rollback() {
        try {
            $this->transaction->rollback();
        } catch (ApiTransactionRollbackFailedException $e) {
            // XXX: determine if it's recoverable and take
            //      appropriate action, e.g. wait and try
            //      again or log the remaining undo stack
            //      for a human to look into it.
            throw new ApiCommandRollbackFailedException();
        }
    }
}


// this wraps the setgroupcapacity api call and
// provides a method for rolling back    
class EnsureGroupAvailableSpaceCommand implements IApiCommand {
    private $soap;
    private $group;
    private $numItems;
    private $previousCapacity;

    public function __construct($soap, $group, $numItems=1) {
        $this->soap = $soap;
        $this->group = $group;
        $this->numItems = $numItems;
    }

    public function execute() {
        try {
            $capacity = $this->soap->getGroupCapacity($this->group);
            $itemsInGroup = $this->soap->getNumberOfItemsInGroup($this->group);
            $availableSpace = $capacity - $itemsInGroup;
            if ($availableSpace < $this->numItems) {
                $newCapacity = $capacity + ($this->numItems - $availableSpace);
                $this->soap->setGroupCapacity($this->group, $newCapacity);
                $this->previousCapacity = $capacity;
            }
        } catch (SoapException $e) {
            throw new ApiCommandFailedException();
        }
    }

    public function rollback() {
        try {
            if (!is_null($this->previousCapacity)) {
                $this->soap->setGroupCapacity($this->group, $this->previousCapacity);
            }
        } catch (SoapException $e) {
            throw new ApiCommandRollbackFailedException();
        }
    }
}

// this wraps the additemtogroup soap api call
// and provides a method to roll the changes back
class AddItemToGroupCommand implements IApiCommand {
    private $soap;
    private $group;
    private $item;
    private $complete = false;

    public function __construct($soap, $group, $item) {
        $this->soap = $soap;
        $this->group = $group;
        $this->item = $item;
    }

    public function execute() {
        try {
            $this->soap->addItemToGroup($this->group, $this->item);
            $this->complete = true;
        } catch (SoapException $e) {
            throw new ApiCommandFailedException();
        }
    }

    public function rollback() {
        try {
            if ($this->complete) {
                $this->soap->removeItemFromGroup($this->group, $this->item);
            }
        } catch (SoapException $e) {
            throw new ApiCommandRollbackFailedException();
        }
    }
}


// a mock of your api
class SoapException extends Exception {}
class MockSoapClient {
    private $items = array();
    private $capacities = array();

    public function addItemToGroup($group, $item) {
        if ($group == "group2" && $item == "item1") throw new SoapException();
        $this->items[$group][] = $item;
    }

    public function removeItemFromGroup($group, $item) {
        foreach ($this->items[$group] as $k => $i) {
            if ($item == $i) {
                unset($this->items[$group][$k]);
            }
        }
    }

    public function setGroupCapacity($group, $capacity) {
        $this->capacities[$group] = $capacity;
    }

    public function getGroupCapacity($group) {
        return $this->capacities[$group];
    }

    public function getNumberOfItemsInGroup($group) {
        return count($this->items[$group]);
    }
}

// nested transaction example
// mock soap client is hardcoded to fail on the third additemtogroup attempt
// to show rollback
try {
    $soap = new MockSoapClient();
    $transaction = new ApiTransaction();
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group1", "item1")); 
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group1", "item2"));
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group2", "item1"));
    $transaction->execute(new AddItemToGroupTransactionCommand($soap, "group2", "item2"));
} catch (ApiCommandFailedException $e) {
    $transaction->rollback();
    // XXX: if the rollback fails, you'll need to figure out
    //      what you want to do depending on the nature of the failure.
    //      e.g. wait and try again, etc.
}
function add_item($group, $item) { $soap = new SoapClient(...); $transaction = $soap->startTransaction(); # or: # $lock = $soap->lockGroup($group, "w"); # strictly to prevent duplication of the rest of the code: # $transaction = $lock; $capacity = $soap->getGroupCapacity($transaction, $group); $itemsInGroup = $soap->getNumberOfItemsInGroup($transaction, $group); if ($itemsInGroup == $capacity) { $soap->setGroupCapacity($transaction, $group, $capacity + 1); } $soap->addItemToGroup($transaction, $group, $item); $transaction->commit(); # or: $lock->release(); } 函数添加项($group,$item){ $soap=新的SoapClient(…); $transaction=$soap->startTransaction(); #或: #$lock=$soap->lockGroup($group,“w”); #严格防止代码其余部分重复: #$transaction=$lock; $capacity=$soap->getGroupCapacity($transaction,$group); $itemsInGroup=$soap->getNumberOfItemsInGroup($transaction,$group); 如果($itemsInGroup==$capacity){ $soap->setGroupCapacity($transaction,$group,$capacity+1); } $soap->addItemToGroup($transaction,$group,$item); $transaction->commit(); #或者:$lock->release(); }
当然,您需要处理行为不端的客户机,例如那些在提交/释放之前崩溃的客户机,或者那些锁定过多的客户机,导致其他客户机不必要地失败。这在不活动、最大超时以及每个客户端的最大锁数的情况下是可能的。

将事务逻辑放在远程端。setGroupCapacity()应封装在addItemToGroup()中。这是内部状态,调用者不应该为此烦恼。使用此选项,您可以逐个添加项,并使用deleteItemFromGroup()轻松地解除该项


如果您必须使用低级API,那么回滚依赖于您跟踪您的操作流。

Gregor Hohpe对远程处理错误的各种方法做了一个很好的总结:

简言之:

  • 注销:不做任何事,或放弃已完成的工作
  • 重试:重试失败的部件。如果您将服务设计为幂等的,那么就更容易了,这样就可以使用相同的输入重复运行服务,而不会产生不良影响
  • 补偿操作:为服务提供补偿操作,使您可以撤消目前为止的工作
  • 事务协调器:传统的两阶段提交。理论上是理想的,在实践中很难实现,有很多有缺陷的中间件
但是,在您的情况下,可能是远程API的粒度太细。您真的需要将
setGroupCapacity
作为一项单独的服务吗?只提供
addUserToGroup
,让服务在内部处理任何必要的容量增加,怎么样?这样,整个事务可以包含在单个服务调用中


您当前的API也会因并发问题和竞争条件而打开。如果在调用
getNumberOfItemsInGroup
setGroupCapacity
之间,其他线程设法添加了一个用户,该怎么办?您的请求将失败,因为另一个线程“偷走”了您的容量增加。

很抱歉,无法理解这会有什么帮助。我应该在哪里捕获这些异常并放置回滚代码?例如,也许?:-)感谢远程服务不支持它。SOAP只是一个例子,我需要更通用的解决方案。基于WS-*堆栈,BPEL看起来很庞大。我不确定,有什么服务可以配合吗?我建议你采用“补偿”的概念,并实施类似的措施。好的,谢谢。稍后我将查找更多详细信息。从reading()中可以看出,这个概念只是保存状态,并在流程的每个步骤上都有try/except块。这在没有BPEL的情况下是非常清楚的,但问题也在于如何在不编写太多代码的情况下实现智能化。如果出现问题/异常,您需要将其恢复到早期状态。您可以尝试将初始状态存储在某个对象(比如bkp)中,如果有 function add_item($group, $item) { $soap = new SoapClient(...); $transaction = $soap->startTransaction(); # or: # $lock = $soap->lockGroup($group, "w"); # strictly to prevent duplication of the rest of the code: # $transaction = $lock; $capacity = $soap->getGroupCapacity($transaction, $group); $itemsInGroup = $soap->getNumberOfItemsInGroup($transaction, $group); if ($itemsInGroup == $capacity) { $soap->setGroupCapacity($transaction, $group, $capacity + 1); } $soap->addItemToGroup($transaction, $group, $item); $transaction->commit(); # or: $lock->release(); }