分布式事务解决方案之最大努力通知 下篇

背景:
订单完成支付,通知商户
商户系统接口必须实现幂等性
订单服务提供商户订单查询接口

流程:
消息生产端:
完成事件 -> 调用消息服务,发送消息
消息消费端:
接收消息 -> 调用通知服务(判断该消息未保存过,保存通知消息)-> 构建通知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里面消费通知消息)

.......

转载于:https://www.cnblogs.com/wuzhiwei549/p/9113511.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值