封裝rabbitMQ的連接池
消息子系統的構建
1.構建消息服務子系統(包含接口)
存儲預發送消息(主動方應用系統)
確認併發送消息(主動方應用系統)
確認消息已被成功消費(被動方應用系統)
查詢狀態確認超時的消息(消息狀態確認子系統)
查詢消費確認超時的消息(消息恢復子系統)
2.構建其他服務(骨架大搭建)
1.訂單服務
2.積分服務
3.消息子系統
4.網關服務
3.簡單的封裝rabbitMQ的連接池
1.解決連接池,無法取出連接的問題
4、消息發佈確認
5、消息存儲的設計(任務數據存儲)
version 版本號
create_time 創建時間
message_id 消息ID
message_body 消息內容
consumer_queue 消費隊列
message_retries_number 消息重發次數
dead 是否死亡
status 狀態(預發送、發送中、已消費、已死亡)
6、消息id的生成
7、服務提供任務執行結果查詢接口
整體流程:
1、用戶下單,主動方應用預發送消息給消息服務子系統。
2、消息服務子系統存儲預發送的消息。
3、返回存儲預發送消息的結果。
4、如果第3步返回的結果是成功的,則執行業務操作,否則不執行。
5、業務操作成功後,調用消息服務子系統進行確認發送消息。
6、將消息服務庫中存儲的預發送消息發送,並更新該消息的狀態爲已發送(但不是已被消費)。
7、消息中間件發送消息到消費端應用。
8、消費端應用調用被動方應用服務。
9、被動方應用返回結果給消費端應用。
10、消費端應用向消息中間件ack此條消息,並向消息服務子系統進行確認成功消費消息,11、讓消息服務子系統刪除該條消息或者將狀態置爲已成功消費。
12、消息狀態子系統定時去查一下消息數據,看看有沒有是已發送狀態的超時消息,就是一直沒有變成已成功消費的那種消息,主動方應用系統應該提供查詢接口,針對某條消息查詢該條消息對應的業務數據是否爲處理成功
13、如果業務數據是處理成功的狀態,那麼就再次調用確認併發送消息,即進入第6步。
14、如果業務數據是處理失敗的,那麼就調用消息服務子系統進行刪除該條消息數據。
現在在做紅色框框的部分,代碼體現
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];
}
通過rpc調用message服務的預存儲消息函數,通過redis事務把數據存入message_system,和message_system_time中,他們是通過$msg_id來區分不同的消息
public function prepareMsg($prepareMsgData): array
{
$msg_id = $prepareMsgData['msg_id'];
//存數據,還要存時間,依據時間查找超時的任務
$result = $this->connectionRedis->transaction(function (\Redis $redis) use ($msg_id, $prepareMsgData) {
$redis->hset("message_system", (string)$msg_id, json_encode($prepareMsgData));
$redis->zAdd("message_system_time", $prepareMsgData['create_time'], (string)$msg_id);
});
if ($result[0] == false) {
return ['status' => 0, 'result' => '預發送消息失敗'];
}
return ['status' => 1, 'result' => '預發送消息成功'];
}
預存儲消息成功後,rpc調用訂單服務的update函數更新訂單的狀態,如果訂單狀態更新成功,則調用消息服務確認發送消息的函數confirmMsgToSend
public function update($data): array
{
//業務執行成功
//調用mysql更新信息(業務邏輯)
//確認某個任務執行成功
if($this->redis->hset("order_message_job",(string)$data['msg_id'],(string)1)){
return ['status'=>1,'result' =>'訂單狀態更新成功'];
}
return ['status'=>1,'result' =>'訂單狀態更新失敗'];
}
confirmMsgToSend函數,連接rabbitmq的連接池,從連接池裏面取出對應的實例,調用裏面的方法,從redis裏面取出消息的數據,如果消息狀態爲2就是之前沒有發送成功,現在恢復發送的消息,在嘗試次數上加1,創建消息隊列,發送消息到交換機中,如果redis中這條消息爲0並且發佈消息爲null,則投遞消息成功
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());
}
}
現在來看這兩行代碼發生了什麼
$connection = $this->rabbit->connect();
$connectionRabbit = $connection->connection;
這裏封裝了rabbitmq連接連接池的相關代碼
public function connect()
{
try {
/* @var ConnectionManager $conManager */
$conManager = BeanFactory::getBean('connection.manager');
$connection = $this->getConnection();
$connection->setRelease(true); //設置重新使用
$conManager->setConnection($connection); //設置連接
} catch (Throwable $e) {
throw new \Exception(
sprintf('Pool error is %s file=%s line=%d', $e->getMessage(), $e->getFile(), $e->getLine())
);
}
// Not instanceof Connection
if (!$connection instanceof Connection) {
throw new \Exception(
sprintf('%s is not instanceof %s', get_class($connection), Connection::class)
);
}
return $connection;
}
getBean函數是從容器中調用獲取實例化方法獲取實例名字
public static function getBean(string $name)
{
return Container::getInstance()->get($name);
}
getConnection函數,通過通道獲取連接
public function getConnection(): ConnectionInterface
{
return $this->getConnectionByChannel();
}
getConnectionByChannel函數,如果通道爲空創建一個,如果沒有到達最小連接數則創建連接,如果通道不爲空則從裏面拋出一個連接,如果連接不爲空,則更新連接的最新數據並返回,如果小於最大連接數,則創建連接,如果最大等待大於0,並且消費數量大於最大等待拋異常,以上都沒進入返回就從通道拋出最大等待時間,調用更新返回連接函數並返回連接
private function getConnectionByChannel(): ConnectionInterface
{
// Create channel
if ($this->channel === null) {
$this->channel = new Channel($this->maxActive);
}
// To reach `minActive` number
if ($this->count < $this->minActive) {
return $this->create();
}
// Pop connection
$connection = null;
if (!$this->channel->isEmpty()) {
$connection = $this->popByChannel();
}
// Pop connection is not null
if ($connection !== null) {
// Update last time
$connection->updateLastTime();
return $connection;
}
// Channel is empty or not reach `maxActive` number
if ($this->count < $this->maxActive) {
return $this->create();
}
// Out of `maxWait` number
$stats = $this->channel->stats();
if ($this->maxWait > 0 && $stats['consumer_num'] >= $this->maxWait) {
throw new ConnectionPoolException(
sprintf('Channel consumer is full, maxActive=%d, maxWait=%d, currentCount=%d',
$this->maxActive, $this->maxWaitTime, $this->count)
);
}
/* @var ConnectionInterface $connection*/
// Sleep coroutine and resume coroutine after `maxWaitTime`, Return false is waiting timeout
$connection = $this->channel->pop($this->maxWaitTime);
if ($connection === false) {
throw new ConnectionPoolException(
sprintf('Channel pop timeout by %fs', $this->maxWaitTime)
);
}
// Update last time
$connection->updateLastTime();
return $connection;
}
看看通道里面的create函數
/**
* @return ConnectionInterface
*
* @throws ConnectionPoolException
*/
private function create(): ConnectionInterface
{
// Count before to fix more connection bug
$this->count++;
try {
$connection = $this->createConnection();
} catch (Throwable $e) {
// Create error to reset count
$this->count--;
throw new ConnectionPoolException(
sprintf('Create connection error(%s) file(%s) line (%d)',
$e->getMessage(),
$e->getFile(),
$e->getLine())
);
}
return $connection;
}
create裏面調用了createConnection()
public function createConnection(): ConnectionInterface
{
if (empty($this->client)) {
throw new \Exception(
sprintf('Pool(%s) client can not be null!', __CLASS__)
);
}
return $this->client->createConnection($this);
}
然後是client調用連接池的createConnection()
public function createConnection($pool): Connection
{
$connection = Connection::new($this, $pool);
$connection->create();
return $connection;
}
然後連接池createConnection調用的create方法,是一個tcp連接
public function create(): void
{
$connection = new \Co\Client(SWOOLE_SOCK_TCP);
[$host, $port] = $this->getHostPort();
$setting = $this->client->getSetting();
//賦值屬性用於區分服務
$this->host=$host;
$this->port=$port;
if (!empty($setting)) {
$connection->set($setting);
}
if (!$connection->connect($host, (int)$port)) {
throw new RpcClientException(
sprintf('Connect failed host=%s port=%d', $host, $port)
);
}
$this->connection = $connection;
}
再看看這裏面的connect是調用Swoole\Coroutine的client類,是一個協程連接
public function connect(string $host, int $port=null, float $timeout=null, $sock_flag=null){}
打印$connection = $this->rabbit->connect();這個數據,數據太多刪掉了一些
object(Six\Rabbit\Connection)#6651 (9) {
["connection"]=>
object(PhpAmqpLib\Connection\AMQPStreamConnection)#6652 (44) {
["channels"]=>
array(1) {
[0]=>
*RECURSION*
}
}
["methodMap":protected]=>
object(PhpAmqpLib\Helper\Protocol\MethodMap091)#6661 (1) {
["method_map":protected]=>
array(64) {
["10,10"]=>
string(16) "connection_start"
["10,11"]=>
}
}
["channel_id":protected]=>
int(0)
["msg_property_reader":protected]=>
["pool":protected]=>
object(Six\Rabbit\Pool)#6437 (11) {
["client":protected]=>
object(Six\Rabbit\Rabbit)#6417 (3) {
["host":protected]=>
string(9) "127.0.0.1"
["port":protected]=>
int(5672)
["channel":protected]=>
object(Swoole\Coroutine\Channel)#6649 (2) {
["capacity"]=>
int(1000)
["errCode"]=>
int(0)
}
}
設置連接函數setConnection就是把connection打包成數組
public function setConnection(ConnectionInterface $connection): void
{
$key = sprintf('%d.%d.%d', Co::tid(), Co::id(), $connection->getId());
$this->set($key, $connection);
}