项目扩展三:生产者的确认应答
一、介绍:RabbitMQ的生产者确认应答机制
RabbitMQ对生产者的确认应答,是在对应消息发布到相匹配的消息队列之后就会对生产者进行确认应答
而不是等到消费者ACK消息之后才对生产者进行确认应答
如果该消息被推送到了某个队列当中,那么就会对生产者进行ACK
如果该消息因为某种原因,未能成功接收或者持久化,那么就会对生产者进行NACK
1.如果消息没有匹配任何队列呢?
1.RabbitMQ如何做的?
此时RabbitMQ通常不会直接对生产者进行ACK/NACK,而是通过备用交换机、死信队列、消息返回等等机制来处理这些未路由的消息
这些特性不是RabbitMQ的核心点,而是它的高级特性,实现起来总归是比较复杂的,因此我们就不按RabbitMQ的来了
2.我们打算怎么做?
我们大可以直接给生产者返回一个NACK,并且告诉他:NACK的原因是“no queues”或者“no success route”,该交换机没有绑定队列或者所有绑定的队列都没有匹配上
二、如何实现?
1.需求与设计
1.确认应答处理函数的设计
生产者对于消息是否成功消费,可以有自己灵活的处理方式:
比如:重新发布、记录日志、触发警报、进行下一步操作【比如订单消息成功发送之后,下一步就是更新数据库中的订单状态】等等操作
因此,我们为每个生产者设置一个确认应答处理函数
这个函数应该怎么设计呢?
生产者tag,虚拟机名称,交换机名称,消息属性,消息体,发布机制,bool ok,status_str
这些字段,都在BasicPublishResponse当中,因此我们只需要搞一个参数即可
using ProductorCallback = std::function<void(const std::shared_ptr<ns_proto::BasicPublishResponse> &resp)>;
2.生产者设计
客户端的连接需要根据BasicPublishResponse来找到对应的生产者去调用其确认应答处理函数
因此客户端需要有生产者的描述结构体,其内部至少需要2个内容:
- 生产者tag
- 确认应答处理函数
因为消费者跟信道是一一对应的,所以我们为了追求设计上的对称和优雅感,我们也规定生产者跟信道一一对应
一个客户端,即可以作为生产者,还可以作为消费者,没有任何问题
又因为生产者不牵扯队列和交换机,因此客户端这里我们以信道为单位管理生产者
而对于服务器来说:
因此,在服务器那里我们无需管理生产者,不过服务器的BasicPublish必须要能够拿到该消息的生产者tag
也是为了实现Request和Response的统一,我们给BasicPublishRequest增加一个生产者tag标识
//7. 消息的发布与确认
message BasicPublishRequest
{
string req_id = 1;
string channel_id = 2;
string vhost_name = 3;
string exchange_name = 4; //需要用户指定:消息发布到哪个交换机上,然后我们给他进行路由匹配,放到对应队列当中
string body = 5;
BasicProperities properities = 6;
PublishMechanism mechanism = 7; // 消息的发布方式
string productor_tag = 8; //生产者tag
}
protoc --cpp_out=. mq_proto.proto
2.确认应答Response
下面我们写一个BasicPublishResponse,这是服务器向生产者发送的确认应答响应:
//9. 消息的确认应答响应
message BasicPublishResponse
{
string channel_id = 1;//通过该字段才能找到该生产者所关联的信道
string productor_tag = 2;
string vhost_name = 3;
string exchange_name = 4;
string body = 5;
BasicProperities properities = 6;
PublishMechanism mechanism = 7;
bool ok = 8;//消息路由是否成功,用来区分ACK/NACK
string status_str = 9;//失败的原因
}
protoc --cpp_out=. mq_proto.proto
三、server实现
1.信道模块
1.ackCallback
// 给生产者发送BasicPublishResponse
void ackCallback(const BasicPublishRequestPtr &req, bool ok, const std::string &status_str)
{
BasicPublishResponse resp;
resp.set_channel_id(req->channel_id());
resp.set_productor_tag(req->productor_tag());
resp.set_vhost_name(req->vhost_name());
resp.set_exchange_name(req->exchange_name());
resp.set_body(req->body());
if (req->has_properities())
{
BasicProperities *bp = req->mutable_properities();
resp.mutable_properities()->set_msg_id(bp->msg_id());
resp.mutable_properities()->set_routing_key(bp->routing_key());
resp.mutable_properities()->set_mode(bp->mode());
}
resp.set_mechanism(req->mechanism());
resp.set_ok(ok);
resp.set_status_str(status_str);
_codec->send(_conn, resp);
}
2.basicPublish修改
因为这个确认应答解耦,所以我们打包成异步任务抛入线程池
void basicPublish(const BasicPublishRequestPtr &req)
{
// 1. 先找到该交换机的交换机类型
Exchange::ptr ep = _vhost_manager_ptr->getExchange(req->vhost_name(), req->exchange_name());
if (ep.get() == nullptr)
{
default_fatal("发布消息失败,因为交换机不存在\n,交换机名称:%s"req->exchange_name().c_str());
basicResponse(req->req_id(), req->channel_id(), false);
auto func = [this, req]()
{
ackCallback(req, false, "交换机不存在,交换机名称:" + req->exchange_name());
};
_pool_ptr->put(func);
return;
}
// 2. 先找到消息发布的交换机 绑定的所有队列
MsgQueueBindingMap qmap = _vhost_manager_ptr->getAllBindingsByExchange(req->vhost_name(), req->exchange_name());
bool ok = false; // 假设路由匹配时没有匹配到任何队列
// 3. 遍历所有队列,进行路由匹配与消息投递
for (auto &kv : qmap)
{
Binding::ptr bp = kv.second;
BasicProperities *properities = nullptr;
::std::string routing_key;
if (req->has_properities())
{
properities = req->mutable_properities();
routing_key = properities->routing_key();
}
if (Router::route(routing_key, bp->binding_key, ep->type))
{
ok = true;
// 把消息投递到指定队列
_vhost_manager_ptr->basicPublish(req->vhost_name(), bp->queue_name, properities, req->body(), req->mechanism());
// 判断该消息是否需要推送
if (req->mechanism() == PUSH || req->mechanism() == BOTH)
{
// 5. 向线程池添加一个消息消费任务,消费任务交给线程池中的线程去做,解放Channel线程去做更重要的任务
auto func = ::std::bind(&Channel::publishCallback, this, req->vhost_name(), bp->queue_name);
_pool_ptr->put(func);
}
}
}
if (ok)
{
auto func = [this, req, ok]()
{
ackCallback(req, ok, "一切顺利");
};
_pool_ptr->put(func);
}
else
{
if (qmap.empty())
{
auto func = [this, req, ok]()
{
ackCallback(req, ok, "该交换机未绑定任何队列");
};
_pool_ptr->put(func);
}
else
{
auto func = [this, req, ok]()
{
ackCallback(req, ok, "路由匹配时没有匹配到任何队列");
};
_pool_ptr->put(func);
}
}
// 返回响应即可
basicResponse(req->req_id(), req->channel_id(), true);
}
四、client实现
1.生产者定义
using ProductorCallback = std::function<void(const std::string& productor_tag,const std::string& vhost_name,const std::string& exchange_name,
const ns_proto::BasicProperities * bp, const std::string & body, ns_proto::PublishMechanism mechanism,bool ok,const std::string& status_str)>;
struct Productor
{
using ptr = std::shared_ptr<Productor>;
Productor(const std::string &productor_tag, const ProductorCallback &callback)
: _productor_tag(productor_tag), _callback(callback) {}
Productor() = default;
std::string _productor_tag;
ProductorCallback _callback;
};
2.信道模块
using BasicPublishResponsePtr = std::shared_ptr<BasicPublishResponse>;
1.生产者的绑定与解除绑定
生产者在发布消息之前必须进行绑定,发布完消息可以选择解除绑定
bool ProductorBind(const std::string& productor_tag,const ProductorCallback& callback)
{
if(_productor.get()!=nullptr)
{
default_error("生产者绑定失败,因为该信道已经绑定了生产者");
return false;
}
_productor=std::make_shared<Productor>(productor_tag,callback);
return true;
}
void ProductorUnBind()
{
_productor.reset();
}
2.提供给连接模块的确认应答回调函数
- 信道模块要实现一个提供给连接模块的用于调用其关联的生产者的确认应答函数
void productor(BasicPublishResponsePtr resp)
{
// 0. 看productor是否还在
if(_productor.get()==nullptr)
{
default_info("消息确认应答失败,本信道关联的生产者已经不在了");
return;
}
// 1. 看信道是否相同
if (resp->channel_id() != _channel_id)
{
default_info("消息确认应答失败,因为resp的信道ID与本信道ID不同,resp:%s ,本信道ID: %s",resp->channel_id().c_str(),_channel_id.c_str());
return;
}
// 2. 看生产者tag是否相同
if (resp->productor_tag() != _productor->_productor_tag)
{
default_info("消息确认应答失败,因为resp的生产者tag与本信道生产者tag不同,resp:%s ,本信道生产者tag: %s",resp->productor_tag().c_str(),_productor->_productor_tag.c_str());
return;
}
// 3. 调用该生产者的确认应答处理函数
_productor->_callback(resp);
}
3.连接模块
注册一个针对BasicPublishResponse的事件响应函数
_dispatcher.registerMessageCallback<BasicPublishResponse>(std::bind(&Connection::OnPublishResponse, this, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3));
void OnPublishResponse(const muduo::net::TcpConnectionPtr& conn,const BasicPublishResponsePtr& resp,muduo::Timestamp)
{
// 1. 找到信道
Channel::ptr cp=_channel_manager->getChannel(resp->channel_id());
if(cp.get()==nullptr)
{
default_info("调用生产者的确认应答处理回调函数失败,因为未找到该信道, 信道ID: %s",resp->channel_id().c_str());
return;
}
// 2. 将 调用该信道对应的productor任务包装一下抛入线程池
_worker->_pool.put([cp,resp]{
cp->productor(resp);
});
}
4.发布消息的修改
bool BasicPublish(const std::string &vhost_name, const std::string &exchange_name, const BasicProperities *bp, const std::string &body, PublishMechanism mechanism)
{
if(_productor.get()==nullptr)
{
default_error("发布消息失败,因为该信道尚未关联生产者");
return false;
}
BasicPublishRequest req;
std::string rid = UUIDHelper::uuid();
req.set_req_id(rid);
req.set_channel_id(_channel_id);
req.set_vhost_name(vhost_name);
req.set_exchange_name(exchange_name);
req.set_body(body);
req.set_mechanism(mechanism);
req.set_productor_tag(_productor->_productor_tag);
if (bp != nullptr)
{
req.mutable_properities()->set_msg_id(bp->msg_id());
req.mutable_properities()->set_mode(bp->mode());
req.mutable_properities()->set_routing_key(bp->routing_key());
}
// 发送请求
_codec->send(_conn, req);
BasicCommonResponsePtr resp = waitResponse(rid);
std::ostringstream oss;
if (resp->ok())
{
default_info("发布消息成功 %s",body.c_str());
}
else
{
default_info("发布消息失败 %s",body.c_str());
}
return resp->ok();
}
五、信道池的修改
因为我们的信道池当中的信道是复用的,所以当中的信道必须不能关联任何生产者,所以我们要修改一下信道池当中的recover函数
void recover(const Channel::ptr &key)
{
std::unique_lock<std::mutex> ulock(_mutex);
// 将unavailable_list当中的指定信道移动到available_list的尾部【splice】
// 先查找该值在哈希表当中是否存在
auto iter_hash = data_hash.find(key);
if (iter_hash == data_hash.end())
{
default_warning("恢复信道时,对应信道并未在哈希表当中找到");
return;
}
iter_type iter_list = iter_hash->second;
// 为AVAILABLE,说明它在available_list,有问题:因为只有available_list当中的信道才是空闲信道,才有可能被恢复,不能恢复空闲信道
if (iter_list->second == AVAILABLE)
{
default_warning("恢复信道时,对应信道为空闲信道,不予恢复");
return;
}
// 调用该信道的basicCancel来取消订阅
if (key->BasicCancel())
{
default_info("恢复信道时,取消消费者关联成功");
}
key->ProductorUnBind();
// 移动该迭代器,放到空闲链表的尾部(保证最近最少使用的是在头部)
available_list.splice(available_list.end(), unavailable_list, iter_list);
// 移动之后,将他改为avaliable
iter_list->second = AVAILABLE;
// 添加该信道的定时
_helper->set_timer(key);
default_info("恢复信道成功 %s",key->cid().c_str());
}
六、测试
1.测试1
因为我们的生产者的确认应答跟消费者无关,所以我们测试不需要写消费者,最后我们写个消费者确认一下不会有任何BUG对生产者造成问题即可
1.生产者代码
这里生产者没有解除绑定,顺便测一下信道池对信道资源的恢复是否彻底
#include "connection.hpp"
using namespace ns_mq;
#include <thread>
#include <vector>
using namespace std;
void Callback(const Channel::ptr &cp, const BasicPublishResponsePtr &resp)
{
cout << resp->productor_tag() << " 收到消息的确认应答,消息体:" << resp->body() << ",消息处理情况:是否ok:" << (resp->ok() ? "成功" : "失败")
<< resp->status_str() << "\n";
if (resp->has_properities())
{
cout << "消息routing_key:" << resp->properities().routing_key() << "\n";
cout << "消息投递模式:" << DeliveryMode_Name(resp->properities().mode()) << "\n";
cout << "消息ID:" << resp->properities().msg_id() << "\n";
}
else
{
cout << "该消息没有基础属性\n";
}
cout << "消息发布机制:" << resp->mechanism() << "\n";
if(!resp->ok())
{
cp->BasicPublish(resp->vhost_name(),resp->exchange_name(),resp->mutable_properities(),resp->body(),resp->mechanism());
}
}
// host1
void publisher1(const Connection::ptr &conn, const std::string &thread_name)
{
// 1. 创建信道
Channel::ptr cp = conn->getChannel();
// 2. 创建虚拟机,交换机,队列,并进行绑定
cp->declareVirtualHost("host1", "./host1/resource.db", "./host1/message");
cp->declareExchange("host1", "exchange1", TOPIC, true, false, {});
cp->declareMsgQueue("host1", "queue1", true, false, false, {});
cp->declareMsgQueue("host1", "queue2", true, false, false, {});
cp->bind("host1", "exchange1", "queue1", "news.sport.#");
cp->bind("host1", "exchange1", "queue2", "news.*.zhangsan");
// 3. 关联生产者
cp->ProductorBind("productor1", std::bind(Callback, cp, std::placeholders::_1));
// 4. 发送10条消息
BasicProperities bp;
bp.set_mode(DURABLE);
bp.set_routing_key("news.sport.basketball");
for (int i = 0; i < 10; i++)
{
bp.set_msg_id(UUIDHelper::uuid());
cp->BasicPublish("host1", "exchange1", &bp, "Hello -" + std::to_string(i), PUSH);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 5. 关闭信道
conn->returnChannel(cp);
}
// host2
void publisher2(const Connection::ptr &conn, const std::string &thread_name)
{
// 1. 创建信道
Channel::ptr cp = conn->getChannel();
// 2. 创建虚拟机,交换机,队列,并进行绑定
cp->declareVirtualHost("host2", "./host2/resource.db", "./host2/message");
cp->declareExchange("host2", "exchange1", TOPIC, true, false, {});
cp->declareMsgQueue("host2", "queue1", true, false, false, {});
cp->declareMsgQueue("host2", "queue2", true, false, false, {});
cp->bind("host2", "exchange1", "queue1", "news.sport.#");
cp->bind("host2", "exchange1", "queue2", "news.*.zhangsan");
// 3. 关联生产者
cp->ProductorBind("productor2", std::bind(Callback, cp, std::placeholders::_1));
// 4. 发送10条消息
BasicProperities bp;
bp.set_mode(DURABLE);
bp.set_routing_key("news.sport.basketball");
for (int i = 0; i < 10; i++)
{
bp.set_msg_id(UUIDHelper::uuid());
cp->BasicPublish("host2", "exchange1", &bp, "Hello -" + std::to_string(i), PUSH);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 5. 关闭信道
conn->returnChannel(cp);
}
int main()
{
AsyncWorker::ptr worker = std::make_shared<AsyncWorker>();
Connection::ptr myconn = std::make_shared<Connection>("127.0.0.1", 8888, worker);
vector<thread> thread_v;
thread_v.push_back(thread(publisher1, myconn, "thread1"));
thread_v.push_back(thread(publisher2, myconn, "thread2"));
for (auto &t : thread_v)
t.join();
return 0;
}
2.验证
server那里一直提示是因为发布消息时,没有消费者,所以只能将消息发布到待拉取消息链表当中了
2.测试2
下面,我们把消息的routing_key改掉,让这个消息无法在路由时匹配到任何队列,应该会给我们提示
路由匹配时没有匹配到任何队列
我们的处理方式是重传,这只是测试而已,测的是功能,无需太结合具体处理方案,大家谅解一下啦
bp.set_routing_key("news.finance");
我们把routing_key改成金融新闻,这样他就无法成功路由匹配到任何队列
这里太快是因为我们重传是立刻重传的,所以…
3.测试3
我们把routing_key改回来
bp.set_routing_key("news.sport.basketball");
下面把消费者引入,验证不会出现任何影响
#include "connection.hpp"
using namespace ns_mq;
#include <thread>
#include <vector>
#include <thread>
using namespace std;
// 因为要拿到信道才能进行确认,所以这里需要把Channel::ptr bind过来
void Callback(const Channel::ptr &cp, const std::string &consumer_tag, const BasicProperities *bp, const std::string &body)
{
// 1. 消费消息
std::string id;
if (bp != nullptr)
{
id = bp->msg_id();
}
std::cout << consumer_tag << " 消费了消息: " << body << ", 消息ID: " << id << "\n";
// 2. 确认消息
if (bp != nullptr)
std::cout << cp->BasicAck(id) << "\n";
}
void consumer1(const Connection::ptr &conn, const std::string &thread_name)
{
Channel::ptr cp = conn->getChannel();
default_info("consumer1: 信道ID:%s");
// 2. 创建虚拟机,交换机,队列,并进行绑定
cp->declareVirtualHost("host1", "./host1/resource.db", "./host1/message");
cp->declareExchange("host1", "exchange1", TOPIC, true, false, {});
cp->declareMsgQueue("host1", "queue1", true, false, false, {});
cp->declareMsgQueue("host1", "queue2", true, false, false, {});
cp->bind("host1", "exchange1", "queue1", "news.sport.#");
cp->bind("host1", "exchange1", "queue2", "news.*.zhangsan");
// 3. 创建消费者
cp->BasicConsume("host1", "consumer1", "queue1",
std::bind(Callback, cp, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3), false);
// 4. 等待消息
while (true)
{
cp->BasicPull();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 5. 关闭信道
conn->returnChannel(cp);
}
void consumer2(const Connection::ptr &conn, const std::string &thread_name)
{
Channel::ptr cp = conn->getChannel();
default_info("consumer2: 信道ID:%s",cp->cid().c_str());
// 2. 创建虚拟机,交换机,队列,并进行绑定
cp->declareVirtualHost("host2", "./host2/resource.db", "./host2/message");
cp->declareExchange("host2", "exchange1", TOPIC, true, false, {});
cp->declareMsgQueue("host2", "queue1", true, false, false, {});
cp->declareMsgQueue("host2", "queue2", true, false, false, {});
cp->bind("host2", "exchange1", "queue1", "news.sport.#");
cp->bind("host2", "exchange1", "queue2", "news.*.zhangsan");
// 3. 创建消费者
cp->BasicConsume("host2", "consumer2", "queue1",
std::bind(Callback, cp, std::placeholders::_1, std::placeholders::_2, std::placeholders::_3), false);
// 4. 等待消息
while (true)
{
cp->BasicPull();
std::this_thread::sleep_for(std::chrono::seconds(1));
}
// 5. 关闭信道
conn->returnChannel(cp);
}
int main()
{
AsyncWorker::ptr worker = std::make_shared<AsyncWorker>();
// 1. 创建连接和信道
Connection::ptr conn = std::make_shared<Connection>("127.0.0.1", 8888, worker);
vector<thread> thread_v;
thread_v.push_back(thread(consumer1, conn, "thread1"));
thread_v.push_back(thread(consumer2, conn, "thread2"));
for (auto &t : thread_v)
t.join();
return 0;
}
验证成功,所有消息均已成功推送与消费
以上就是项目扩展三:生产者的确认应答的全部内容