Qwe*_*rty 5 php service design-patterns transactions
例如,有以下调用的远程API:
getGroupCapacity(group)
setGroupCapacity(group, quantity)
getNumberOfItemsInGroup(group)
addItemToGroup(group, item)
deleteItemFromGroup(group, item)
Run Code Online (Sandbox Code Playgroud)
任务是向某个组添加一些项目.团体有能力.首先,我们应该检查组是否已满.如果是,请增加容量,然后添加项目.像这样的东西(例如API用SOAP公开):
function add_item($group, $item) {
$soap = new SoapClient(...);
$capacity = $soap->getGroupCapacity($group);
$itemsInGroup = $soap->getNumberOfItemsInGroup($group);
if ($itemsInGroup == $capacity) {
$soap->setGroupCapacity($group, $capacity + 1);
}
$soap->addItemToGroup($group, $item);
}
Run Code Online (Sandbox Code Playgroud)
现在如果addItemToGroup失败(项目不好)怎么办?我们需要回滚集团的能力.
现在想象一下,您必须添加10个项目进行分组,然后设置添加了一些属性的项目 - 所有这些都在一个事务中.这意味着如果它在中间某处失败,你必须将所有内容回滚到之前的状态.
没有一堆IF和意大利面条代码可能吗?任何将简化此类操作的库,框架,模式或体系结构决策(在PHP中)?
UPD: SOAP就是一个例子.解决方案应该适合任何服务,甚至是原始TCP.问题的关键是如何使用基础非事务API组织事务行为.
UPD2:我想这个问题在所有编程语言中都是一样的.所以任何答案都受到欢迎,不仅仅是PHP.
提前致谢!
<?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.
}
Run Code Online (Sandbox Code Playgroud)