代碼實現
實現
一個完整的業務活動由一個主業務服務與若干從業務服務組成
主業務服務負責發起並完成整個業務活動
從業務服務提供TCC型業務操作
業務活動管理器控制業務活動的一致性,它登記業務活動中的操作, 並在
業務活動提交時確認所有的TCC型操作的confirm操作,在業務活動取消
時調用所有TCC型操作的cancel操作
首先在rpc控制器中給業務服務加tcc標記,
例如主服務調用orderSuccess方法,就會執行主服務Order服務端的creditOrderTcc方法和從服務payAccount端的creditAccountTcc方法,如果其中一個執行不成功,就會回滾,或者執行補償方法
class RpcController
{
/**
* @Reference(pool="order.pool",fallback="OrderFallback",type="tcc")
*
* @var OrderInterface
*/
private $orderService;
/**
* @Reference(pool="payAccount.pool",fallback="PayAccountFallback",type="tcc")
*
* @var PayAccountInterface
*/
private $payAccountService;
/**
* @RequestMapping("success")
* @Compensable(
* master={"services"=OrderInterface::class,"tryMethod"="creditOrderTcc","confirmMethod"="confirmCreditOrderTcc","cancelMethod"="cancelCreditOrderTcc"},
* slave ={
* {"services"=PayAccountInterface::class,"tryMethod"="creditAccountTcc","confirmMethod"="confirmCreditAccountTcc","cancelMethod"="cancelCreditAccountTcc"}
* }
* )
*
* @return array
*/
public function orderSuccess()
{
$context='';
$this->orderService->creditOrderTcc($context); //主服務觸發TCC型調用,從服務只需要定義不需要觸發
//var_dump($this->orderService1->creditOrderTcc($context));
//$this->payAccountService->creditAccountTcc($context); //商戶餘額增加
}
/**
* @RequestMapping("error")
* @Compensable(
* master={"services"=OrderInterface::class,"tryMethod"="creditOrderTcc","confirmMethod"="confirmCreditOrderTcc","cancelMethod"="cancelCreditOrderTcc"},
* slave ={
* {"services"=PayAccountInterface::class,"tryMethod"="creditAccountTcc","confirmMethod"="confirmCreditAccountTcc","cancelMethod"="cancelCreditAccountTcc"},
*
* }
* )
* @return array
*/
public function orderError()
{
$context='';
$this->orderService1->creditOrderTcc($context); //主服務觸發TCC型調用,從服務只需要定義不需要觸發
//var_dump($this->orderService1->creditOrderTcc($context));
//$this->payAccountService->creditAccountTcc($context); //商戶餘額增加
}
}
主服務和從服務都分別有這三個方法,以下是主服務
class OrderService implements OrderInterface
{
public function creditOrderTcc($context):array
{
var_dump("一階段try成功");
return ['status' => 1, 'result' => '一階段try成功'];
}
/**
* 確認並且投遞參數
* @return array
*/
public function confirmCreditOrderTcc($context): array
{
var_dump("二階段confirm成功");
return ['status' => 1, 'result' => '二階段confirm成功'];
}
public function cancelCreditOrderTcc($context): array
{
var_dump("二階段cancel成功");
return ['status' => 1, 'result' => '二階段cancel成功'];
}
}
從服務
class PayAccountService implements PayAccountInterface
{
public function creditAccountTcc($context):array
{
var_dump("一階段try成功");
return ['status' => 1, 'result' => '一階段try成功'];
}
/**
* 確認並且投遞參數
* @return array
*/
public function confirmCreditAccountTcc($context): array
{
var_dump("二階段confirm成功");
return ['status' => 1, 'result' => '二階段confirm成功'];
}
public function cancelCreditAccountTcc($context): array
{
var_dump("二階段cancel成功");
return ['status' => 1, 'result' => '二階段cancel成功'];
}
}
聲明tcc服務的註解
class Compensable
{
protected $tccService;
/**
* Reference constructor.
*
* @param array $values
*/
public function __construct(array $values)
{
//驗證
if (!empty($values)) {
$this->tccService = $values;
}
}
/**
* @return string
*/
public function getTccService(): array
{
return $this->tccService;
}
}
註冊到bean容器中
class CompensableParser extends Parser
{
/**
* @param int $type
* @param Reference $annotationObject
*
* @return array
* @throws RpcClientException
* @throws AnnotationException
* @throws ReflectionException
* @throws ProxyException
*/
public function parse(int $type, $annotationObject): array
{
$service=$annotationObject->getTccService();
RouteRegister::register($service);
//返回當前類名註冊到框架的bean容器當中
return [$this->className,$this->className,Bean::SINGLETON,''];
}
}
在調用時得區分TCC型rpc和普通rpc,
先找出有tcc標記的服務,找出主服務,判斷是不是第一階段的try,如果不是,調用tcc類的tcc方法請求,如果是第一次請求,組裝tcc請求的相關數據,把數據存入redis,然後調用tcc類的tcc方法,發送請求,如果不是就調用普通的rpc請求即可
trait ServiceTrait
{
protected function __proxyCall(string $interfaceClass, string $methodName, array $params)
{
//區分TCC型rpc的跟普通RPC調用
$services = \Six\Tcc\RouteRegister::getRoute(__CLASS__);
if (!empty($services)) {
//如果是從異常tcc事務處理髮起的請求是不需要生成數據的
$tcc_method=array_search($methodName,$services['master']);
if($tcc_method!='tryMethod'){
//只要傳tid
$res = Tcc::Tcc($services, $interfaceClass,$tcc_method, $params,$params[0]['tid'],$this);
}elseif($tcc_method=='tryMethod'){
$tid= session_create_id(md5(microtime()));
$tccData = [
'tid' => $tid,//事務id
'services' => $services, //參與者信息
'content' => $params, //傳遞的參數
'status' => 'normal', //(normal,abnormal,success,fail)事務整體狀態
'tcc_method' => 'tryMethod', //try,confirm,cancel (當前是哪個階段)
'retried_cancel_count' => 0, //重試次數
'retried_confirm_count' => 0, //重試次數
'retried_max_count' =>1, //最大允許重試次數
'create_time' => time(), //創建時間
'last_update_time' => time() //最後的更新時間
];
//記錄tcc活動日誌
$redis=new \Co\Redis();
$redis->connect('127.0.0.1',6379);
$redis->hSet("Tcc",$tid,json_encode($tccData));
//整體的併發請求,等待一組協程的結果,發起第二階段的請求
$res = Tcc::Tcc($services, $interfaceClass, 'tryMethod', $params,$tid,$this);
}
var_dump($res);
} else {
return $this->send(__CLASS__, $interfaceClass, $methodName, $params);
}
return $res;
}
}
現在來看看上面的$services = \Six\Tcc\RouteRegister::getRoute(CLASS);是怎麼獲取tcc服務的
public static function getRoute(string $interClass): array
{
foreach (self::$services as $s) {
if ($s['master']['services'] == $interClass) return $s;
}
}
public static function register($service): void
{
$interfaceInfo = ReferenceRegister::getAllTcc();
//var_dump("--------------------------",$interfaceInfo,"--------------------------");
//找到所有的,註冊類型爲TCC的接口服務,然後替換一下interface的名稱,
foreach ($interfaceInfo as $k => $v) {
$k_prefix = explode("_", $k)[0];
//循環Tcc路由,如果接口服務已經存在Tcc路由當中那麼就跳過
foreach (self::$services as $tccServices) {
if($tccServices['master']['services']==$k){
continue 2;
}
//過濾處理服務,避免產生重複
foreach ($tccServices['slave'] as $slave_k=>$slave){
if($slave['services']==$k){
// var_dump($k);
continue 3;
}
}
}
//替換主服務
if($k_prefix==$service['master']['services']){
$service['master']['services']=$k;
}
//替換從服務
foreach ($service['slave'] as $slave_k => $slave_service) {
if ($k_prefix == $slave_service['services']) {
$service['slave'][$slave_k]['services']=$k;
}
}
}
self::$services[] = $service;
var_dump("--------------------------",self::$services,"-------------------------");
}
上面一旦執行register方法就會得到tcc所有的服務,register方法是在解析註解時調用,CompensableParser類裏面的parse方法
public function parse(int $type, $annotationObject): array
{
$service=$annotationObject->getTccService();
RouteRegister::register($service);
//返回當前類名註冊到框架的bean容器當中
return [$this->className,$this->className,Bean::SINGLETON,''];
}
getAllTcc方法
public static function getAllTcc()
{
$references=[];
foreach (self::$references as $k=>$v){
if($v['type'] == 'tcc'){
$references[$k]=$v;
}
}
return $references;
}
Tcc類的tcc方法,
一、首先設置一個標記flag爲0,如果是comfirm階段則設置flag爲1,記錄當前的事務的階段,
二、WaitGroup整體的併發請求,等待一組協程的結果,發起第二階段的請求,
三、主服務發起第一階段請求,然後從服務發起第一階段請求,
四、等待第一階段結果,如果status爲空或者爲0則拋異常,
五、如果當前沒有問題,記錄當前事務狀態,
六、如果走到這一步方法名爲try階段沒有異常,如果flag爲0,遞歸調用自身,但是方法名改爲confirm階段,最後返回$res;
七、如果這中間拋異常,異常裏面有Tcc或者Rpc的
----1.如果在try階段的回滾,直接調用cancel回滾
----2.在(cancel)階段的時候出現了異常,會重試有限次數,重複調用cancel,超過最大次數,設置fail狀態,拋出異常
----3.在(confirm)階段的時候,會重試有限次數,重複調用confirm,超過最大次數,調用cancel,補償機制跟try階段不一樣
public static function Tcc($services, $interfaceClass, $tcc_methodName, $params, $tid, $obj)
{
try {
$flag = 0;
if ($tcc_methodName == 'confirmMethod') {
$flag = 1;
}
//記錄當前事務處於哪個階段
$data['tcc_method'] = $tcc_methodName;
$data['status'] = 'normal';
self::tccStatus($tid,3,$tcc_methodName,$data);
//整體的併發請求,等待一組協程的結果,發起第二階段的請求
$wait = new WaitGroup(count($services['slave']) + 1);
//sgo(function ()use($wait,$services,$interfaceClass,$params,$obj,$tcc_methodName){
//主服務發起第一階段的請求
$res = $obj->send($services['master']['services'], $interfaceClass, $services['master'][$tcc_methodName], $params);
$res['interfaceClass'] = $interfaceClass;
$res['method'] = $tcc_methodName;
//當結果正常,修改主服務的狀態
// if (!empty($res['status']) || $res['status'] == 1) {
// $data['services']['tcc_method'] = $tcc_methodName;
// $data['services']['status'] = 'success';
// self::tccStatus($tid, 3,json_encode($data));
// }
$wait->push($res);
// });
//從服務發起第一階段的請求
foreach ($services['slave'] as $k => $slave) {
//sgo(function ()use($wait,$slave,$params,$obj,$tcc_methodName){
$slaveInterfaceClass = explode("_", $slave['services'])[0];
$slaveRes = $obj->send($slave['services'], $slaveInterfaceClass, $slave[$tcc_methodName], $params); //默認情況下從服務沒有辦法發起請求
$slaveRes['interfaceClass'] = $slaveInterfaceClass;
$slaveRes['method'] = $tcc_methodName;
// //當結果正常,修改從服務的狀態
// if (!empty($slaveRes['status']) || $slaveRes['status'] == 1) {
// $data['services']['slave'][$k]['tcc_method'] = $tcc_methodName;
// $data['services']['slave'][$k]['status'] = 'success';
// }
// self::tccStatus($tid, 3,json_encode($data));
$wait->push($slaveRes);
// });
}
//等待一階段調用結果
$res = $wait->wait(); //阻塞
foreach ($res as $v) {
if (empty($v['status']) || $v['status'] == 0) {
throw new \Exception("Tcc error!:" . $tcc_methodName);
return;
}
}
//假設當前操作沒有任何問題
$data['tcc_method'] = $tcc_methodName;
$data['status'] = 'success';
self::tccStatus($tid, 3,$tcc_methodName,$data); //整體服務的狀態
//只有在當前的方法爲try時才提交
if ($tcc_methodName == 'tryMethod') {
//第二階段提交
if ($flag == 0) {
return self::Tcc($services, $interfaceClass, 'confirmMethod', $params, $tid, $obj);
}
}
return $res;
} catch (\Exception $e) {
$message = $e->getMessage();
echo 'Tcc message:'.$e->getMessage().' line:'.$e->getFile().' file:'.$e->getFile().PHP_EOL;
//無論是哪個服務拋出異常,回滾所有的服務
if (stristr($message, "Tcc error!") || stristr($message, "Rpc CircuitBreak")) {
//結果異常,跟調用異常的標準不同(也是因爲swoft框架做了一次重試操作)
//回滾時記錄當前的回滾次數,下面這段僅僅只是結果出現異常的回滾
//調用出現異常(調用超時,網絡出現問題,調用失敗)
//在try階段的回滾,直接調用cancel回滾
//在(cancel)階段的時候出現了異常,會重試有限次數,重複調用cancel,超過最大次數,設置fail狀態,拋出異常
//在(confirm)階段的時候,會重試有限次數,重複調用confirm,超過最大次數,調用cancel,補償機制跟try階段不一樣
if ($tcc_methodName == 'tryMethod') {
return self::Tcc($services, $interfaceClass, 'cancelMethod', $params, $tid, $obj);
} elseif ($tcc_methodName == 'cancelMethod') {
if (self::tccStatus($tid, 1, $tcc_methodName)) {
return self::Tcc($services, $interfaceClass, 'cancelMethod', $params, $tid, $obj);
}
return ["回滾異常"];
} elseif ($tcc_methodName == 'confirmMethod') {
if (self::tccStatus($tid,2, $tcc_methodName)) {
return self::Tcc($services, $interfaceClass, $tcc_methodName, $params, $tid, $obj);
}
//意味當前已經有方法提交了,有可能執行了業務了,我們需要額外的補償
$params[0]['cancel_confirm_flag']=1;
return self::Tcc($services, $interfaceClass, 'cancelMethod', $params, $tid, $obj);
}
}
}
}
上面設置事務狀態的函數TccStatus,先根據tid從redis中找到事務的相關信息
一、如果flag爲1,表示回滾出現的異常,如果嘗試取消次數超過最大次數,則狀態改爲fail,根據tid把事務數據存入redis中,並返回false,如果沒有超過最大次數,則嘗試次數+1,狀態改爲abnormal,存入redis,返回true;
二、如果flag爲2,表示confirm階段出現的異常,如果確認嘗試次數超過最大次數,狀態改爲fail,存入redis並返回false,如果沒有超過最大次數,則嘗試次數+1,狀態改爲abnormal,存入redis,返回true
三、如果flag爲3,修改當前事務的階段,改事務方法,狀態和時間,存入redis中
public static function tccStatus($tid, $flag = 1, $tcc_method = '', $data = [])
{
$redis = new \Co\Redis();
$redis->connect('127.0.0.1', 6379);
$originalData = $redis->hget("Tcc", $tid);
$originalData = json_decode($originalData, true);
//(回滾處理)修改回滾次數,並且記錄當前是哪個階段出現了異常
if ($flag == 1) {
//判斷當前事務重試的次數爲幾次,如果重試次數超過最大次數,則取消重試
if ($originalData['retried_cancel_count'] >= $originalData['retried_max_count']) {
$originalData['status'] = 'fail';
$redis->hSet('Tcc', $tid, json_encode($originalData));
return false;
}
$originalData['retried_cancel_count']++;
$originalData['tcc_method'] = $tcc_method;
$originalData['status'] = 'abnormal';
$originalData['last_update_time']=time();
$redis->hSet('Tcc', $tid, json_encode($originalData));
return true;
}
//(confirm處理)修改嘗試次數,並且記錄當前是哪個階段出現了異常
if ($flag == 2) {
//判斷當前事務重試的次數爲幾次,如果重試次數超過最大次數,則取消重試
if ($originalData['retried_confirm_count'] >=1) {
$originalData['status'] = 'fail';
$redis->hSet('Tcc', $tid, json_encode($originalData));
return false;
}
$originalData['retried_confirm_count']++;
$originalData['tcc_method'] = $tcc_method;
$originalData['status'] = 'abnormal';
$originalData['last_update_time']=time();
$redis->hSet('Tcc', $tid, json_encode($originalData));
return true;
}
//修改當前事務的階段
if ($flag == 3) {
$originalData['tcc_method']=$data['tcc_method'];
$originalData['status']=$data['status'];
$originalData['last_update_time']=time();
$redis->hSet('Tcc', $tid, json_encode($originalData)); //主服務狀態
}
}
然後定義一個監聽事件,設置一個定時器,
查詢狀態正常持久化到日誌組件,狀態不正常的,提交到一半就結束(1.只完成了一個階段的 2.只完成了某個服務)
找到那些超時的事務
一、如果狀態是success並且方法不是confirm階段的
----1.如果是在try階段,直接調用回滾
----2.如果是在cancel階段,判斷嘗試次數,如果沒有超過最大cancel次數,執行cancelCreditOrderTcc回滾操作,這時候可以回滾,如果超過最大嘗試次數,發郵件或者記錄到日誌中,然後從redis中刪除這個事務,以免重複執行
----3.如果是在confirm階段,判斷下當前的嘗試次數,如果嘗試次數不超過一次,則改變事務狀態爲cancel階段,讓它執行回滾;如果嘗試次數超過一次了,意味當前已經有方法提交了,有可能執行了業務了,我們需要額外的補償
二、如果事務狀態爲success並且方法爲cancel,表示刪除成功,從redis中把這個事務刪掉並且持久化到日誌中
三、如果事務狀態爲success並且方法爲confirm,表示事務confirm階段也執行成功,把這個事務從redis中刪除並持久化到日誌中
public function handle($event):void
{
swoole_timer_tick(2000,function (){
//查詢狀態正常持久化到日誌組件,狀態不正常的,提交到一半就結束(1.只完成了一個階段的 2.只完成了某個服務)
$timeOut=5;
sgo(function()use($timeOut){
try{
//自動初始化一個Context上下文對象(協程環境下)
$context = ServiceContext::new();
\Swoft\Context\Context::set($context);
$data=$this->connection->hGetAll('Tcc');
foreach ($data as $k=>$v){
$v=json_decode($v,true);
//跳過尚未超時正在執行的任務
if($v['last_update_time']+$timeOut > time()){
continue;
}
//表示當前服務異常了
if($v['status']!='success' && $v['tcc_method']!='confirmMethod'){
//在try階段的回滾,直接調用cancel回滾
//在(confirm)階段的時候,會重試有限次數,重複調用confirm,超過最大次數,調用cancel,補償機制跟try階段不一樣
//在(cancel)階段的時候出現了異常,會重試有限次數,重複調用cancel,超過最大次數,設置fail狀態,拋出異常
if ($v['tcc_method'] == 'tryMethod') {
//直接調用回滾
var_dump("tryMethod回滾");
$v['tcc_method']='cancelMethod';
$res=$this->orderService->cancelCreditOrderTcc($v);
var_dump($res);
} elseif ($v['tcc_method'] == 'cancelMethod') {
//判斷在異常事務處理當中的嘗試次數
if (self::tccStatus($v['tid'], 1, 'cancelMethod')) {
$this->orderService->cancelCreditOrderTcc($v);
}else{
//發郵件,報警
var_dump("cancel異常刪除");
$this->connection->hDel('Tcc',$k);
}
} elseif ($v['tcc_method'] == 'confirmMethod') {
var_dump("confirmMethod回滾或者提交");
//也要去判斷下當前的嘗試次數
if (self::tccStatus($v['tid'],2, 'cancelMethod')) {
//$this->orderService->confirmCreditOrderTcc($v);
}
//意味當前已經有方法提交了,有可能執行了業務了,我們需要額外的補償
$params[0]['cancel_confirm_flag']=1;
}
}elseif($v['status']=='success' && $v['tcc_method']=='cancelMethod'){
//redis當中刪除並且持久化到日誌組件當中
var_dump("cancel成功正常刪除");
$this->connection->hDel('Tcc',$k);
}elseif ($v['status']=='success' && $v['tcc_method']=='confirmMethod'){
//redis當中刪除並且持久化到日誌組件當中
var_dump("confirm成功正常刪除");
$this->connection->hDel('Tcc',$k);
}
}
}catch (\Exception $e){
echo 'tick message:'.$e->getMessage().' line:'.$e->getFile().' file:'.$e->getFile().PHP_EOL;
}
});
});
}