随着网上购物消费模式热度的不断提高,网上销售平台上各种促销手段也层出不穷,其中“秒购”已经是各种网站普遍流行的促销方式了。“秒购”对数据的实效性和精确性要求非常高,所以通过分布式运算实现高并发数据处理应该是正确的选择。不过,高并发也意味着高频率的数据操作冲突,而高频使用“锁”又会严重影响效率及容易造成不可控异常,所以又被迫选择单线程运行模式。单线程、分布式虽然表面相悖,不过如上篇博文所述:可以利用akka-cluster-sharding分片可指定调用的特性将一种商品的所有操作放到同一个shard上运算(因为shard即是actor,mailbox里的运算指令是按序执行的)可容许在一个分布式环境下有多个分片来同时操作。如此可在获取分布式运算高效率的同时又保证了数据的安全性和完整性。

虽然通过分布式运算可以实现近实时的“秒购”交易,但每个“秒购”请求都直接被发往一个actor信箱里等待执行,如果在一个短时间内出现超大量请求的话就很可能使shard actor mailbox超载,造成系统崩溃,这时需要一种缓冲机制根据具体负载情况来推送任务。当然,这种机制必须具备数据持久化能力,所以kafka是这个缓冲机制的一个最佳选择。

在这篇讨论里我想通过一个“近实时交易平台nrtxn(near realtime transaction)”项目来示范“用kafka实现分布式近实时交易”具体的设计和实现。

nrtxn的应用方案是这样的:提供一个平台及相关api给平台用户。各用户分别将自己的产品推送到平台数据库由平台托管。用户通过平台提供的http api向nrtxn平台提交交易请求(如库存扣减请求),等待或查询平台返回操作状态回应。

nrtxn的系统流程如下:

用户调用http api提交请求 ->
http-server将请求派送给各用户所属的分片workManager ->
workManager将请求写入kafka ->
kafka reader读出请求并按请求中交易项目将请求发送给项目所属的分片txnProcessor->
txnProcessor完成操作后发送回应至workManager ->
workManager在按请求所属的回应地址将最终回应返回给http server -> 用户获取请求回应
值得注意的是交易请求在到达终点actor txnProcessor传递中途经过了kafka,所以在txnProcessor完成数据操作后需要通过一些actor地址管理才能正确地回应到http server上的请求线程。
我们大致可以从请求内容了解平台提供的功能:

class TxnRequest( //唯一键:shopId+seller+itemCode+reqTime
shopId: String = "", //托售单位(平台用户)
seller: String = "", //售货组别
buyer: String = "", //购货单位
respond: Int = 1, //1=需要返回操作结果,0=不返回response (fire-and-go)
reqTime: String = "", //请求时间(yyyyMMddHHmmssSSS)
itemCode: String = "", //交易项目
reqValue: Double = 0.00, //操作价值
remarks: String = "", //操作备注
submitTm: String = "", //提交时间 由系统填写(yyyyMMddHHmmssSSS)
//写入kafka之前填写,读出时超出指定时段视为无效请求
)

操作请求支持两种模式:request-response, fire-and-go, 由respond值表示。无论如何,平台都会产生相应的response并记录在平台数据库里。用户可在使用fire-and-go模式或者系统出现异常情况时通过原request查询对应的response。

回应response内容如下:

case class TxnResponse(
shopId: String = "", //托售单位(平台用户)
seller: String = "", //售货单位
buyer: String = "", //购货单位
reqTime: String = "", //请求时间(yyyyMMddHHmmssSSS
failed: Int = 0, //操作失败 <> 0
reason: String = "", //失败原因说明
rspTime: String = "", //完成时间
itemCode: String = "", //操作目标
rsrvdValue: Double = 0.00, //系统预留价值
reqValue: Double = 0, //操作价值
preValue: Double = 0.00, //操作前价值
reqValue: Double = 0.00, //操作价值
)

1、http api 发送request到workManager分片;

pathPrefix("submit") {
val entityRef = sharding.entityRefFor(WorkManager.EntityKey, s"$shopId:$posId")
entity(as[String]) { json =>
log.step(s"received submit($shopId,$json)")
//json validation
val reqInfo = TxnServices.parseRequest(shopId,json)
if (reqInfo._1) {
val req = reqInfo._3
//mark submit time
val jsonReq = toJson(req.copy(submitTm = mgoDateToString(mgoDateTimeNow,"yyyyMMddHHmmssSSS")))
if (req.respond == 1) {
val futResp = entityRef.ask[WorkManager.Response](WorkManager.TakeWork(jsonReq, _))
.map {
case WorkManager.ResultMsg(json) => HttpEntity(MediaTypes.`application/json`, json)
case WorkManager.ErrorMsg(msg) => HttpEntity(MediaTypes.`application/json`, msg)
}
onSuccess(futResp)(complete(_))
} else {
entityRef ! WorkManager.FireupWork(jsonReq)
complete("OK")
}
} else {
log.step(s"submit($shopId,$json): ${reqInfo._2}")
complete(toJson(Map[String,Any]("sts" -> -1, "msg" -> reqInfo._2)))
}
}
} ~

如果respond=1,entityRef.ask[WorkManager.Response](WorkManager.TakeWork(jsonReq, _)) 构建了一个future session 直至收到回复或超时。

2、workManager是一种actor,负责管理请求回应地址及写入Kafka:

             case TakeWork(jsonReq,replyTo) =>
log.step(s"WorkManager: TakeWork($jsonReq)[${entityId}]")
val req = fromJson[TxnRequest](jsonReq)
val work = Work(
req.shopId + req.seller + req.itemCode + req.reqTime,
Instant.now(),
replyTo,
)
workStates = workStates.addWork(work)
log.step(s"WorkManager: adding work: $work, new workStates: ${workStates.toString()}")
for {
_ <- TxnServices.logRequest(jsonReq, trace)
_ <- SendProducer(ps).send(new ProducerRecord[String, String](kafkaTopic, jsonReq))
} yield "Done"
log.step(s"WorkManager: current workStates: ${workStates.toString()}")
Behaviors.same

workStates是一个请求管理类,如下:

   case class Work(jobId: String, startTime: Instant, replyTo: ActorRef[Response]) {
def timeElapsed(): FiniteDuration = {
Duration.between(startTime, Instant.now()).toScala
}
}
case class WorkStates(pendingWorks: List[Work] = Nil) { self =>
def cleanWorkList(timeLap: FiniteDuration) = {
val pw = pendingWorks.foldRight(List[Work]()) { (w, accu) =>
if (w.timeElapsed() > timeLap) {
accu
} else w :: accu
}
copy(pendingWorks = pw)
}
def addWork(w: Work) = copy(pendingWorks = w :: pendingWorks)
def getWork(jobId: String) = {
pendingWorks.find { w =>
w.jobId == jobId
}
}
override def toString(): String = {
pendingWorks.foldRight("") {(w,accu) =>
accu + w.jobId + " " + w.startTime + "\n"
}
}
}

workManager在收到txnProcessor完成数据操作后的状态回应后从workStates中找出对应的请求地址进行回应:

            case WorkResponse(rsp) =>
log.step(s"WorkManager: WorkResponse($rsp)[${entityId}]")
log.step(s"WorkManager: current workStates: ${workStates.toString()}")
val jobid = rsp.shopId+rsp.seller+rsp.itemCode+rsp.reqTime
log.step(s"WorkManager: finding work with jobId = $jobid")
val someWork = workStates.getWork(jobid)
log.step(s"WorkManager: WorkResponse someWork = $someWork")
if(someWork.isDefined) {
someWork.get.replyTo ! ResultMsg(toJson(Map[String, Any] ("sts" -> 0, "msg" -> "", "data" -> rsp)))
Done(loc.shopid, loc.posid, s"got WorkResponse($rsp).")
}
Behaviors.same

3、Kafka reader 读出请求后按请求交易项目编号指定txnProcessor分片派送请求:

def start =
(1 to numReaders).toList.map { _ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => commitableSource }
// .viaMat(KillSwitches.single)(Keep.right)
.mapAsyncUnordered(32) { msg =>
TxnServices.toTxnProcessor(msg.record.value(), responseTimeout, trace)
.onComplete {
case Success(res) =>
log.step(s"AtLeastOnceReaderGroup-toTxnProcessor returns ${res}: ${msg.record.value()}")
case Failure(err) =>
log.step(s"AtLeastOnceReaderGroup-toTxnProcessor Error ${err.getMessage}: ${msg.record.value()}")
}
FastFuture.successful(msg.committableOffset)
}
.toMat(Committer.sink(committerSettings))(Keep.left)
.run()
}

mapAsyncUnordered(32)可支持32个线程同时执行TxnServices.toTxnProcessor。通过back pressure,确保每个线程在完成后才进行下一个请求读取。
TxnServices.toTxnProcessor中通过请求产生分片的entityId,然后向对应的txnProcessor分片发送请求:

  def toTxnProcessor(jsonReq: String, askTimeout: FiniteDuration, trace: Boolean)(
implicit sharding: ClusterSharding, streamFlowTimeout: Timeout, loc: MachineId) = {
log.stepOn = trace
log.step(s"TxnServices-toTxnProcessor($jsonReq)")
val txnReq = fromJson[TxnRequest](jsonReq)
val submt = mgoStringToDateTime("yyyyMMddHHmmssSSS",txnReq.submitTm)
if (Duration.between(submt.toInstant,Instant.now()).toScala < askTimeout) {
val entityId = hashItemCode(txnReq.itemCode)
log.step(s"TxnServices-toTxnProcessor: entityId = $entityId")
val workerEntityRef = sharding.entityRefFor(TxnProcessor.EntityKey, entityId)
if (txnReq.respond == 0) {
if (txnReq.reqValue > 0)
workerEntityRef.ask[TxnProcessor.Response](TxnProcessor.RunDec(txnReq, _))
else workerEntityRef.ask[TxnProcessor.Response](TxnProcessor.RunInc(txnReq, _))
} else {
if (txnReq.reqValue > 0)
workerEntityRef.ask[TxnProcessor.Response](TxnProcessor.DecValue(txnReq, _))
else workerEntityRef.ask[TxnProcessor.Response](TxnProcessor.IncValue(txnReq, _))
}
} else FastFuture.successful("Skip")
}

在TxnServices.toTxnProcessor中对请求进行了超时验证。系统出现故障重启后留在kafka队列里的请求视为无效,因为http 端请求早已经过时了。

4、txnProcessor收到请求完成操作后产生response并发送给workManager:

           case DecValue(txnreq,replyTo) =>
log.step(s"TxnProcessor: DecValue($txnreq)[${entityId}]")
ctx.pipeToSelf(
for {
decRes <- decValue(txnreq.shopId,txnreq.itemCode,txnreq.reqValue,trace)
_ <- FastFuture.successful {
log.step(s"TxnProcessor: DecValue($txnreq)[${entityId}] DecValue result = $decRes")
}
_ <- if (decRes._1._1 == 0) {
logResponse(txnreq, decRes._2._2.asInstanceOf[Double], decRes._2._1.asInstanceOf[Double],trace)
} else FastFuture.successful {
log.step(s"TxnProcessor: DecValue($txnreq)[${entityId}] skip logging response")
}
} yield decRes
) {
case Success(res) => {
val txnrsp = TxnResponse(
txnreq.shopId,
txnreq.seller,
txnreq.buyer,
txnreq.reqTime,
res._1._1,
res._1._2,
mgoDateToString(mgoDateTimeNow,"yyyyMMddHHmmssSSS"),
txnreq.itemCode,
res._2._2.asInstanceOf[Double],
res._2._1.asInstanceOf[Double],
txnreq.reqValue,
)
entityRef ! WorkManager.WorkResponse(txnrsp)
replyTo ! DoneOp
Done(loc.shopid, loc.posid, s"DecValue response: $txnrsp")
}
case Failure(err) =>
log.error(s"TxnProcessor: DecValue Error: ${err.getMessage}[${entityId}]")
val txnrsp = TxnResponse(
txnreq.shopId,
txnreq.seller,
txnreq.buyer,
txnreq.reqTime,
1,
err.getMessage,
mgoDateToString(mgoDateTimeNow,"yyyyMMddHHmmssSSS"),
txnreq.itemCode,
0,
0,
txnreq.reqValue,
)
entityRef ! WorkManager.WorkFailure(txnrsp)
replyTo ! DoneOp
Done(loc.shopid, loc.posid, s"DecValue with error: ${err.getMessage}")
}
Behaviors.same

workManager收到WorkResponse后从workStates找到对应work的actorRef并将response发送至http route。
另外,nrtxn支持扣减DecValue和增加IncValue。在交易过程中,一般退还库存应该优先处理,可以用actor的priorityMailbox来实现:

 prio-dispatcher {
type = Dispatcher
mailbox-type = "com.datatech.nrtx.server.TxnProcessor$PriorityMailbox"
} class PriorityMailbox (settings: Settings, cfg: Config)
extends UnboundedPriorityMailbox( PriorityGenerator {
case x: IncValue => 0
case x: RunInc => 1
case _ => 2
}
)
...
val priorityDispatcher = DispatcherSelector.fromConfig("prio-dispatcher") log.step(s"initializing sharding for ${TxnProcessor.EntityKey} ...")
val txnProcessorType = Entity(TxnProcessor.EntityKey) { entityContext =>
TxnProcessor(entityContext.shard,mgoClient,indexer,entityContext.entityId,keepAlive,trace)
}.withStopMessage(TxnProcessor.StopWorker)
sharding.init(txnProcessorType.withEntityProps(priorityDispatcher))

这样,txnProcessor会优先处理IncValue,RunInc消息。

alpakka-kafka(10)-用kafka实现分布式近实时交易的更多相关文章

  1. 基于Canal和Kafka实现MySQL的Binlog近实时同步

    前提 近段时间,业务系统架构基本完备,数据层面的建设比较薄弱,因为笔者目前工作重心在于搭建一个小型的数据平台.优先级比较高的一个任务就是需要近实时同步业务系统的数据(包括保存.更新或者软删除)到一个另 ...

  2. 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的

    ☞☞☞ 我是如何将一个老系统的kafka消费者服务的性能提升近百倍的 ☜☜☜ ○○○○○○○○○○○○○○○ 大家好,又见面了~ kafka作为一种高吞吐量的分布式发布订阅消息系统,在业务系统中被广泛 ...

  3. Structured Streaming + Kafka Integration Guide 结构化流+Kafka集成指南 (Kafka broker version 0.10.0 or higher)

    用于Kafka 0.10的结构化流集成从Kafka读取数据并将数据写入到Kafka. 1. Linking 对于使用SBT/Maven项目定义的Scala/Java应用程序,用以下工件artifact ...

  4. Kafka#4:存储设计 分布式设计 源码分析

    https://sites.google.com/a/mammatustech.com/mammatusmain/kafka-architecture/4-kafka-detailed-archite ...

  5. Flume+Kafka收集Docker容器内分布式日志应用实践

    1 背景和问题 随着云计算.PaaS平台的普及,虚拟化.容器化等技术的应用,例如Docker等技术,越来越多的服务会部署在云端.通常,我们需要需要获取日志,来进行监控.分析.预测.统计等工作,但是云端 ...

  6. CDH5.10 添加kafka服务

    简介: CDH的parcel包中是没有kafka的,kafka被剥离了出来,需要从新下载parcel包安装.或者在线安装,但是在线安装都很慢,这里使用下载parcel包离线安装的方式. PS:kafk ...

  7. Kafka集群安装部署、Kafka生产者、Kafka消费者

    Storm上游数据源之Kakfa 目标: 理解Storm消费的数据来源.理解JMS规范.理解Kafka核心组件.掌握Kakfa生产者API.掌握Kafka消费者API.对流式计算的生态环境有深入的了解 ...

  8. Kafka:ZK+Kafka+Spark Streaming集群环境搭建(十)安装hadoop2.9.0搭建HA

    如何搭建配置centos虚拟机请参考<Kafka:ZK+Kafka+Spark Streaming集群环境搭建(一)VMW安装四台CentOS,并实现本机与它们能交互,虚拟机内部实现可以上网.& ...

  9. Kafka剖析:Kafka背景及架构介绍

    <Kafka剖析:Kafka背景及架构介绍> <Kafka设计解析:Kafka High Availability(上)> <Kafka设计解析:Kafka High A ...

随机推荐

  1. Oracle:使用PL-SQL登录时报ORA-12541:无监听程序的解决办法

    背景: 在自己公司安装的Oracle,当时Oracle的监听地址都是写的公司的地址 后来由于项目需要,办公地点转移到了客户处, 大概有半年没有以sys用户登录数据库了. 最近在上下班途中学习Djang ...

  2. python 之 pip、pypdf2 安装与卸载

    pip是个啥? pip 是一个现代的,通用的 Python 包管理工具.提供了对 Python 包的查找.下载.安装.卸载的功能. 第一步:pip 下载:https://pypi.org/projec ...

  3. 779. 第K个语法符号

    <找规律> <递归> 题目描述 在第一行我们写上一个 0.接下来的每一行,将前一行中的0替换为01,1替换为10. 给定行数 N 和序数 K,返回第 N 行中第 K个字符.(K ...

  4. 创建react开发环境

    准备工作 1.下载node.js(http://nodejs.cn/download/)推荐下载长期支持的版本 2.下载cnpm(https://jingyan.baidu.com/article/9 ...

  5. js几种常见排序的实现

    1. 冒泡排序 定义: 比较相邻的前后二个数据,如果前面数据大于后面的数据,就将二个 数据交换. 这样对数组的第0个数据到N-1个数据进行一次遍历后,最大的一个数据就"沉"到数组第 ...

  6. 打开Cmd的方式与基础Dos命令

    基础的Dos命令 打开Cmd的方式 开始->Windows系统->命令提示符 Win键 + R输入cmd打开控制台 在任意的文件夹下面,按住shift键+鼠标右键点击在此处打开powers ...

  7. RDA5807M开发指南 & 开源库函数

    文档标识符:RDA5807M_T-D-P16 作者:DLHC 最后修改日期:2022.1.14 最后修改内容:修改.添加内容 发布状态:已发布 本文链接:https://www.cnblogs.com ...

  8. Java对象内存模型

    2 Java对象内存模型 在HotSpot虚拟机中,对象在内存中存储的布局可以分为3块区域:对象头(Header). 实例数据(Instance Data)和对齐填充(Padding). 在 JVM ...

  9. 基础概念(3):怎么写一个c程序?

    总结卡片: 遵循c语言的规则,即可写出c程序.规则下有两个重要概念:函数与变量.就好像游戏中的打仗,要考虑怎么打,谁来打."怎么打"就是流程,把流程封装起来就是函数,流程也叫算法. ...

  10. golang中gomodule讲解

    0. GOMODULES模式 1. GOPATH的缺点 1. 无版本控制概念 2. 无法同步一致第三方版本号 3. 无法指定当前项目引用的第三方版本号 2. go1.11版本之后可以使用GoModul ...