引言
一个订单系统的设计绝非这么简单,它需要一批又一批的人去维护、去优化,根据公司的业务情况做出改变和兼容.作为一个返利平台,其核心就是订单拉取,收益计算,邀新
.在接下来的文章里我会围绕这些功能逐一向大家讲解.因为每个公司的业务都不一样,本文以提供思路方案为主,代码为辅,涉及业务代码部分大多以伪代码处理.
订单拉取思路
技术栈
- schedulerx:阿里中间件自研的基于 Akka 架构的新一代分布式任务调度平台
- canal:主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费
- RocketMQ:一款开源的分布式消息系统,基于高可用分布式集群技术,提供低延时的、高可靠的消息发布与订阅服务。
订单拉取思维导图
以上是一个完整的订单拉取流程思维导图,以图为基础简单分析下订单收益系统的整体架构.
- 用户的支付和浏览大部分依托于跳转到第三方平台操作.平台只供给商品浏览和收藏分享等功能.
- 用户在第三方平台支付后,调用第三方开放平台api进行拉订单的操作.
通常使用定时任务调度处理
. - 拉取到订单后程序处理相应的返利,成长值,积分等等操作并记录订单信息到数据库中.
- 在记录订单信息的同时会保存该订单的收益到收益表.这部分收益包含且不限于,自购|分销|分享.
- 借助于canal监控收益表的变动记录并发送到RocketMQ中,服务消费异构一张收益日报表,便于后期的收益统计.
订单拉取实战
步骤技巧
我的做法是定义BasePullSevice接口,因为都会有拉取动作和处理动作,我就把拉取动作和处理动作定义在这个Base接口类中.然后如果新增平台就直接多加一个实现类去实现.这样如果后期别人维护接口时也会对一目了然.
public interface BasePullSevice {
/**
* 定时扫描订单
*/
boolean scanning();
/**
* 扫描的订单处理入库
*/
UserOrder delUserOrder(UserOrder userOrder);
}
接下来在delUserOrder中处理这些订单,把不同平台的订单组装为我们自己平台的订单实体类.为了防止掉单的情况,在delUserOrder处理完组装成平台订单的实体后,存入mysql持久化,并标记这个单目前是未处理的状态.表设计如下
CREATE TABLE `t_user_order_history` (
`id` int(20) NOT NULL AUTO_INCREMENT COMMENT '流水表主键',
`order_id` bigint(20) DEFAULT NULL COMMENT '本平台订单id',
`order_sn` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '第三方平台订单号',
`order_sun_sn` varchar(64) CHARACTER SET utf8mb4 DEFAULT NULL COMMENT '第三方平台子订单号',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品id',
`source_data` text COMMENT '拉取的第三方平台json',
`data` text COMMENT '本平台组装订单属性',
`times_sort` tinyint(2) DEFAULT '1' COMMENT '订单拉取的排序,初始为1,以后每次拉取到排序加1',
`change_type` tinyint(2) DEFAULT '0' COMMENT '拉取类型 0为订单初始化 1为订单改变',
`del_flag` tinyint(2) DEFAULT '0' COMMENT '是否已处理 1已处理',
`user_id` int(11) NOT NULL COMMENT '下单人',
PRIMARY KEY (`id`) USING BTREE,
KEY `INX_orderId` (`order_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3805 DEFAULT CHARSET=utf8 COMMENT='订单拉取流水表'
记录完毕后再通过MQ发送消息,让MQ去分发消息并处理这些订单.这里有个小细节,我们在订单拉取流水表入库的时候会判断orderId是否存在,存在则在原有times_sort的基础上+1,并标记change_type为1订单改变状态.为了解决高峰时段的订单量大问题,我启动了多个消费者服务去消费这些订单消息.消费者伪代码如下
public Action consume(Message message, ConsumeContext consumeContext) {
String tag = message.getTag();
String body = new String(message.getBody());
log.error("orderBody={}", body);
try {
//消息为订单流水表变动消息
StringJoiner joiner = new StringJoiner(",");
if (MqTagConstant.ORDER_HISTORY_TAG.equals(tag)) {
String mqRepeatKey = "mq_repeat_".concat(message.getKey());
if (cacheClient.exist(mqRepeatKey)) {
return Action.CommitMessage;
}
cacheClient.set(mqRepeatKey, "1", 60 * 2);
UserOrderHistory userOrderHistory = userOrderHistoryService.getById(body);
distributedLock.lock("user:history:".concat(String.valueOf(userOrderHistory.getUserId())));
UserOrder userOrder;
log.info("同步订单数据 begin");
if (userOrderHistory.getChangeType() == 1) {
userOrder = saveUserOrder.uppUserOrder(userOrderHistory);
} else {
List<OrderPushModel> orderPushModelList = new LinkedList<>();
userOrder = saveUserOrder.saveUserOrder(userOrderHistory, orderPushModelList);
}
log.info("同步订单数据 end");
distributedLock.unlock("user:history:".concat(String.valueOf(userOrderHistory.getUserId())));
return Action.CommitMessage;
}
} catch (Exception e) {
log.error("订单拉取失败:", e);
}
return Action.ReconsumeLater;
}
因为订单涉及到返利的变化,所以这里用redis做了一个分布式锁,保证同一个用户的订单是在前一个订单走完后才进行处理
.处理类大致就是些订单逻辑,各个公司都有不同的处理方式.
/**
* @param userOrderHistory 当前订单流水实体
* @description: 修改用户订单
* @author: chenyunxuan
* @updateTime: 2020/11/10 4:54 下午
*/
@Transactional(rollbackFor = Exception.class)
public UserOrder uppUserOrder(UserOrderHistory userOrderHistory) {
.........
}
/**
* @param userOrderHistory 当前订单流水实体
* @param orderPushModelList 需要发送的消息列表
* @description: 保存用户订单
* @author: chenyunxuan
* @updateTime: 2020/11/10 4:54 下午
*/
@Transactional(rollbackFor = Exception.class)
public UserOrder saveUserOrder(UserOrderHistory userOrderHistory, List<OrderPushModel> orderPushModelList) {
......
}
声明式事务@Transactional
是为了防止代码块里对用户金额和等级改变后发生报错,前面执行的数据无法回滚造成脏数据.在处理消费者中,因为在saveUserOrder
和uppUserOrder
加了事务,可以保证执行完这个方法绝对是程序没有报错的,这时会回写流水表,把该条订单流水标记为已处理并发送相应的MQ,完成新增订单的消息给前端这一动作和一些边缘任务(如订单积分,等级奖励)的触发.
后记
整个订单服务的设计主要围绕效率和准确,用MQ去削流量峰值,用流水表去持久化订单拉取信息,巧妙运用事务特性去发送准确的消息.希望这个开发思路能够在实际开发中帮助大家去解决自己系统的问题.