代码实现
实现
一个完整的业务活动由一个主业务服务与若干从业务服务组成
主业务服务负责发起并完成整个业务活动
从业务服务提供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;
}
});
});
}