【黑马】Java架构师实战训练营1期1《博学谷》

356 阅读12分钟

虚拟商品交易架构设计及源码实现

1.虚拟商品交易架构体系深入剖析

1.1.什么是虚拟电商


所谓虚拟电商,跟传统电商最大的特点就是没有实体物流配送,像充话费,充流量。比如对于传统电商而言买个笔记本电脑,支付完成后,后台需要走物流配送流程,而像话费充值这种虚拟商品,购买后不需要物流配送,但是需要和第三方接口(例如中国移动)对接,第三方充值成功或者失败后需要回调异步通知。这类虚拟电商业务除了话费充值外还包括很多还包括加油卡,礼品卡,游戏充值,保险等,我们把这些虚拟商品的交易统称电商虚拟交易。

【黑马】Java架构师实战训练营1期1《博学谷》 提娶码:xb2k

虚拟电商交易是对传统电商业务的一种补充,占据着非常重要的位置,随着社会技术的进步,未来会发展得更好,像某东/某宝上都有虚拟业务交易对应的板块。V: cml46679910


两家企业在虚拟业务上都投入了大量的人力物力,足见虚拟业务的重要性。
因此:虚拟电商交易类型的项目有一定业务价值和技术价值,值得探讨。

1.2.虚拟电商业务特点

2.交易策略系统架构

要设计什么样的交易策略才能保证能够满足虚拟交易业务的完整性呢?

2.1.虚拟交易系统策略架构

对于虚拟电商而言,它的很多业务在架构上跟传统电商是一样的,也就是说主体的电商架构都差不多,譬如:比如:商品,订单,收银/结算,优惠促销等;但是不同虚拟业务场景下外围的一些架构各有不同,由此也引申出针对不同虚拟场景的交易策略。
撮合交易策略:撮合交易是指证券经营机构受理投资者的买卖委托后,应即刻将信息按时间先后顺序传送到交易所主机,公开申报竞价。股票申报竞价时,可依有关规定采用集合竞价或连续竞价方式进行,交易所的撮合系统将按“价格优先,时间优先”的原则自动撮合成交。 目前,沪、深两家交易所均存在集合竞价和连续竞价方式。


撮合系统在撮合时实际上是依据前一笔成交价而定出最新成交价的。如果前一笔成交价低于或等于卖出价,则最新成交价就是卖出价;如果前一笔成交价高于或等于买入价,则最新成交价就是买入价;如果前一笔成交价在卖出价与买入价之间,则最新成交价就是前一笔的成交价。

举例说明:

比如,买方出价1399点,卖方出价是1397点。如果前一笔成交价为1397或1397点以下,最新成交价就是1397点;如果前一笔成交价为1399或1399点以上,则最新成交价就是1399点;如果前一笔成交价是1398
点,则最新成交价就是1398点。
推荐交易策略:推荐策略本身也不仅仅针对虚拟商品交易,几乎可以面向所有电商产品,而推荐策略系统会涉
及到一些实时流数据的特征处理,对数据进行过滤,排序,解释,总之这中间会用到相应的一些算法引擎,数据经
过算法引擎之后就会在特定的算法场景中有所体现,比如:商品推荐,活动推荐,店铺推荐,虚拟品类推荐,相似
推荐等。
本课程中着重是以话费充值等这种类似的业务场景来说明一下在这个过程中的一些交易策略
类话费充值业务功能架构如下:

通过图可以看出在功能架构上跟传统的实体电商业务功能上有些是重合的,但也有自己特殊的功能架构,主要
体现在虚拟业务技术实现上的一些特点,比如以话费充值为例,在技术实现上我们就需要考虑以下的一些特殊的业
务场景:\

  1. 余额/押金导致的供应商轮转\
  2. 接口调用失败之后的重试\
  3. 重试次数的阈值限定,超过后的退款等业务\
  4. 对接成功,接口回调的后续业务处理\
  5. 主动的状态检查,接口回调的补偿
    在这些业务场景的实现过程中衍生出了虚拟交易特殊的策略系统:延迟任务系统

2.2.延迟任务系统架构

2.2.1.什么是延迟任务

延迟任务:顾明思议,我们把需要延迟执行的任务叫做延迟任务。它是由事件触发的,任务可添加,延时执行,也可取消;有别于定时任务,定时任务往往是按照固定的时间规则周期性的执行。延迟任务的使用场景很丰富,譬如:\

  1. 红包 24 小时未被查收,需要延迟执行退还业务;\
  2. 订单下单之后 30 分钟后,用户如果没有付钱,系统需要自动取消订单;\
  3. 接口对接过程中因为网络等原因导致失败,1分钟后重试,直到成功,当然失败次数达阈值后取消重试

2.2.2.为何要自研延迟任务系统


延迟任务的实现方案有很多,方案本身没有好坏之分,和系统架构一样,关键是适合自身业务系统,不过随着市场业务的日新月异以及IT技术的不断翻新,有些市面上开源中间件已经不能满足大厂自身业务,很多大厂都是从新定制开发,下面我们从几种常见的延迟任务实现方案来看一下自研延迟任务系统的必要性:

Java API 实现延迟任务


Java API 提供了两种实现延迟任务的方法:DelayQueue 和 ScheduledExecutorService
DelayQueue 是一个支持延时获取元素的阻塞队列, 队列中的元素必须实现 Delayed 接口,并重写 getDelay(TimeUnit) 和 compareTo(Delayed) 方法;在创建元素时可以指定多久才可以从队列中获取当前元素,只有在延迟期满时才能从队列中提取元素。
使用ScheduledExecutorService中的 scheduleWithFixedDelay(Runnable) 方法以固定的频率一直执行任务,虽然可以延迟执行任务,但由于会以某个频率一直循环执行使得它更像定时任务。


优势:单机版,实现简单,
弊端:没办法做成一个通用组件,满足不了大型复杂的业务场景,无法高可用,另外此种方式能添加及消费任务但是无法友好的取消任务。
Redis 实现延迟任务
借助zset 数据类型,把延迟任务存储在此数据集合中,然后在开启一个无线循环查询当前时间的所有任务进行消费。
redis 有序集合sorted Set和集合Set一样也是string类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序。
存储任务数据时可将任务的执行时间当作元素的分数,这样所有任务在zset中按照执行时间会进行排序,并且redis支持按照分数来获取元素
优势:redis本身是一个通用的组件,现在开发一个系统基本也离不开它,用它来实现非常的方便,在一定的数据量下性能比较高
弊端:redis的zset既要完成去重,排序,又要承担着任务元素的添加,消费,移除等工作,在大型复杂的业务场景下,一旦缓存的数据量太大,性能和业务完整性无法保证。另外使用redis也无法做到保留任务的
历史数据,如果基于它自身的持久化机制对性能上又有损耗。
MQ 实现延迟任务
几乎所有的 MQ 中间件都可以实现延迟任务,或者更准确的叫法应该叫延时队列。
以rabbitmq为例,实现延迟队列的方式有两种:
1:通过消息过期后进入死信交换器,再由交换器转发到延迟消费队列,实现延迟功能。
2:使用 rabbitmq-delayed-message-exchange 插件实现延迟功能
优势:方案成熟,性能和可靠性上有保证
弊端:如果专门开启一个 MQ中间件来执行延迟任务,就有点杀鸡用宰牛刀般的奢侈了,除非系统已经有MQ环境了;另外使用MQ同样无法友好的取消任务,也无法保留任务的历史数据定时任务框架实现延迟任务
使用一些定时任务框架譬如:springTask,Quartz,这些都是功能比较强大的任务调度工具,可实现复杂的调度功能,
优势:不用去考虑实现的细节,套用框架即可,各方面细节都有所保证
弊端:对应用有一定侵入性,需要承担框架带来的维护成本,框架隐藏了实现的细节虽然方便但也为性
能调优工作带来了困难。其次这些框架更适合去做定时任务。
总之:为了满足大型且复杂的业务场景,为了保证延迟任务系统的高性能,高可用,为了让延迟任务组件更通用化,我们需要自研一套延迟任务系统,我们自研的系统弥补了现有方案的短板,即支持任务的添加,消费,取消,持久化,大数据量下的性能和可靠性保证,普适性;同时也兼备现有方案的一些优点,使用简单,方便接入且
无侵入。

3.系统数据存储设计

3.1.环境启动


1:docker相关环境启动
对于课程中所需要使用到的一些基础组件,已经通过docker部署完成,直接使用即可,比如:mysql,redis,zookeeper,consul,rabbitmq等
挂载课程资料中提供的 docker 虚拟机,并且登陆到这台虚拟主机,主机ip地址:192.168.200.129,登陆账号:root,登陆密码:itcast
登陆成功后在 /root/virtual-trade 目录下有提供好的使用 docker-compose 编排的各个容器,我们使用如下命令启动即可
2:项目工程环境启动
直接从课程资料中导入提供好的工程即可!
3.2.数据库设计
对于基础组件-延迟任务系统我们要保留任务的历史数据就必须要对任务数据进行持久化操作,因此我们选择了mysql登陆远程129主机上的mysql,root账号的密码是:root123,查看表结构
docker-compose up -d
CREATE TABLE taskinfo (
task_id bigint(20) NOT NULL comment '任务id',
execute_time datetime(3) NOT NULL comment '执行时间',
parameters longblob comment '参数',
priority int(11) NOT NULL comment '优先级',
task_type int(11) NOT NULL comment '任务类型',
PRIMARY KEY (task_id),
KEY index_taskinfo_time (task_type,priority,execute_time)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE taskinfo_logs (
task_id bigint(20) NOT NULL COMMENT '任务id',
execute_time datetime(3) NOT NULL COMMENT '执行时间',

3.3.缓存存储结构设计


在延迟任务的设计过程中我们会用到redis的以下两种数据结构:
有序集合Sorted SetRedis 有序集合和集合一样也是string类型元素的集合,且不允许重复的成员。不同的是每个元素都会关联一个double类型的分数。redis正是通过分数来为集合中的成员进行从小到大的排序,有序集合的成员是唯一的,但分数(score)却可以重复。
我们依然会用zset来存储一些任务数据,但并不是全部数据,是满足一定条件下的任务数据,同时我们会按照任务的类型和优先级拆分出多个zset集合,而不是简单的一个zset。
我们会用到的zset相关的命令大概有:
[ZADD key score1 member1 [score2 member2]]:向有序集合添加一个或多个成员,或者更新已存在成员的分数

ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT] :通过分数返回有序集合指定区间内的成员
[ZREM key member [member ...]] :移除有序集合中的一个或多个成员
列表 List
Redis列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素导列表的头部(左边)或者尾
部(右边)
我们利用List的特点去构造一个高速的任务消费队列,保证任务消费的快速和高效
我们会用到的List相关的命令大概有:
[LPUSH key value1 [value2]] :将一个或多个值插入到列表头部
RPOP key :移除并获取列表最后一个元素
parameters longblob COMMENT '参数',
priority int(11) NOT NULL COMMENT '优先级',
task_type int(11) NOT NULL COMMENT '任务类型',
version int(11) NOT NULL COMMENT '版本号,用乐观锁',
status int(11) DEFAULT '0' COMMENT '状态 0=初始化状态 1=EXECUTED 2=CANCELLED',
PRIMARY KEY (task_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
知识小贴士:mysql BLOB类型介绍
MySQL中,BLOB是一个二进制大型对象,是一个可以存储大量数据的容器,它能容纳不同大小的数据。BLOB类型实际
是个类型系列(TinyBlob、Blob、MediumBlob、LongBlob),除了在存储的最大信息量上不同外,他们是等同的
MySQL的四种BLOB类型:
类型 大小(单位:字节)
TinyBlob 最大 255
Blob 最大 65K
MediumBlob 最大 16M
LongBlob 最大 4G