对应的步骤
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/30619cbd3a5143b7bd0e2f7b337ba3dd.png)
看看代码实现流程,支付成功后,首先是pay网关收到支付成功的消息,调用rpc接口,预发送消息,更新订单状态,存储发送消息,发送消息到中间件,由中间件把消息传递给消费端的监听队列,最终增加用户的积分,更新订单状态和增加积分虽然不是同时进行,但是最终的结果是一致的,就是分布式事务的最终一致性
存储预发送消息(主动方应用系统)
先看看pay网关的order函数,预发送消息,然后通过rpc调用messge微服务的prepareMsg方法预存储消息,然后rpc调用order微服务的update方法更新订单状态,如果成功就rpc调用messge微服务的confirmTosend方法确认并发送消息到消息中间件
public function order()
{
//预发送消息(消息状态子系统)
$msg_id=session_create_id(md5(uniqid()));
$data=[
'msg_id'=>uniqid(),
'version'=>1,
'create_time'=>time(),
'message_body'=>['order_id'=>12133,'shop_id'=>2],
'notify_url'=>'http://127.0.0.1:9804/notify/index',//通知地址
'notify_rule'=>[1=>5,2=>10,3=>15],//单位为秒
'notify_retries_number'=>0, //重试次数,
'default_delay_time'=>1,//毫秒为单位
'status'=>1, //消息状态
];
// var_dump($this->notifyService->publish($data));
// return ['1'];
$prepareMsgData=[
'msg_id'=>$msg_id,
'version'=>1,
'create_time'=>time(),
'message_body'=>['order_id'=>12133,'shop_id'=>2],
'consumer_queue'=>'order', //消费队列(消费者)
'message_retries_number'=>0, //重试次数,
'status'=>1, //消息状态
];
//预存储消息
$result = $this->messageService->prepareMsg($prepareMsgData);
$data=[
'order_id'=>1,
'msg_id'=>$msg_id
];
//调用订单服务更新状态
if ($result['status'] == 1) { //消息恢复子系统(查询未确认消息) 确认并且投递
$this->orderService->update($data)['status']==1 && $this->messageService->confirmMsgToSend($msg_id,1);//更新订单
}
//确认并且投递消息
return [$result];
}
查询消费确认超时的消息(消息恢复子系统)
然后在message微服务设置一个监听事件,用做恢复消息子系统,定义一个定时器,查询超时未确认的任务,消息状态为2表示已经投递给消息中间件,但是没有被消费,判断一下这条消息的投递次数,如果超过两次就不用让它继续投递了,因为如果消息一直失败,会对服务器造成很大压力,把这条消息从redis删掉,存到某个队列当中,可以人工手动进行恢复,如果消息没有超过投递次数,就继续投递消息;如果消息的状态为1,表示已经进入消息子系统但是未投递的,这时候确认订单状态,如果订单状态更新了,就继续投递消息,如果订单状态没有更新成功,直接删除这条消息
public function handle(EventInterface $event): void
{
//多进程自己实现下
$time=1; //超时任务
swoole_timer_tick(10000,function ()use($time){
sgo(function()use($time){
//自动初始化一个Context上下文对象(协程环境下)
$context = ServiceContext::new();
Context::set($context);
try{
//查询超时未确认的任务
$service=$this->connection->zRangeByScore('message_system_time', "-inf", (string)(time()-$time));
var_dump(conunt($service));
foreach ($service as $v){
$data=$this->connection->hget('message_system',(string)$v);
if(!empty($data)){
$data=json_decode($data,true);
if($data['status']==2){ //状态为2代表的是已投递,超时没有被正确消费(消息恢复系统)
//尝试重新投
//如果投递的次数超过最大值,删除任务,并且存到单独存到redis一个队列当中
if($data['message_retries_number']>=2){
//var_dump($data['message_retries_number'],"投递失败,手动重试");
//可以封装成服务
$this->connection->transaction(function (\Redis $redis) use ($v,$data) {
$redis->hdel("message_system", (string)$v);
$redis->zrem("message_system_time", (string)$v);
//放在某个队列当中,在消息管理子系统当中可以手动恢复
$redis->lPush("message_system_dead",json_encode($data));
});
}
$this->messageService->confirmMsgToSend($v,2); //投递业务
}elseif($data['status']==1){ //消息状态子系统(已经进入消息子系统但是未投递的)
$stateJob=$this->orderService->confirmStatus($v);
//1.查询任务结果(主动方任务是成功的,第一次投递到被动方的服务)
if($stateJob['status']==1){
$this->messageService->confirmMsgToSend($v,1); //投递业务
}elseif($stateJob['status']==0){ //当前任务是失败的任务,删掉
//3.任务失败(删除任务)
$this->messageService->ackMsg($v);
}
}
}
}
//判断任务的状态是预发送,并且确认消息状态,如果主动方任务成功,我们就投递,否则删除
//确认业务状态,业务成功投递,业务失败删除
}catch (\Exception $e){
var_dump($e->getMessage());
}
});
});
}
确认并发送消息(主动方应用系统)
消息投递到消息中间件的监听队列,是通过confirmToSend这个函数,用的是rabbitmq这个中间件,创建队列,创建交换机,绑定消息交换机和队列,从redis获取这条消息的相关数据,如果flag为2,说明是被恢复过的消息,投递次数+1,发布消息到交换机当中,并绑定好路由关系,设置消息在redis中的状态,并将消息投递给MQ(实时消息队列),成功则投递成功,失败或者消息数据为空则投递失败
public function confirmMsgToSend($msg_id, $flag): array
{
try {
$connection = $this->rabbit->connect();
$connectionRabbit = $connection->connection;
$exchangeName = 'tradeExchange';
$routeKey = '/trade';
$queueName = 'trade';
$channel = $connectionRabbit->channel();
/**
* 创建队列(Queue)
* name: hello // 队列名称
* passive: false // 如果设置true存在则返回OK,否则就报错。设置false存在返回OK,不存在则自动创建
* durable: true // 是否持久化,设置false是存放到内存中的,RabbitMQ重启后会丢失
* exclusive: false // 是否排他,指定该选项为true则队列只对当前连接有效,连接断开后自动删除
* auto_delete: false // 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除
*/
$channel->queue_declare($queueName, false, true, false, false);
/**
* 创建交换机(Exchange)
* name: vckai_exchange// 交换机名称
* type: direct // 交换机类型,分别为direct/fanout/topic,参考另外文章的Exchange Type说明。
* passive: false // 如果设置true存在则返回OK,否则就报错。设置false存在返回OK,不存在则自动创建
* durable: false // 是否持久化,设置false是存放到内存中的,RabbitMQ重启后会丢失
* auto_delete: false // 是否自动删除,当最后一个消费者断开连接之后队列是否自动被删除
*/
$channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);
// 绑定消息交换机和队列
$channel->queue_bind($queueName, $exchangeName);
$data = $this->connectionRedis->hget("message_system", (string)$msg_id);
if (!empty($data)) {
$data = json_decode($data, true);
$data['status'] = 2;
if ($flag == 2) {
//被消息恢复子系统投递的任务
$data['message_retries_number'] = $data['message_retries_number'] + 1;
}
$data = json_encode($data);
/**
* 创建AMQP消息类型
* delivery_mode 消息是否持久化
* AMQPMessage::DELIVERY_MODE_NON_PERSISTENT 不持久化
* AMQPMessage::DELIVERY_MODE_PERSISTENT 持久化
*/
$msg = new \PhpAmqpLib\Message\AMQPMessage($data, ['delivery_mode' => \PhpAmqpLib\Message\AMQPMessage:: DELIVERY_MODE_NON_PERSISTENT]);
//发布消息到交换机当中,并且绑定好路由关系
if ($this->connectionRedis->hset("message_system", (string)$msg_id, $data) == 0 && $channel->basic_publish($msg,$exchangeName, $routeKey) == null) {
//将消息投递给MQ(实时消息服务)
$data = ['status' => 1, 'result' => '确认并且投递成功'];
} else {
$data = ['status' => 0, 'result' => '确认投递失败'];
}
} else {
$data = ['status' => 0, 'result' => '确认投递失败'];
}
$channel->close();
$connection->release(true);
return $data;
} catch (\Exception $e) {
var_dump($e->getFile(), $e->getLine(), $e->getMessage());
}
}
确认消息已被成功消费(被动方应用系统)
消息投递到实时消息队列后,定义一个消费端的监听事件,来消费这些消息,从而增加用户的积分,先连接rabbitmq,队列绑定交换机跟路由,调用basic_consume取到消息队列中的数据,根据消息的id从redis中取到这条消息的积分消息状态,如果状态为2,说明消费成功了,如果状态为1,说明正在进行中,如果没有相关的状态,先设置消息在redis中的积分消息状态为1(setex消费幂等:同一个任务执行10次跟执行一次的效果是一样),这是模拟让我在执行中,然后设置消息状态为2,调用message微服务消费成功的方法ackMsg从redis中删除消息并返回消费成功,最后basic_ack确认这条消息,表示这条已经被消费,wait表示消息的消费是阻塞等待的
public function handle(EventInterface $event): void
{
//注册服务
$config = bean('config')->get('provider.consul');
bean('consulProvider')->registerServer($config);
$callBack = function () {
go(function () {
$context = ServiceContext::new();
\Swoft\Context\Context::set($context);
$exchangeName = 'tradeExchange';
$routeKey = '/trade';
$queueName = 'trade';
$connection=$this->rabbit->connect();
$connectionRabbit=$connection->connection;
$channel = $connectionRabbit->channel();
$channel->queue_declare($queueName, false, true, false, false);
$channel->exchange_declare($exchangeName, \PhpAmqpLib\Exchange\AMQPExchangeType::DIRECT, false, true, false);
//队列绑定交换机跟路由
$channel->queue_bind($queueName, $exchangeName, $routeKey);
$channel->basic_consume($queueName, '', false, false, false, false, function ($message) {
$data = json_decode($message->body, true);
//1.记录消息任务是否完成(消费幂等:同一个任务执行10次跟执行一次的效果是一样)
$statusJob = $this->connectionRedis->get("integrating_message_job", (string)$data['msg_id']);
if ($statusJob == 2) { //已经消费成功了
$this->messageService->ackMsg($data['msg_id']); //这个任务已经消费成功了
} elseif ($statusJob == 1) { //任务正在执行
var_dump("任务正在执行当中");
return;
}else{
//执行任务当中,并且设置释放的时间
$this->connectionRedis->setex("integrating_message_job:".$data['msg_id'],10,1);
//sleep(5);//任务正在执行当中
//2.操作mysql更新积分(业务逻辑执行完毕)
$this->connectionRedis->set("integrating_message_job:".$data['msg_id'] , 2);//执行任务完毕
$this->messageService->ackMsg($data['msg_id']); //这个任务已经消费成功了
//回应ack
$message->delivery_info['channel']->basic_ack($message->delivery_info['delivery_tag']);
var_dump($data);
}
});
// Loop as long as the channel has callbacks registered
while ($channel->is_consuming()) {
$channel->wait(); //阻塞消费
}
/**
* @param \PhpAmqpLib\Channel\AMQPChannel $channel
* @param \PhpAmqpLib\Connection\AbstractConnection $connection
*/
function shutdown($channel, $connection)
{
$channel->close();
$connection->close();
}
register_shutdown_function('shutdown', $channel, $connection);
});
};
// //防止进程意外崩溃,回收子进程
\Swoole\Process::signal(SIGCHLD, function ($sig) use ($callBack) {
while ($ret = \Swoole\Process::wait(false)) {
$p = new \Swoole\Process($callBack);
$p->start();
}
});
$p = new \Swoole\Process($callBack);
$p->start();
}
看看ackMsg方法,从redis中删除该条消息,并返回消费成功
/**
* 消息消费成功
* @return array
*/
public function ackMsg($msg_id): array
{
//删除已确认消费的消息
$result = $this->connectionRedis->transaction(function (\Redis $redis) use ($msg_id) {
$redis->hdel("message_system", (string)$msg_id);
$redis->zrem("message_system_time", (string)$msg_id);
});
if ($result[0] !== false) {
$data = ['status' => 1, 'result' => '任务消费成功'];
} else {
$data = ['status' => 0, 'result' => '任务消费失败'];
}
return $data;
}