背景:
订单完成支付,通知商户
商户系统接口必须实现幂等性
订单服务提供商户订单查询接口
流程:
消息生产端:
完成事件 -> 调用消息服务,发送消息
消息消费端:
接收消息 -> 调用通知服务(判断该消息未保存过,保存通知消息)-> 构建通知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里面消费通知消息)
.......