文章分为四个部分
- 1、主要功能
- 2、运用的技术
- 3、系统设计
- 4、优化与总结
1、主要功能
对平台支付网关的交易订单进行实时的统计,包括实时的交易金额与交易订单量、不同支付方式的交易总额、订单量以及占比、当天各个时间段的数据统计折线图,实现效果图如下:
2、运用的技术
- Redis:利用Redis的消息发布与订阅功能、以及List、SortedSet、Hash的数据结构特性
- WebSocket:负责将实时汇总的交易数据推送至浏览器客户端
3、系统设计
实时交易数据监控系统所涉及的工程包括交易服务、监控统计服务、监控应用(Dubbo服务化)。
- 交易服务在交易成功后向Redis中发布消息并将数据发送至Redis的list队列
- 监控服务负责Redis消息的订阅并进行统计,统计完成后将实时的统计结果再次发送至Redis
- 监控应用作为WebSocket的服务端,也负责监听监控服务推送过来的实时统计数据并通过WebSocket将数据推送至客户端。
Redis数据结构图如下:
利用list的lpush、lpop功能进行对数据的存取操作,SortedSet最开始主要是用于排序,将交易时间作为score进行排序,但是因为涉及到一些数据的计算,在高并发以及分布式部署的情况下,利用SortedSet进行数据统计是会存在问题的,文末会提到,hash结构主要是用于对数据进行原子性的计算。
UML时序图如下:
3.1、交易系统——支付服务
支付服务在交易成功后,会给Redis发布一条订单记录消息,并向Redis的list列表lpush一条同样的订单记录信息,为了不影响正常的支付业务流程,所以采用的是异步的方式,伪代码如下:
/**
* Redis消息通道
*/
@Value("#{settings['redis.trade.channel']}")
private String redisChannel;
/**
* 微信支付的订单队列key
*/
@Value("#{settings['redis.trade.wxDetails']}")
private String redisWxQueue;
/**
* 支付宝支付的订单队列key
*/
@Value("#{settings['redis.trade.alipayDetails']}")
private String redisAlipayQueue;
/**
* 单个线程的线程池
*/
protected static ExecutorService executorService = Executors.newSingleThreadExecutor();
/**
* 交易成功后需要执行的业务逻辑
* @param paymentRecord
*/
public void successPayment(final PaymentRecord paymentRecord) {
// do otherthing...
// 异步发送消息
executorService.submit(new Runnable() {
@Override
public void run() {
try {
pushPaymentRecordMonitorVo(paymentRecord);
} catch (Exception e) {
log.error("payment send to redis fail,PaymengRecord:" + JsonUtil.toJsonString(paymentRecord));
}
}
});
}
/**
* 将交易成功的订单信息插入至Redis队列并发送一条通知通知
* @param paymentRecord
* @throws Exception
*/
private void pushPaymentRecordMonitorVo (PaymentRecord paymentRecord) throws Exception{
PaymentRecordMonitorVo paymentRecordMonitorVo = new PaymentRecordMonitorVo();
paymentRecordMonitorVo.setMerchantOrderNo(paymentRecord.getMerchantOrderNo());
paymentRecordMonitorVo.setPayWay(paymentRecord.getPayWayCode() == null ? null : paymentRecord.getPayWayCode().name());
paymentRecordMonitorVo.setTradeTime(paymentRecord.getPaySuccessTime());
paymentRecordMonitorVo.setAmount(paymentRecord.getOrderAmount());
log.info("订单消息插入Redis队列...");
if (paymentRecord.getPayWayCode() != null && paymentRecord.getPayWayCode().equals(PayWayEnum.WEIXIN)) {
JedisHelper.dataCluster().lpush(redisWxQueue,JsonUtil.toJsonString(paymentRecordMonitorVo));
} else if (paymentRecord.getPayWayCode() != null && paymentRecord.getPayWayCode().equals(PayWayEnum.ALIPAY)) {
JedisHelper.dataCluster().lpush(redisAlipayQueue,JsonUtil.toJsonString(paymentRecordMonitorVo));
}
log.info("订单消息插入Redis队列结束...");
// 发布消息
log.info("订单消息发布到Redis...");
JedisHelper.dataCluster().publish(redisChannel,JsonUtil.toJsonString(paymentRecordMonitorVo));
log.info("订单消息发布到Redis结束...");
}
3.2、监控服务
3.2.1.主要功能包括:
- 订阅Redis中交易服务发布过来的订单消息以及获取list列表中的订单数据
- 根据订单的交易时间,按照每15分钟为一个数据汇总点进行汇总
- 对每15分钟汇总的SortedSet进行统计后,将结果再发布至Redis的消息中
3.2.2.遇到的坑:
在监控服务启动的时候会进行Redis的list列表中数据的统计初始化,并开启Redis消息订阅者的监听。但有三个比较坑的地方就是:
(1)因为用的是Redis6个节点组成的一个集群,所以是用JedisCluster,但是JedisCluster在2.8.x版本以上才支持消息的发布与订阅,项目原先用的是2.7.3版本 解决方案:把项目Jedis版本改为2.8.1,pom.xml内容如下:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
(2)Redis的消息发布与订阅,在订阅方必须要手动的调用subscribe()方法,并将监听者和需要监听的通道作为参数传入才能开启监听,而像ActiveMQ这种消息中间件是不需要显示的调用,只需配置好消息监听者就会自动监听的。
还有一个坑就是subscribe()方法是一个线程阻塞方法,本想在项目启动的时候就调用subscribe()开启消息的订阅,结果发现方法调用后,其他的代码根本没法往下执行。 解决方案是:在项目启动的时候调用subscribe()方法开启消息监听,并且新开一个线程去调用subscribe()方法来避免阻塞主线程。
(3)Redis不支持消息的持久化。在订阅者没有启动的时候,消息发布者将消息发出去了,订阅者没有收到,那订阅者重新启动的时候也不会收到之前发的消息了,而像ActiveMQ是支持消息的持久化的。
解决方案:在往Redis发布消息的时候也同样往Redis的list列表中lpush一条同样消息的数据(参照上面交易服务中的代码),消息订阅者接收到消息并进行相应的业务处理后,再将list列表中的数据删除,那在监控服务挂掉的情况下,Redis消息无法正常被监听消费,但是Redis的list列表中还是会存有消息的数据,所以后续我们可以从list列表中取出消息数据再进行相应的业务处理,这样就间接的实现了Redis消息的持久化。
3.2.3.部分代码
(1)RedisSubscribeHelper.java:监控服务启动时,进行Redis队列中数据的统计初始化,并开启Redis消息订阅者的监听的
package com.ylp.core.monitor.redis;
import com.ylp.common.tools.utils.JsonUtil;
import com.ylp.core.monitor.biz.MonitorBiz;
import com.ylp.facade.monitor.utils.JedisHelper;
import com.ylp.facade.monitor.utils.MonitorUtils;
import com.ylp.facade.monitor.vo.PaymentRecordMonitorVo;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.fact