概述

  接着我们上一篇继续分析消息发送,上节讲到消息发送前有可能遇到 broker 失效的情况,RocketMQ 主要是采用两种策略 :

  • 重试发送
  • broker 故障延迟机制
      后者指的是当发送给某一broker失败后,会将该broker暂时排除在消息队列的选择范围内,到达某个时间点后再继续重试发送,发送的时候消耗的时长越多,那么延迟的时长就越多(就像缓存算法一样,使用得越少,越容易给淘汰)。下面介绍broker 故障延时机制

LatencyFaultToleranceImpl

接着昨天选取某一个消息队列的代码
    /**
* Broker 故障延迟机制
* 获取,判断是否失效,失效则移存记录
*
*/
public MessageQueue selectOneMessageQueue(final TopicPublishInfo tpInfo, final String lastBrokerName) {
if (this.sendLatencyFaultEnable) {
try {
//同一个线程,index 增加 1
int index = tpInfo.getSendWhichQueue().getAndIncrement();
for (int i = 0; i < tpInfo.getMessageQueueList().size(); i++) {
//取余,使同个线程发送到不同的 messagequeue
int pos = Math.abs(index++) % tpInfo.getMessageQueueList().size();
if (pos < 0)
pos = 0;
MessageQueue mq = tpInfo.getMessageQueueList().get(pos);
//判断该 broker 是否失效
if (latencyFaultTolerance.isAvailable(mq.getBrokerName())) {
if (null == lastBrokerName || mq.getBrokerName().equals(lastBrokerName))
return mq;
}
}
// 获取的 mq 失效
final String notBestBroker = latencyFaultTolerance.pickOneAtLeast();
int writeQueueNums = tpInfo.getQueueIdByBroker(notBestBroker);
if (writeQueueNums > 0) {
final MessageQueue mq = tpInfo.selectOneMessageQueue();
if (notBestBroker != null) {
mq.setBrokerName(notBestBroker);
mq.setQueueId(tpInfo.getSendWhichQueue().getAndIncrement() % writeQueueNums);
}
return mq;
} else {
latencyFaultTolerance.remove(notBestBroker);
}
} catch (Exception e) {
log.error("Error occurred when selecting message queue", e);
} return tpInfo.selectOneMessageQueue();
} return tpInfo.selectOneMessageQueue(lastBrokerName);
}
broker 故障延迟机制相关类如下

其中LatencyFaultToleranceImpl 定义内部类 FaultItem ,从名字也可以看出这是放置失效broker用的,内部存放 currentLatency表示延时时间,startTimestamp 表示失效时间终点时间戳。
    // LatencyFaultToleranceImpl#updateFaultItem 

    @Override
/**
*
* @param name broker名字
* @param currentLatency 当前延迟时长
* @param notAvailableDuration 延迟时长
*/
@Override
public void updateFaultItem(final String name, final long currentLatency, final long notAvailableDuration) {
FaultItem old = this.faultItemTable.get(name);
if (null == old) {
final FaultItem faultItem = new FaultItem(name);
faultItem.setCurrentLatency(currentLatency);
//可以看到 下次可用的时间 = 现在的时间 + 延迟的时间
faultItem.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration); old = this.faultItemTable.putIfAbsent(name, faultItem);
if (old != null) {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
} else {
old.setCurrentLatency(currentLatency);
old.setStartTimestamp(System.currentTimeMillis() + notAvailableDuration);
}
} /**
* 尝试从规避的broker 中选择一个可用的较好的 broker ,如果没找到,将返回 null
* 哪一个较好呢? 肯定是规避时间较短的
*/
@Override
public String pickOneAtLeast() {
final Enumeration<FaultItem> elements = this.faultItemTable.elements();
List<FaultItem> tmpList = new LinkedList<FaultItem>();
while (elements.hasMoreElements()) {
final FaultItem faultItem = elements.nextElement();
tmpList.add(faultItem);
} if (!tmpList.isEmpty()) {
//洗牌后又排序,这里看不懂为啥要洗牌后排序,为什么不直接排序?
Collections.shuffle(tmpList);
//排序的根据看 FaultItem 的 compareTo 方法,根据时间排序
Collections.sort(tmpList); final int half = tmpList.size() / 2;
if (half <= 0) {
return tmpList.get(0).getName();
} else {
// whichItemWorst 记录本次选取的 broker ,加若选去后还是发送失败,那么同个线程下次再次调用 pickOneAtLeast 的时候
//就会调用选用另外一个broker
final int i = this.whichItemWorst.getAndIncrement() % half;
return tmpList.get(i).getName();
}
} return null;
}
    // MQFaultStrategy#updateFaultItem
// 其中 currentLatency 是发送时长(发送花了多少时间)
public void updateFaultItem(final String brokerName, final long currentLatency, boolean isolation) {
if (this.sendLatencyFaultEnable) {
long duration = computeNotAvailableDuration(isolation ? 30000 : currentLatency);
this.latencyFaultTolerance.updateFaultItem(brokerName, currentLatency, duration);
}
} //计算出需要延迟的时长
private long computeNotAvailableDuration(final long currentLatency) {
for (int i = latencyMax.length - 1; i >= 0; i--) {
if (currentLatency >= latencyMax[i])
return this.notAvailableDuration[i];
} return 0;
} private long[] latencyMax = {50L, 100L, 550L, 1000L, 2000L, 3000L, 15000L};
private long[] notAvailableDuration = {0L, 0L, 30000L, 60000L, 120000L, 180000L, 600000L};

消息发送核心方法

  假如是一个异步发送方法

    public static void main(
String[] args) throws MQClientException, InterruptedException, UnsupportedEncodingException { DefaultMQProducer producer = new DefaultMQProducer("Jodie_Daily_test");
producer.start();
producer.setRetryTimesWhenSendAsyncFailed(0); for (int i = 0; i < 10000000; i++) {
try {
final int index = i;
Message msg = new Message("Jodie_topic_1023",
"TagA",
"OrderID188",
"Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET));
producer.send(msg, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId());
} @Override
public void onException(Throwable e) {
System.out.printf("%-10d Exception %s %n", index, e);
e.printStackTrace();
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
producer.shutdown();
}
DefaultMQProducerimpl#sendKernelImpl是消息发送的核心方法,我们看一下
    /**
*
* @param msg 发送的消息
* @param mq 选定的消息队列
* @param communicationMode 交互模式
* @param sendCallback 异步消息 callback 回调
* @param topicPublishInfo topic消息
* @param timeout 超时时长
* @return
* @throws MQClientException
* @throws RemotingException
* @throws MQBrokerException
* @throws InterruptedException
*/
private SendResult sendKernelImpl(final Message msg,
final MessageQueue mq,
final CommunicationMode communicationMode,
final SendCallback sendCallback,
final TopicPublishInfo topicPublishInfo,
final long timeout) throws MQClientException, RemotingException, MQBrokerException, InterruptedException {
//No.1 获取地址
String brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
if (null == brokerAddr) {
tryToFindTopicPublishInfo(mq.getTopic());
brokerAddr = this.mQClientFactory.findBrokerAddressInPublish(mq.getBrokerName());
} SendMessageContext context = null;
if (brokerAddr != null) {
//
brokerAddr = MixAll.brokerVIPChannel(this.defaultMQProducer.isSendMessageWithVIPChannel(), brokerAddr); byte[] prevBody = msg.getBody();
try {
//for MessageBatch,ID has been set in the generating process
// 为消息分配全局唯一 ID ,如果消息体默认超过 4K ,会对消息采用 zip 压缩,设置标识位
if (!(msg instanceof MessageBatch)) {
MessageClientIDSetter.setUniqID(msg);
} int sysFlag = 0;
if (this.tryToCompressMessage(msg)) {
sysFlag |= MessageSysFlag.COMPRESSED_FLAG;
} final String tranMsg = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (tranMsg != null && Boolean.parseBoolean(tranMsg)) {
sysFlag |= MessageSysFlag.TRANSACTION_PREPARED_TYPE;
} // 如果有钩子函数,执行钩子函数
if (hasCheckForbiddenHook()) {
CheckForbiddenContext checkForbiddenContext = new CheckForbiddenContext();
checkForbiddenContext.setNameSrvAddr(this.defaultMQProducer.getNamesrvAddr());
checkForbiddenContext.setGroup(this.defaultMQProducer.getProducerGroup());
checkForbiddenContext.setCommunicationMode(communicationMode);
checkForbiddenContext.setBrokerAddr(brokerAddr);
checkForbiddenContext.setMessage(msg);
checkForbiddenContext.setMq(mq);
checkForbiddenContext.setUnitMode(this.isUnitMode());
this.executeCheckForbiddenHook(checkForbiddenContext);
} if (this.hasSendMessageHook()) {
context = new SendMessageContext();
context.setProducer(this);
context.setProducerGroup(this.defaultMQProducer.getProducerGroup());
context.setCommunicationMode(communicationMode);
context.setBornHost(this.defaultMQProducer.getClientIP());
context.setBrokerAddr(brokerAddr);
context.setMessage(msg);
context.setMq(mq);
String isTrans = msg.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (isTrans != null && isTrans.equals("true")) {
context.setMsgType(MessageType.Trans_Msg_Half);
} if (msg.getProperty("__STARTDELIVERTIME") != null || msg.getProperty(MessageConst.PROPERTY_DELAY_TIME_LEVEL) != null) {
context.setMsgType(MessageType.Delay_Msg);
}
this.executeSendMessageHookBefore(context);
} //==================== 构造request请求体 ================== SendMessageRequestHeader requestHeader = new SendMessageRequestHeader();
requestHeader.setProducerGroup(this.defaultMQProducer.getProducerGroup());
requestHeader.setTopic(msg.getTopic());
requestHeader.setDefaultTopic(this.defaultMQProducer.getCreateTopicKey());
requestHeader.setDefaultTopicQueueNums(this.defaultMQProducer.getDefaultTopicQueueNums());
requestHeader.setQueueId(mq.getQueueId());
requestHeader.setSysFlag(sysFlag);
requestHeader.setBornTimestamp(System.currentTimeMillis());
requestHeader.setFlag(msg.getFlag());
requestHeader.setProperties(MessageDecoder.messageProperties2String(msg.getProperties()));
requestHeader.setReconsumeTimes(0);
requestHeader.setUnitMode(this.isUnitMode());
requestHeader.setBatch(msg instanceof MessageBatch);
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
String reconsumeTimes = MessageAccessor.getReconsumeTime(msg);
if (reconsumeTimes != null) {
requestHeader.setReconsumeTimes(Integer.valueOf(reconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_RECONSUME_TIME);
} String maxReconsumeTimes = MessageAccessor.getMaxReconsumeTimes(msg);
if (maxReconsumeTimes != null) {
requestHeader.setMaxReconsumeTimes(Integer.valueOf(maxReconsumeTimes));
MessageAccessor.clearProperty(msg, MessageConst.PROPERTY_MAX_RECONSUME_TIMES);
}
} SendResult sendResult = null; //==================== 构造request请求体 ================== //(重要)根据不同的交互模式,发送请求,核心发送逻辑
switch (communicationMode) {
case ASYNC:
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
msg,
requestHeader,
timeout,
communicationMode,
sendCallback,
topicPublishInfo,
this.mQClientFactory,
this.defaultMQProducer.getRetryTimesWhenSendAsyncFailed(),
context,
this);
break;
case ONEWAY:
case SYNC:
sendResult = this.mQClientFactory.getMQClientAPIImpl().sendMessage(
brokerAddr,
mq.getBrokerName(),
msg,
requestHeader,
timeout,
communicationMode,
context,
this);
break;
default:
assert false;
break;
} if (this.hasSendMessageHook()) {
context.setSendResult(sendResult);
this.executeSendMessageHookAfter(context);
} return sendResult;
....
....
(异常处理) }

底层发送

  MQ 客户端发送消息的入口是 MQClientAPIImpl#sendMessage,MQClientAPIImpl 持有 RemotingClient 字段,它是个接口,实现类是 NettyRemotingClient ,即是它就是真正执行发送的对象,请求命令是 RequestCode.SEND_MESSAGE,我们可以找到该命令的处理类: org . apache.rocketmq. broker.processor.SendMessageProcessor。人口方法在 SendMessageProcessor#sendMessage。

    //SendMessageProcessor#sendMessage 

    private RemotingCommand sendMessage(final ChannelHandlerContext ctx,
final RemotingCommand request,
final SendMessageContext sendMessageContext,
final SendMessageRequestHeader requestHeader) throws RemotingCommandException { final RemotingCommand response = RemotingCommand.createResponseCommand(SendMessageResponseHeader.class);
final SendMessageResponseHeader responseHeader = (SendMessageResponseHeader) response.readCustomHeader(); response.setOpaque(request.getOpaque()); response.addExtField(MessageConst.PROPERTY_MSG_REGION, this.brokerController.getBrokerConfig().getRegionId());
response.addExtField(MessageConst.PROPERTY_TRACE_SWITCH, String.valueOf(this.brokerController.getBrokerConfig().isTraceOn())); log.debug("receive SendMessage request command, {}", request); final long startTimstamp = this.brokerController.getBrokerConfig().getStartAcceptSendRequestTimeStamp();
if (this.brokerController.getMessageStore().now() < startTimstamp) {
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark(String.format("broker unable to service, until %s", UtilAll.timeMillisToHumanString2(startTimstamp)));
return response;
} response.setCode(-1);
// No.1 先检查
super.msgCheck(ctx, requestHeader, response);
if (response.getCode() != -1) {
return response;
} final byte[] body = request.getBody(); int queueIdInt = requestHeader.getQueueId();
TopicConfig topicConfig = this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic()); if (queueIdInt < 0) {
queueIdInt = Math.abs(this.random.nextInt() % 99999999) % topicConfig.getWriteQueueNums();
} MessageExtBrokerInner msgInner = new MessageExtBrokerInner();
msgInner.setTopic(requestHeader.getTopic());
msgInner.setQueueId(queueIdInt);
// 如果消息重试次数超过允许的最大重试次数,消息将进入DLD延迟队列
if (!handleRetryAndDLQ(requestHeader, response, request, msgInner, topicConfig)) {
return response;
} msgInner.setBody(body);
msgInner.setFlag(requestHeader.getFlag());
MessageAccessor.setProperties(msgInner, MessageDecoder.string2messageProperties(requestHeader.getProperties()));
msgInner.setPropertiesString(requestHeader.getProperties());
msgInner.setBornTimestamp(requestHeader.getBornTimestamp());
msgInner.setBornHost(ctx.channel().remoteAddress());
msgInner.setStoreHost(this.getStoreHost());
msgInner.setReconsumeTimes(requestHeader.getReconsumeTimes() == null ? 0 : requestHeader.getReconsumeTimes()); if (this.brokerController.getBrokerConfig().isRejectTransactionMessage()) {
String traFlag = msgInner.getProperty(MessageConst.PROPERTY_TRANSACTION_PREPARED);
if (traFlag != null) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark(
"the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1() + "] sending transaction message is forbidden");
return response;
}
} //(重要)进行消息存储,后续分析
PutMessageResult putMessageResult = this.brokerController.getMessageStore().putMessage(msgInner); return handlePutMessageResult(putMessageResult, response, request, msgInner, responseHeader, sendMessageContext, ctx, queueIdInt); }
    //

    protected RemotingCommand msgCheck(final ChannelHandlerContext ctx,
final SendMessageRequestHeader requestHeader, final RemotingCommand response) {
// 检查是否可写
if (!PermName.isWriteable(this.brokerController.getBrokerConfig().getBrokerPermission())
&& this.brokerController.getTopicConfigManager().isOrderTopic(requestHeader.getTopic())) {
response.setCode(ResponseCode.NO_PERMISSION);
response.setRemark("the broker[" + this.brokerController.getBrokerConfig().getBrokerIP1()
+ "] sending message is forbidden");
return response;
}
// 检查该Topic是否可以进行消息发送
if (!this.brokerController.getTopicConfigManager().isTopicCanSendMessage(requestHeader.getTopic())) {
String errorMsg = "the topic[" + requestHeader.getTopic() + "] is conflict with system reserved words.";
log.warn(errorMsg);
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark(errorMsg);
return response;
} //在 NameServer端存储主题的配置信息
TopicConfig topicConfig =
this.brokerController.getTopicConfigManager().selectTopicConfig(requestHeader.getTopic());
if (null == topicConfig) {
int topicSysFlag = 0;
if (requestHeader.isUnitMode()) {
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
topicSysFlag = TopicSysFlag.buildSysFlag(false, true);
} else {
topicSysFlag = TopicSysFlag.buildSysFlag(true, false);
}
} log.warn("the topic {} not exist, producer: {}", requestHeader.getTopic(), ctx.channel().remoteAddress());
topicConfig = this.brokerController.getTopicConfigManager().createTopicInSendMessageMethod(
requestHeader.getTopic(),
requestHeader.getDefaultTopic(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel()),
requestHeader.getDefaultTopicQueueNums(), topicSysFlag); if (null == topicConfig) {
if (requestHeader.getTopic().startsWith(MixAll.RETRY_GROUP_TOPIC_PREFIX)) {
topicConfig =
this.brokerController.getTopicConfigManager().createTopicInSendMessageBackMethod(
requestHeader.getTopic(), 1, PermName.PERM_WRITE | PermName.PERM_READ,
topicSysFlag);
}
} if (null == topicConfig) {
response.setCode(ResponseCode.TOPIC_NOT_EXIST);
response.setRemark("topic[" + requestHeader.getTopic() + "] not exist, apply first please!"
+ FAQUrl.suggestTodo(FAQUrl.APPLY_TOPIC_URL));
return response;
}
}
// 检查队列,如果队列不合法,返回错误码
int queueIdInt = requestHeader.getQueueId();
int idValid = Math.max(topicConfig.getWriteQueueNums(), topicConfig.getReadQueueNums());
if (queueIdInt >= idValid) {
String errorInfo = String.format("request queueId[%d] is illegal, %s Producer: %s",
queueIdInt,
topicConfig.toString(),
RemotingHelper.parseChannelRemoteAddr(ctx.channel())); log.warn(errorInfo);
response.setCode(ResponseCode.SYSTEM_ERROR);
response.setRemark(errorInfo); return response;
}
return response;
}

总结

  发送的逻辑我们可以归纳一下

  1. 创建 DefaultProducer,启动其 start 方法
  2. send 方法调用会,利用 DefaultProducer 内部的 DefaultProducerImpl 执行发送操作,DefaultProducerImpl 使用 MQClient 执行发送,这中间有个broker 故障延迟发送机制,最后到了MQClientAPIImpl 的发送方法,MQClientAPIImpl 使用 RemotingClient 发送command 的,消息检查,最后到达DefaultMessageStore#putMessage 进行消息存储 。
      再有一个,我们知道了brokerController 和 NamerServerController 这两个类存放这各种重要的信息,使得消息在发送模块等其他模块(只要该模块持有controller)就可以方法得到各种消息。
      同时类在设计的时候通过设计接口,功能扩展封装在抽象类中,而实际使用不要持有最终实现,而是持有接口类,方便以后扩展。

参考资料

  • 《RocketMQ技术内幕》

消息队列(四)--- RocketMQ-消息发送2的更多相关文章

  1. 消息队列之事务消息,RocketMQ 和 Kafka 是如何做的?

    每个时代,都不会亏待会学习的人. 大家好,我是 yes. 今天我们来谈一谈消息队列的事务消息,一说起事务相信大家都不陌生,脑海里蹦出来的就是 ACID. 通常我们理解的事务就是为了一些更新操作要么都成 ...

  2. 消息队列之-RocketMQ入门

    简介 RocketMQ是阿里开源的消息中间件,目前已经捐献个Apache基金会,它是由Java语言开发的,具备高吞吐量.高可用性.适合大规模分布式系统应用等特点,经历过双11的洗礼,实力不容小觑. 官 ...

  3. 剖析nsq消息队列(四) 消息的负载处理

    剖析nsq消息队列-目录 实际应用中,一部分服务集群可能会同时订阅同一个topic,并且处于同一个channel下.当nsqd有消息需要发送给订阅客户端去处理时,发给哪个客户端是需要考虑的,也就是我要 ...

  4. 分布式消息队列RocketMQ--事务消息--解决分布式事务

    说到分布式事务,就会谈到那个经典的”账号转账”问题:2个账号,分布处于2个不同的DB,或者说2个不同的子系统里面,A要扣钱,B要加钱,如何保证原子性? 一般的思路都是通过消息中间件来实现“最终一致性” ...

  5. 几种MQ消息队列对比与消息队列之间的通信问题

    消息队列 开发语言 协议支持 设计模式 持久化支持 事务支持 负载均衡支持 功能特点 缺点 RabbitMQ Erlang AMQP,XMPP,SMTP,STOMP 代理(Broker)模式(消息在发 ...

  6. 阿里消息队列中间件 RocketMQ源码解析:Message发送&接收

  7. [分布式学习]消息队列之rocketmq笔记

    文档地址 RocketMQ架构 哔哩哔哩上的视频 mq有很多,近期买了<分布式消息中间件实践>这本书,学习关于mq的相关知识.mq大致有有4个功能: 异步处理.比如业务端需要给用户发送邮件 ...

  8. 消息队列之--RocketMQ

    序言 资料 https://github.com/alibaba/RocketMQ http://rocketmq.apache.org/

  9. 消息队列中间件 RocketMQ 源码分析 —— Message 存储

  10. 阿里消息队列中间件 RocketMQ 源码分析 —— Message 拉取与消费(上)

随机推荐

  1. Big research problems (1)

    1. how to measure the uncertainty of prediction model or data analysis? 2.

  2. 根下var目录迁移

    问题 var目录挂载在根目录下,占用/分区的空间,如果/var/log日志空间过大,将导致/分区空间满,导致系统无法使用. 解决方案 在硬盘上划分单独的分区,挂载到/var目录,并迁移/var目录下的 ...

  3. 实用sql语句合集

    1. 将选取A表的name字段  然后选择A表和B表,最后进行id相等比较 最终得到的是合集 $res = \DB::select("SELECT name FROM users,car_a ...

  4. istio部署-istio prometheus

    参考 fleeto/sleep fleeto/flaskapp 1. 使用 Prometheus 1.1 访问 Prometheus 1.1.1 端口转发 Prometheus 服务默认启用. # o ...

  5. 钉钉内网穿透工具在windows的使用。

    钉钉内网穿透工具在windows环境下使用 1.WIN+R,然后cmd,调出dos控制台 2.进入内网穿透程序ding.exe所在目录 3.执行 ./ding.exe -config=ding.cfg ...

  6. vue updated

    updated方法的触发条件———— 当data中定义的数据有变化时就会加载updated方法

  7. Django_模型

    1. ORM 2. 简单使用 3. 外键 2.0以上的版本要这样写s_grade = models.ForeignKey(Grade,on_delete=models.CASCADE) 3. 修改表名 ...

  8. LED Decorative Light Supplier - Decorative Use Of LED Light Strips

    Led strip refers to the led assembly in the ribbon of the FPC (flexible circuit board) or PCB hard b ...

  9. mysql忘记密码,更改密码

    对MySQL有研究的读者,可能会发现MySQL更新很快,在安装方式上,MySQL提供了两种经典安装方式:解压式和一键式,虽然是两种安装方式,但我更提倡选择解压式安装,不仅快,还干净.在操作系统上,My ...

  10. hdu 1045 Fire Net(二分图)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=1045 题目大意为给定一个最大为4*4的棋盘,棋盘可以放置堡垒,处在同一行或者同一列的堡垒可以相互攻击, ...