分布式事务解决方案之最大努力通知 下篇
背景:
订单完成支付,通知商户
商户系统接口必须实现幂等性
订单服务提供商户订单查询接口
流程:
消息生产端:
完成事件 -> 调用消息服务,发送消息
消息消费端:
接收消息 -> 调用通知服务(判断该消息未保存过,保存通知消息)-> 构建通知task (delayqueue通知队列) -> 调用消息服务确认消息
利用delayqueue 阻塞队列执行通知,通知失败后,如果还未超过通知最大次数,更新后通知信息,进入队列等待下次通知。必须实现重启服务加载未完成的通知
1.Create MySql Notify Table
DROP TABLE IF EXISTS `rp_notify_record`; CREATE TABLE `rp_notify_record` ( `id` varchar(50) NOT NULL DEFAULT '' COMMENT '主键ID', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本事情', `create_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间', `edit_time` datetime DEFAULT NULL COMMENT '最后修改时间', `notify_rule` varchar(255) DEFAULT NULL COMMENT '通知规则(单位:分钟)', `notify_times` int(11) NOT NULL DEFAULT '0' COMMENT '已通知次数', `limit_notify_times` int(11) NOT NULL DEFAULT '0' COMMENT '最大通知次数限制', `url` varchar(2000) NOT NULL DEFAULT '' COMMENT '通知请求链接(包含通知内容)', `merchant_order_no` varchar(50) NOT NULL DEFAULT '' COMMENT '商户订单号', `merchant_no` varchar(50) NOT NULL DEFAULT '' COMMENT '商户编号', `status` varchar(50) NOT NULL DEFAULT '' COMMENT '通知状态(对应枚举值)', `notify_type` varchar(30) DEFAULT NULL COMMENT '通知类型', PRIMARY KEY (`id`), KEY `AK_KEY_2` (`merchant_order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='通知记录表 RP_NOTIFY_RECORD';
DROP TABLE IF EXISTS `rp_notify_record_log`; CREATE TABLE `rp_notify_record_log` ( `id` varchar(50) NOT NULL DEFAULT '' COMMENT 'ID', `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号', `edit_time` datetime DEFAULT NULL COMMENT '最后修改时间', `create_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '创建时间', `notify_id` varchar(50) NOT NULL DEFAULT '' COMMENT '通知记录ID', `request` varchar(2000) NOT NULL DEFAULT '' COMMENT '请求内容', `response` varchar(2000) NOT NULL DEFAULT '' COMMENT '响应内容', `merchant_no` varchar(50) NOT NULL DEFAULT '' COMMENT '商户编号', `merchant_order_no` varchar(50) NOT NULL COMMENT '商户订单号', `http_status` varchar(50) NOT NULL COMMENT 'HTTP状态', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='通知记录日志表 RP_NOTIFY_RECORD_LOG';
2. Notify API(通知服务)
public class NotifyParam { /** * 通知参数(通知规则Map) */ private Map<Integer, Integer> notifyParams; /** * 通知后用于判断是否成功的返回值(成功标识),由HttpResponse获取 */ private String successValue; /** * 最大通知次数限制. * @return */ public Integer getMaxNotifyTimes() { // config } }
/** * 通知记录持久化类. */ @Service("notifyPersist") public class NotifyPersist { private static final Log LOG = LogFactory.getLog(NotifyPersist.class); @Autowired private RpNotifyService rpNotifyService; @Autowired private NotifyParam notifyParam; @Autowired private NotifyQueue notifyQueue; /** * 创建商户通知记录.<br/> * * @param notifyRecord * @return */ public long saveNotifyRecord(RpNotifyRecord notifyRecord) { //TODO } /** * 更新商户通知记录.<br/> * * @param id * @param notifyTimes * 通知次数.<br/> * @param status * 通知状态.<br/> * @return 更新结果 */ public void updateNotifyRord(String id, int notifyTimes, String status, Date editTime) { //TODO } /** * 创建商户通知日志记录.<br/> * * @param notifyId * 通知记录ID.<br/> * @param merchantNo * 商户编号.<br/> * @param merchantOrderNo * 商户订单号.<br/> * @param request * 请求信息.<br/> * @param response * 返回信息.<br/> * @param httpStatus * 通知状态(HTTP状态).<br/> * @return 创建结果 */ public long saveNotifyRecordLogs(String notifyId, String merchantNo, String merchantOrderNo, String request, String response, int httpStatus) { //TODO } /** * 从数据库中取一次数据用来当系统启动时初始化 */ public void initNotifyDataFromDB() { LOG.info("===>init get notify data from database"); //TODO } }
/** * 监听消费MQ队列中的消息. */ public void onMessage(Message message) { try { try { notifyPersist.saveNotifyRecord(notifyRecord); // 将获取到的通知先保存到数据库中 notifyQueue.addToNotifyTaskDelayQueue(notifyRecord); // 添加到通知队列(第一次通知) } catch (BizException e) { log.error("BizException :", e); } catch (Exception e) { log.error(e); } } catch (Exception e) { e.printStackTrace(); log.error(e); } }
public class NotifyQueue implements Serializable { /** * */ private static final long serialVersionUID = 1L; private static final Log LOG = LogFactory.getLog(NotifyQueue.class); @Autowired private NotifyParam notifyParam; /** * 将传过来的对象进行通知次数判断,决定是否放在任务队列中.<br/> * @param notifyRecord * @throws Exception */ public void addToNotifyTaskDelayQueue(RpNotifyRecord notifyRecord) { if (notifyRecord == null) { return; } LOG.info("===>addToNotifyTaskDelayQueue notify id:" + notifyRecord.getId()); Integer notifyTimes = notifyRecord.getNotifyTimes(); // 通知次数 Integer maxNotifyTimes = notifyRecord.getLimitNotifyTimes(); // 最大通知次数 if (notifyRecord.getNotifyTimes().intValue() == 0) { notifyRecord.setLastNotifyTime(new Date()); // 第一次发送(取当前时间) }else{ notifyRecord.setLastNotifyTime(notifyRecord.getEditTime()); // 非第一次发送(取上一次修改时间,也是上一次发送时间) } if (notifyTimes < maxNotifyTimes) { // 未超过最大通知次数,继续下一次通知 LOG.info("===>notify id:" + notifyRecord.getId() + ", 上次通知时间lastNotifyTime:" + DateUtils.formatDate(notifyRecord.getLastNotifyTime(), "yyyy-MM-dd HH:mm:ss SSS")); App.tasks.put(new NotifyTask(notifyRecord, this, notifyParam)); } } }
3. Notify Consumer (消费端)
/** * 通知任务类. */ public class NotifyTask implements Runnable, Delayed { private static final Log LOG = LogFactory.getLog(NotifyTask.class); private long executeTime; private RpNotifyRecord notifyRecord; private NotifyQueue notifyQueue; private NotifyParam notifyParam; private NotifyPersist notifyPersist = App.notifyPersist; public NotifyTask() { } public NotifyTask(RpNotifyRecord notifyRecord, NotifyQueue notifyQueue, NotifyParam notifyParam) { super(); this.notifyRecord = notifyRecord; this.notifyQueue = notifyQueue; this.notifyParam = notifyParam; this.executeTime = getExecuteTime(notifyRecord); } /** * 计算任务允许执行的开始时间(executeTime).<br/> * @param record * @return */ private long getExecuteTime(RpNotifyRecord record) { long lastNotifyTime = record.getLastNotifyTime().getTime(); // 最后通知时间(上次通知时间) Integer notifyTimes = record.getNotifyTimes(); // 已通知次数 LOG.info("===>notifyTimes:" + notifyTimes); //Integer nextNotifyTimeInterval = notifyParam.getNotifyParams().get(notifyTimes + 1); // 当前发送次数对应的时间间隔数(分钟数) Integer nextNotifyTimeInterval = record.getNotifyRuleMap().get(String.valueOf(notifyTimes + 1)); // 当前发送次数对应的时间间隔数(分钟数) long nextNotifyTime = (nextNotifyTimeInterval == null ? 0 : nextNotifyTimeInterval * 60 * 1000) + lastNotifyTime; LOG.info("===>notify id:" + record.getId() + ", nextNotifyTime:" + DateUtils.formatDate(new Date(nextNotifyTime), "yyyy-MM-dd HH:mm:ss SSS")); return nextNotifyTime; } /** * 比较当前时间(task.executeTime)与任务允许执行的开始时间(executeTime).<br/> * 如果当前时间到了或超过任务允许执行的开始时间,那么就返回-1,可以执行。 */ public int compareTo(Delayed o) { NotifyTask task = (NotifyTask) o; return executeTime > task.executeTime ? 1 : (executeTime < task.executeTime ? -1 : 0); } public long getDelay(TimeUnit unit) { return unit.convert(executeTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS); } /** * 执行通知处理. */ public void run() { Integer notifyTimes = notifyRecord.getNotifyTimes(); // 得到当前通知对象的通知次数 Integer maxNotifyTimes = notifyRecord.getLimitNotifyTimes(); // 最大通知次数 Date notifyTime = new Date(); // 本次通知的时间 // 去通知 try { LOG.info("===>notify url " + notifyRecord.getUrl()+", notify id:" + notifyRecord.getId()+", notifyTimes:" + notifyTimes); // 执行HTTP通知请求 SimpleHttpParam param = new SimpleHttpParam(notifyRecord.getUrl()); SimpleHttpResult result = SimpleHttpUtils.httpRequest(param); notifyRecord.setEditTime(notifyTime); // 取本次通知时间作为最后修改时间 notifyRecord.setNotifyTimes(notifyTimes + 1); // 通知次数+1 String successValue = notifyParam.getSuccessValue(); // 通知成功标识 String responseMsg = ""; Integer responseStatus = result.getStatusCode(); // 写通知日志表 notifyPersist.saveNotifyRecordLogs(notifyRecord.getId(), notifyRecord.getMerchantNo(), notifyRecord.getMerchantOrderNo(), notifyRecord.getUrl(), responseMsg, responseStatus); LOG.info("===>insert NotifyRecordLog, merchantNo:" + notifyRecord.getMerchantNo() + ", merchantOrderNo:" + notifyRecord.getMerchantOrderNo()); // 得到返回状态,如果是20X,也就是通知成功 if (responseStatus == 200 || responseStatus == 201 || responseStatus == 202 || responseStatus == 203 || responseStatus == 204 || responseStatus == 205 || responseStatus == 206) { responseMsg = result.getContent().trim(); responseMsg = responseMsg.length() >= 600 ? responseMsg.substring(0, 600) : responseMsg; // 避免异常日志过长 LOG.info("===>订单号: " + notifyRecord.getMerchantOrderNo() + " HTTP_STATUS:" + responseStatus + ",请求返回信息:" + responseMsg); // 通知成功,更新通知记录为已通知成功(以后不再通知) if (responseMsg.trim().equals(successValue)) { notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.SUCCESS.name(), notifyTime); return; } // 通知不成功(返回的结果不是success) if (notifyRecord.getNotifyTimes() < maxNotifyTimes) { // 判断是否超过重发次数,未超重发次数的,再次进入延迟发送队列 notifyQueue.addToNotifyTaskDelayQueue(notifyRecord); notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.HTTP_REQUEST_SUCCESS.name(), notifyTime); LOG.info("===>update NotifyRecord status to HTTP_REQUEST_SUCCESS, notifyId:" + notifyRecord.getId()); }else{ // 到达最大通知次数限制,标记为通知失败 notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.FAILED.name(), notifyTime); LOG.info("===>update NotifyRecord status to failed, notifyId:" + notifyRecord.getId()); } } else { // 其它HTTP响应状态码情况下 if (notifyRecord.getNotifyTimes() < maxNotifyTimes) { // 判断是否超过重发次数,未超重发次数的,再次进入延迟发送队列 notifyQueue.addToNotifyTaskDelayQueue(notifyRecord); notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.HTTP_REQUEST_FALIED.name(), notifyTime); LOG.info("===>update NotifyRecord status to HTTP_REQUEST_FALIED, notifyId:" + notifyRecord.getId()); }else{ // 到达最大通知次数限制,标记为通知失败 notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.FAILED.name(), notifyTime); LOG.info("===>update NotifyRecord status to failed, notifyId:" + notifyRecord.getId()); } } } catch (BizException e) { LOG.error("===>NotifyTask", e); } catch (Exception e) { // 异常 LOG.error("===>NotifyTask", e); notifyQueue.addToNotifyTaskDelayQueue(notifyRecord); // 判断是否超过重发次数,未超重发次数的,再次进入延迟发送队列 notifyPersist.updateNotifyRord(notifyRecord.getId(), notifyRecord.getNotifyTimes(), NotifyStatusEnum.HTTP_REQUEST_FALIED.name(), notifyTime); notifyPersist.saveNotifyRecordLogs(notifyRecord.getId(), notifyRecord.getMerchantNo(), notifyRecord.getMerchantOrderNo(), notifyRecord.getUrl(), "", 0); } } }
public static DelayQueue<NotifyTask> tasks = new DelayQueue<NotifyTask>(); private static void startThread() { LOG.info("==>startThread"); threadPool.execute(new Runnable() { public void run() { try { while (true) { LOG.info("==>threadPool.getActiveCount():" + threadPool.getActiveCount()); LOG.info("==>threadPool.getMaxPoolSize():" + threadPool.getMaxPoolSize()); // 如果当前活动线程等于最大线程,那么不执行 if (threadPool.getActiveCount() < threadPool.getMaxPoolSize()) { LOG.info("==>tasks.size():" + tasks.size()); final NotifyTask task = tasks.take(); //使用take方法获取过期任务,如果获取不到,就一直等待,知道获取到数据 if (task != null) { threadPool.execute(new Runnable() { public void run() { tasks.remove(task); task.run(); // 执行通知处理 LOG.info("==>tasks.size():" + tasks.size()); } }); } } } } catch (Exception e) { LOG.error("系统异常;",e); } } }); }
优化
1.可视化通知管理界面,手动重发
2.通知队列区分,不同队列不同规则
3.存储使用redis等
4.集群环境下,启动task,初始化未完成通知(防止多个节点同时初始化)
5.内存调优,流量控制(delayqueue 的size判断大于一定值时候,不从MQ里面消费通知消息)
.......