前几节我们了解了可靠消息的基本架构:
但是现成的MQ中间件产品并不支持消息发送一致性(先进存储,再被确认后才能发送的2步式流程),直接改造MQ的难度又太大,所以我们进行了自己的改进。
可靠消息最终一致性解决方案1----本地消息服务
- 将主动方应用系统中的业务操作和消息存储与发送都放到主动方应用系统中,消息存储服务不再是放到消息中间件中
- 将主动方应用系统的业务操作和消息存储与发送都放在同一个本地事务当中,保证了业务处理操作和消息存储与发送功能要么同时成功,要么同时失败,这样子就保证了消息发送一致性的正向流程
- 如果业务操作和消息存储等功能都是成功的,就会将要发送给被动方应用系统的消息发送给实时消息服务(消息中间件,MQ/MQ集群)
- 当消息中间件有数据的时候,由于消息消费端应用一直监听消息中间件,所以有数据的时候,他会有感知,消息消费端应用就会消费消息,消费的时候会调用被动方应用系统的业务接口。
- 被动方进行业务处理完成后,会有一个结果反馈(被动方业务处理成功/失败)给消息消费端应用
- 接收到的消息可能是成功/失败
(1)成功:实时消息服务接收到消息消费端应用发送来的成功的消息,清除掉实时消息服务中的消息队列的对应消息。消息消费端应用告知主动方应用系统操作成功,主动方应用删除本地数据库中存储的消息数据。
(2)失败:利用消息恢复系统进行处理。 - 消息恢复系统提供了重试机制,若主动方应用长时间没有接收到消息的确认,则我们消息恢复系统重新进行查询,将消息重新放到实时消息服务中,重复过程。
- 消息的持久化因为MQ一般没有持久化的功能,所以我们放在了主动方应用的本地数据库中存储,保证了消息的可靠性。
优点:
(1)消息的时效性比较高
(2)从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于MQ中间件,弱化了MQ中间件特性的依赖
(3)方案轻量,容易实现。
缺点:
(1)与具体的业务场景绑定,耦合性强,不可公用
(2)消息数据与业务数据同库,占用业务系统资源
(3)业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。
可靠消息最终一致性解决方案2----独立消息服务的设计
(1)主动方应用的业务处理在一个本地的事务域当中
(2)消息服务子系统所有的消息处理在一个本地事务域当中
(3)被动方应用系统的业务处理在本地事务域当中
(4)主动方应用先预发送消息,消息服务子系统存储主动方应用发送来的预发送消息,此时该消息不能被消费,如果消息服务子系统存储失败,则返回失败信息,主动方业务处理不执行;如果消息服务子系统预发送消息存储成功,则此时返回给主动方应用一个成功的标志位,主动方应用开始执行业务操作,并将结果发送给消息服务子系统,消息服务子系统确认并发送消息(将存储的预发送消息状态改为可发送),消息服务子系统更细完消息状态后,将消息发送到实时消息服务中(传到MQ中),此时实时消息服务中有可发送的消息,消息业务消费端监听到要发送的消息,将其消费,与消息业务消费端进行调用,完成被动方的业务处理。
(5)如果主动方应用和消息服务中间件之间通讯失败:
- 消息服务子系统存储消息成功,但是返回给主动方应用系统的时候由于网络等原因失败
- 主动方接收到消息服务子系统成功,业务处理成功,但是返回给消息服务的子系统时候失败,此时消息服务子系统就没有收到业务处理的结果。
类似这种异常流程,我们可以依赖消息状态确认的子系统来进行处理,消息状态确认子系统就是一个定时任务,会定期按规则查询状态确认超时消息,查询主动方应用业务处理成功但是没有返回给消息服务子系统的业务数据,将这些数据重新给到消息服务子系统中。如果主动方业务执行的消息是失败的,则消息状态确认子系统会通知消息服务对消息删除。
(6)当实时消息服务有消息的时候,消息业务消费端会监听其是否有消息,若有消息,将消息给被动方应用,被动方应用利用该消息进行处理,若处理成功,消息业务消费端就会像实时消息服务发送ack表示操作成功,该服务就会删除MQ中的消息。同时消息业务消费端也会通过消息恢复子系统调用消息服务子系统中确认消息已经被成功消费的接口,将消息状态改为已经消费,或者直接从消息服务库中删除
(7)消息恢复子系统也会定期查询消息服务子系统中消息已经发送给被动方应用,但是没有被确认的消息,将这些消息重新投递。
优点:
- 消息服务独立部署,独立维护,独立伸缩
- 消息存储可以按照需求选择不同的数据库来实现
- 消息服务可以被相同的使用场景公用,降低重复建设消息服务的成本
- 降低了耦合,有利于系统的维护
代码实现:
首先我们看下基本架构:
可以看到我们将消息服务子系统作为接口(roncoo-pay-service-message-api)和具体的实现(roncoo-pay-service-message)
- 其api接口定义的方法:
public interface RpTransactionMessageService {
/**
* 预存储消息.
*/
public int saveMessageWaitingConfirm(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 确认并发送消息.
*/
public void confirmAndSendMessage(String messageId) throws MessageBizException;
/**
* 存储并发送消息.
*/
public int saveAndSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 直接发送消息.
*/
public void directSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 重发消息.
*/
public void reSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
/**
* 根据messageId重发某条消息.
*/
public void reSendMessageByMessageId(String messageId) throws MessageBizException;
/**
* 将消息标记为死亡消息.
*/
public void setMessageToAreadlyDead(String messageId) throws MessageBizException;
/**
* 根据消息ID获取消息
*/
public RpTransactionMessage getMessageByMessageId(String messageId) throws MessageBizException;
/**
* 根据消息ID删除消息
*/
public void deleteMessageByMessageId(String messageId) throws MessageBizException;
/**
* 重发某个消息队列中的全部已死亡的消息.
*/
public void reSendAllDeadMessageByQueueName(String queueName, int batchSize) throws MessageBizException;
/**
* 获取分页数据
*/
PageBean listPage(PageParam pageParam, Map<String, Object> paramMap) throws MessageBizException;
}
我们来依次分析上面的方法,看其具体的代码实现
- 保存预存储消息
/**
* 预存储消息.
*/
public int saveMessageWaitingConfirm(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
上面的这个方法的作用是保存预发送的消息,就是主动方应用进行的第一步操作,将预发送消息发送给消息服务子系统
具体实现:
public int saveMessageWaitingConfirm(RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息为空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消费队列不能为空 ");
}
message.setEditTime(new Date());
message.setStatus(MessageStatusEnum.WAITING_CONFIRM.name());
message.setAreadlyDead(PublicEnum.NO.name());
message.setMessageSendTimes(0);
return rpTransactionMessageDao.insert(message);
}
- 确认并发送消息
/**
* 确认并发送消息.
*/
public void confirmAndSendMessage(String messageId) throws MessageBizException;
该方法是用来在主动方业务执行完成后,将消息发送给消息服务子系统,消息服务子系统接受消息,更改消息状态,并将消息发送到实时消息服务。
public void confirmAndSendMessage(String messageId) {
final RpTransactionMessage message = getMessageByMessageId(messageId);
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "根据消息id查找的消息为空");
}
// 消息状态改变为发送中
message.setStatus(MessageStatusEnum.SENDING.name());
message.setEditTime(new Date());
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
上面的方法的作用就是根据消息id,查询消息服务子系统中保存的消息信息。将其状态进行改变,并将该消息发送到实时消息服务。
- 存储并发送消息
/**
* 存储并发送消息.
*/
public int saveAndSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
其具体的实现:
public int saveAndSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息为空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消费队列不能为空 ");
}
// 消息状态改为发送中
message.setStatus(MessageStatusEnum.SENDING.name());
// 表示该消息未死亡
message.setAreadlyDead(PublicEnum.NO.name());
// 表示发送次数(重试次数)
message.setMessageSendTimes(0);
// 消息发送的时间
message.setEditTime(new Date());
// 保存数据
int result = rpTransactionMessageDao.insert(message);
// 推送给实时消息服务
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
return result;
}
该方法的作用是直接将主动方应用发送来的消息(预发送消息),将其状态改为发送中,这个方法是将预存储消息和确认并发送消息两个方法的合并
- 直接发送消息
/**
* 直接发送消息.
*/
public void directSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
具体实现:
public void directSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息为空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消费队列不能为空 ");
}
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
该方法是讲主动方业务发送给消息服务子系统中的消息直接发送给实时消息服务,消息服务子系统并不做任何的存储。
- 重新发送消息
/**
* 重发消息.
*/
public void reSendMessage(RpTransactionMessage rpTransactionMessage) throws MessageBizException;
具体实现:
public void reSendMessage(final RpTransactionMessage message) {
if (message == null) {
throw new MessageBizException(MessageBizException.SAVA_MESSAGE_IS_NULL, "保存的消息为空");
}
if (StringUtil.isEmpty(message.getConsumerQueue())) {
throw new MessageBizException(MessageBizException.MESSAGE_CONSUMER_QUEUE_IS_NULL, "消息的消费队列不能为空 ");
}
message.addSendTimes();
message.setEditTime(new Date());
rpTransactionMessageDao.update(message);
notifyJmsTemplate.setDefaultDestinationName(message.getConsumerQueue());
notifyJmsTemplate.send(new MessageCreator() {
public Message createMessage(Session session) throws JMSException {
return session.createTextMessage(message.getMessageBody());
}
});
}
当被动方应用系统执行失败并未返回的时候,消息业务消费端就会调用该方法,使其重新发送消息,让其重新执行,查询状态确认超时的消息,以及查询消费确认超时的信息。
- 其他方法
/**
* 根据messageId重发某条消息.
*/
public void reSendMessageByMessageId(String messageId) throws MessageBizException;
/**
* 将消息标记为死亡消息.
*/
public void setMessageToAreadlyDead(String messageId) throws MessageBizException;
/**
* 根据消息ID获取消息
*/
public RpTransactionMessage getMessageByMessageId(String messageId) throws MessageBizException;
/**
* 根据消息ID删除消息
*/
public void deleteMessageByMessageId(String messageId) throws MessageBizException;
/**
* 重发某个消息队列中的全部已死亡的消息.
*/
public void reSendAllDeadMessageByQueueName(String queueName, int batchSize) throws MessageBizException;
/**
* 获取分页数据
*/
PageBean listPage(PageParam pageParam, Map<String, Object> paramMap) throws MessageBizException;
主要作用是对消息服务子系统中的数据进行简单的管理。
架构图如下:
其实消息管理子系统就相当于是MQ中的管理客户端页面。这里就是对消息服务子系统中存储在其数据库中的数据的相关操作。
实际效果图:
可以在此选择一个队列中的数据,对其进行消息的重新发送。
- 该子系统的作用是:当消息服务子系统中存储的是预发送消息,长时间未接收到主动方业务给我们传递的业务结果,我们无法将消息的状态进行改变,无法确认并发送消息。
- 代码实现:
我们将消息状态确认子系统的功能做成一个定时的功能。因为我们需要不定时的查看消息服务子系统中是否存在未确认的消息,若存在需要根据该消息id在主动方应用中查询该业务是否执行成功,若成功则调用消息服务子系统的确认并发送方法,否则删除其消息服务子系统中的存储的数据。
该服务跟业务逻辑有耦合,难以做到通用。
- 定时任务代码
public class MessageTask {
private static final Log log = LogFactory.getLog(MessageTask.class);
private MessageTask() {
}
public static void main(String[] args) {
try {
@SuppressWarnings("resource")
final ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[] { "spring-context.xml" });
context.start();
log.info("定时任务开始执行>>>");
final MessageScheduled settScheduled = (MessageScheduled) context.getBean("messageScheduled");
ThreadPoolTaskExecutor threadPool = (ThreadPoolTaskExecutor) context.getBean("threadPool");
// 开一个子线程处理状态为“待确认”但已超时的消息.
threadPool.execute(new Runnable() {
@Override
public void run() {
while (true) {
log.info("执行(处理[waiting_confirm]状态的消息)任务开始");
settScheduled.handleWaitingConfirmTimeOutMessages();
log.info("执行(处理[waiting_confirm]状态的消息)任务结束");
try {
log.info("[waiting_confirm]睡眠60秒");
Thread.sleep(60000);
} catch (InterruptedException e) {
}
}
}
});
// 开一个子线程处理状态为“发送中”但超时没有被成功消费确认的消息
threadPool.execute(new Runnable() {
@Override
public void run() {
while (true) {
log.info("执行(处理[SENDING]的消息)任务开始");
settScheduled.handleSendingTimeOutMessage();
log.info("执行(处理[SENDING]的消息)任务结束");
try {
log.info("[SENDING]睡眠60秒");
Thread.sleep(60000);
} catch (InterruptedException e) {
}
}
}
});
} catch (Exception e) {
log.error("===>DubboReference context start error:", e);
}
}
}
我们这里主要做了开启定时任务,开启多线程,然后处理状态是待确认但是已经超时的消息。
1.1 开一个子线程处理状态为“待确认”但已超时的消息.
该方法处理的是主动方应用未及时返回给消息服务器
/**
* 处理状态为“待确认”但已超时的消息.
*/
public void handleWaitingConfirmTimeOutMessages() {
try {
int numPerPage = 2000; // 每页条数
int maxHandlePageCount = 3; // 一次最多处理页数
Map<String, Object> paramMap = new HashMap<String, Object>(); // 查询条件
//paramMap.put("consumerQueue", queueName); // 队列名(可以按不同业务队列分开处理)
paramMap.put("listPageSortType", "ASC"); // 分页查询的排序方式,正向排序
// 该时间是当前时间减去当时接受到消息的时间
String dateStr = getCreateTimeBefore();
paramMap.put("createTimeBefore", dateStr);// 取存放了多久的消息
paramMap.put("status", MessageStatusEnum.WAITING_CONFIRM.name());// 取状态为“待确认”的消息
Map<String, RpTransactionMessage> messageMap = getMessageMap(numPerPage, maxHandlePageCount, paramMap);
messageBiz.handleWaitingConfirmTimeOutMessages(messageMap);
} catch (Exception e) {
log.error("处理[waiting_confirm]状态的消息异常" + e);
}
}
上面的方法准备一个map,里面存放了消息的状态(这里我们取的状态是待确认)以及休息已经等待的时间(还未确认的消息),调用handleWaitingConfirmTimeOutMessages(messageMap)方法
/**
* 处理[waiting_confirm]状态的消息
*
* @param messages
*/
public void handleWaitingConfirmTimeOutMessages(Map<String, RpTransactionMessage> messageMap) {
log.debug("开始处理[waiting_confirm]状态的消息,总条数[" + messageMap.size() + "]");
// 单条消息处理(目前该状态的消息,消费队列全部是accounting,如果后期有业务扩充,需做队列判断,做对应的业务处理。)
for (Map.Entry<String, RpTransactionMessage> entry : messageMap.entrySet()) {
RpTransactionMessage message = entry.getValue();
try {
log.debug("开始处理[waiting_confirm]消息ID为[" + message.getMessageId() + "]的消息");
String bankOrderNo = message.getField1();
RpTradePaymentRecord record = rpTradePaymentQueryService.getRecordByBankOrderNo(bankOrderNo);
// 如果订单成功,把消息改为待处理,并发送消息
if (TradeStatusEnum.SUCCESS.name().equals(record.getStatus())) {
// 确认并发送消息
rpTransactionMessageService.confirmAndSendMessage(message.getMessageId());
} else if (TradeStatusEnum.WAITING_PAYMENT.name().equals(record.getStatus())) {
// 订单状态是等到支付,可以直接删除数据
log.debug("订单没有支付成功,删除[waiting_confirm]消息id[" + message.getMessageId() + "]的消息");
rpTransactionMessageService.deleteMessageByMessageId(message.getMessageId());
}
log.debug("结束处理[waiting_confirm]消息ID为[" + message.getMessageId() + "]的消息");
} catch (Exception e) {
log.error("处理[waiting_confirm]消息ID为[" + message.getMessageId() + "]的消息异常:", e);
}
}
}
上面的方法重要的是取到消息的标识符,在主动方服务系统中查找该消息对应的业务是否执行完成,若主动方应用对应的业务已经执行成功则直接调用消息服务子系统的确认并发送方法。若主动方应用业务并没有执行成功,则直接删除消息服务子系统中的消息信息。
-
消息恢复子系统的作用:
主动方业务确认后的消息存放到消息服务子系统中后,消息服务子系统中存放的数据是必须被消费的,所以消息恢复子系统的作用就是确保消息服务子系统中的消息确保被使用,如果被动方应用消费消息失败,我们会利用消息恢复子系统进行消息的重新投递,让被动方可以再一次进行消息的消费。
被动方应用消息消费成功后,会调用消息业务服务端,消息业务服务端会发送标志,让消息服务子系统确认消息被成功消费,然后删除或者改变消息服务子系统中的消息。 -
开一个子线程处理状态为“发送中”但超时没有被成功消费确认的消息
// 开一个子线程处理状态为“发送中”但超时没有被成功消费确认的消息
threadPool.execute(new Runnable() {
@Override
public void run() {
while (true) {
log.info("执行(处理[SENDING]的消息)任务开始");
settScheduled.handleSendingTimeOutMessage();
log.info("执行(处理[SENDING]的消息)任务结束");
try {
log.info("[SENDING]睡眠60秒");
Thread.sleep(60000);
} catch (InterruptedException e) {
}
}
}
});
该方法处理的是被动方应用未将处理结果及时反馈给消息服务子系统
/**
* 处理状态为“发送中”但超时没有被成功消费确认的消息
*/
public void handleSendingTimeOutMessage() {
try {
int numPerPage = 2000; // 每页条数
int maxHandlePageCount = 3; // 一次最多处理页数
Map<String, Object> paramMap = new HashMap<String, Object>(); // 查询条件
//paramMap.put("consumerQueue", queueName); // 队列名(可以按不同业务队列分开处理)
paramMap.put("listPageSortType", "ASC"); // 分页查询的排序方式,正向排序
// 获取配置的开始处理的时间
String dateStr = getCreateTimeBefore();
paramMap.put("createTimeBefore", dateStr);// 取存放了多久的消息
paramMap.put("status", MessageStatusEnum.SENDING.name());// 取状态为发送中的消息
paramMap.put("areadlyDead", PublicEnum.NO.name());// 取存活的发送中消息
Map<String, RpTransactionMessage> messageMap = getMessageMap(numPerPage, maxHandlePageCount, paramMap);
messageBiz.handleSendingTimeOutMessage(messageMap);
} catch (Exception e) {
log.error("处理发送中的消息异常" + e);
}
}
处理发送中的消息第一步与处理待确认的消息的方法是一致的,都是取到消息待确认状态的时间(当前时间和规定的时间的差值),并且将状态(发送中的状态)存放到Map中。
调用handleSendingTimeOutMessage方法
/**
* 处理[SENDING]状态的消息
*
* @param messages
*/
public void handleSendingTimeOutMessage(Map<String, RpTransactionMessage> messageMap) {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
log.debug("开始处理[SENDING]状态的消息,总条数[" + messageMap.size() + "]");
// 根据配置获取通知间隔时间
Map<Integer, Integer> notifyParam = getSendTime();
// 单条消息处理
for (Map.Entry<String, RpTransactionMessage> entry : messageMap.entrySet()) {
RpTransactionMessage message = entry.getValue();
try {
log.debug("开始处理[SENDING]消息ID为[" + message.getMessageId() + "]的消息");
// 判断发送次数
int maxTimes = Integer.valueOf(PublicConfigUtil.readConfig("message.max.send.times"));
log.debug("[SENDING]消息ID为[" + message.getMessageId() + "]的消息,已经重新发送的次数[" + message.getMessageSendTimes() + "]");
// 如果超过最大发送次数直接退出
if (maxTimes < message.getMessageSendTimes()) {
// 标记为死亡
rpTransactionMessageService.setMessageToAreadlyDead(message.getMessageId());
continue;
}
// 判断是否达到发送消息的时间间隔条件
int reSendTimes = message.getMessageSendTimes();
int times = notifyParam.get(reSendTimes == 0 ? 1 : reSendTimes);
long currentTimeInMillis = Calendar.getInstance().getTimeInMillis();
long needTime = currentTimeInMillis - times * 60 * 1000;
long hasTime = message.getEditTime().getTime();
// 判断是否达到了可以再次发送的时间条件
if (hasTime > needTime) {
log.debug("currentTime[" + sdf.format(new Date()) + "],[SENDING]消息上次发送时间[" + sdf.format(message.getEditTime()) + "],必须过了[" + times + "]分钟才可以再发送。");
continue;
}
// 重新发送消息
rpTransactionMessageService.reSendMessage(message);
log.debug("结束处理[SENDING]消息ID为[" + message.getMessageId() + "]的消息");
} catch (Exception e) {
log.error("处理[SENDING]消息ID为[" + message.getMessageId() + "]的消息异常:", e);
}
}
}
该方法判断的是被动方 和消息服务子系统之间异常的处理情况:
(1)如果超过重试的最大次数,直接将消息服务子系统的该消息标记为死亡
(2)如果到达再次发送的条件,消息服务子系统向被动方重新发送消息
实时消息服务我们这里就用常用的MQ就行,实现解耦的作用 ,MQ还有自带的重试机制。
如果被动方业务处理失败,通过消息业务消费端,返回给一个失败的标志给消息服务子系统,此时会将消息服务系统中的该消息删除掉。这里只要成功返回被动方的业务处理结果就不会进入重试机制,重试机制,是指被动方应用执行完成后,长时间未返回其执行状态。