13.微服務-分佈式事務TCC型(二)

實現

一個完整的業務活動由一個主業務服務與若干從業務服務組成
主業務服務負責發起並完成整個業務活動
從業務服務提供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;
     }
 });
});
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章