关于流数据上的事务操作

概述

最近Flink母公司Data Artisans发布了一篇博客关于一个新的组件Streaming Ledger,给出了流数据的事务解决方案(就是常说的数据库的事务,满足ACID,隔离级别为Serializable)。

使用姿势

  • 举例使用经典的转账和存款问题
    • 它是基于Flink的,关于Flink任务初始化的一些内容就不放在这里了
  • 首先创建StreamingLedger。
        // start building the transactional streams
        StreamingLedger tradeLedger = StreamingLedger.create("simple trade example");
  • 第二定义所需要用到的状态和相应的KV类型,这里分别是账户和账单明细。
        // define transactors on states
        tradeLedger.usingStream(deposits, "deposits")
                .apply(new DepositHandler())
                .on(accounts, DepositEvent::getAccountId, "account", READ_WRITE)
                .on(books, DepositEvent::getBookEntryId, "asset", READ_WRITE);
  • 第三步是分别将输入流,具体的事务操作,操作的状态、从事件中获取key的方法、别名(会在事务操作,即此处的TxnHandler中体现)、和权限分别注入StreamLedger并输出为一个sideOutput。
        // produce transactions stream
        DataStream<TransactionEvent> transfers = env.addSource(new TransactionsGenerator(1));

        OutputTag<TransactionResult> transactionResults = tradeLedger.usingStream(transfers, "transactions")
                .apply(new TxnHandler())
                .on(accounts, TransactionEvent::getSourceAccountId, "source-account", READ_WRITE)
                .on(accounts, TransactionEvent::getTargetAccountId, "target-account", READ_WRITE)
                .on(books, TransactionEvent::getSourceBookEntryId, "source-asset", READ_WRITE)
                .on(books, TransactionEvent::getTargetBookEntryId, "target-asset", READ_WRITE)
                .output();
  • 第四步是根据sideOuput的OutputTag输出结果,到这里,除了TxnHandler需要去实现以外,主干逻辑已经完成了。
        //  compute the resulting streams.
        ResultStreams resultsStreams = tradeLedger.resultStreams();

        // output to the console
        resultsStreams.getResultStream(transactionResults).print();
  • 最后就是实现TxnHandler, 具体的转账和写入明细的逻辑都在这里。值得注意的是状态的获取依赖于上一步中在StreamLedger注入的别名,更新完状态之后再输出。
    private static final class TxnHandler extends TransactionProcessFunction<TransactionEvent, TransactionResult> {

        private static final long serialVersionUID = 1;

        @ProcessTransaction
        public void process(
                final TransactionEvent txn,
                final Context<TransactionResult> ctx,
                final @State("source-account") StateAccess<Long> sourceAccount,
                final @State("target-account") StateAccess<Long> targetAccount,
                final @State("source-asset") StateAccess<Long> sourceAsset,
                final @State("target-asset") StateAccess<Long> targetAsset) {

            final long sourceAccountBalance = sourceAccount.readOr(ZERO);
            final long sourceAssetValue = sourceAsset.readOr(ZERO);
            final long targetAccountBalance = targetAccount.readOr(ZERO);
            final long targetAssetValue = targetAsset.readOr(ZERO);

            // check the preconditions
            if (sourceAccountBalance > txn.getMinAccountBalance()
                    && sourceAccountBalance > txn.getAccountTransfer()
                    && sourceAssetValue > txn.getBookEntryTransfer()) {

                // compute the new balances
                final long newSourceBalance = sourceAccountBalance - txn.getAccountTransfer();
                final long newTargetBalance = targetAccountBalance + txn.getAccountTransfer();
                final long newSourceAssets = sourceAssetValue - txn.getBookEntryTransfer();
                final long newTargetAssets = targetAssetValue + txn.getBookEntryTransfer();

                // write back the updated values
                sourceAccount.write(newSourceBalance);
                targetAccount.write(newTargetBalance);
                sourceAsset.write(newSourceAssets);
                targetAsset.write(newTargetAssets);

                // emit result event with updated balances and flag to mark transaction as processed
                ctx.emit(new TransactionResult(txn, true, newSourceBalance, newTargetBalance));
            }
            else {
                // emit result with unchanged balances and a flag to mark transaction as rejected
                ctx.emit(new TransactionResult(txn, false, sourceAccountBalance, targetAccountBalance));
            }
        }
    }

原理

  • 其实我的第一想法是,卧槽好牛逼,这得涉及到分布式事务。把repo clone下来之后发现包含例子只有2000多行代码,一下子震惊了。但是实际的实现还是比较简单地,当然也肯定会带来一些问题。
  • 实际上上面这些API会转换为一个source,一个sink,两个map和一个包含了SerialTransactor(ProcessFunction的实现)的算子。
  • 在这边展示几行代码应该就能明白是如何做到的。关键在于forceNonParallel,这就让所有事情都变得明了了,事实上就是把状态全部都托管到一个并行度为1的算子上,处理的时候也是串行的,这里我才反应过来关键在于隔离级别是Serializable。这里带来的问题就是所有状态都保存在一个节点,并且不能支持水平扩展,所能支撑的吞吐量也不能通过加机器来提升。
        SingleOutputStreamOperator<Void> resultStream = input
                .process(new SerialTransactor(specs(streamLedgerSpecs), sideOutputTags))
                .name(serialTransactorName)
                .uid(serialTransactorName + "___SERIAL_TX")
                .forceNonParallel()
                .returns(Void.class);

感想

其实看到这个功能的第一感觉是很牛逼,但是仔细看过了它的实现觉得真正应用上可能会有不少问题。因为对于最重要的处理事务的那个算子来说,本质上它并不是Scalable的,没有办法横向扩展。不过从功能上来说,确实引出了一个新的发展方向,希望以后还能看到有更优的解决方案,比如针对另外两种隔离级别Read Committed和Repeatable read。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 148,637评论 1 318
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 63,443评论 1 266
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 99,164评论 0 218
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,075评论 0 188
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,080评论 1 266
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,365评论 1 184
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 30,901评论 2 283
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,649评论 0 176
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,122评论 0 223
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,734评论 2 225
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,093评论 1 236
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,548评论 2 222
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,028评论 3 216
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,765评论 0 9
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,291评论 0 178
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,162评论 2 239
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,293评论 2 242

推荐阅读更多精彩内容