Bootstrap

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;
     }
 });
});
}
;