zoukankan      html  css  js  c++  java
  • 分布式事务之本地消息表

    什么是分布式事务

    分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。简单的说,就是一次大的操作由不同的小操作组成,这些小的操作分布在不同的服务器上,且属于不同的应用,分布式事务需要保证这些小操作要么全部成功,要么全部失败。本质上来说,分布式事务就是为了保证不同数据库的数据一致性。

    为什么我们要反反复复的强调一致性?

    因为,一致性就保证了我们的数据,不会出大问题,至少不会导致出现对账对不上等奇怪的问题。 不然的话,扯皮都扯不清。这就是为什么我们宁愿让我们的交易失败,也不愿意让其出现不一致的情况。所以,涉及多个DML操作,特别是更新、新增、删除操作,我们一定要把它们放入一个事务中,进行事务的控制。

    为什么分布式环境中,一致性的问题被如此多的提及,因为分布式环境中,网络问题更多,出现问题的机会会更多,特别又是高并发大数据量的情况下。我们开发环境下,虚拟机上两个机器的集群,相互之间出现网络问题的机会,几乎TM没见过。但是生产环境,我们都是独立部署的。不管怎么样,一旦出现网络问题了呢, 那就可能导致 数据的不一致的 问题。即使出现网络的机会可能只是100w分之一,那么如果一个系统的交易额一个是100w,那么就是说,一天出现一次网络问题的概念是1,是100%。

    也许你会说,如果真出现了这个问题再来人工处理吧,或许人工处理的成本比程序保证的成本更低呢? 但是,一般来说现在的人工成本是很贵的,而程序员的工作就是要保证程序的稳定,尽量少出故障,出现了数据不一致现象,更加是大忌,难以解释。通常认为软件的成本是很低的,人工或者 硬件的成本是比较高的,虽然写一个软件的成本并不低,但是那个已经是程序员的脑力、能力的话题了。对于能力强的程序员来说,写一个稳定的、高效的、数据一致的程序,并不是什么太难的事。 所以呢,我们需要不断学习。。。

    而且,我们的系统有方方面面,要是是不是这里出现数据不一致,那里也出现,那会被骂死。

    分布式事务产生的原因

    从上面本地事务来看,我们可以看为两块,一个是service产生多个节点,另一个是resource产生多个节点。

    service多个节点

    随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,举个简单的例子,一个公司之内,用户的资产可能分为好多个部分,比如余额,积分,优惠券等等。在公司内部有可能积分功能由一个微服务团队维护,优惠券又是另外的团队维护 

    这样的话就无法保证积分扣减了之后,优惠券能否扣减成功。

    resource多个节点

    同样的,互联网发展得太快了,我们的Mysql一般来说装千万级的数据就得进行分库分表,对于一个支付宝的转账业务来说,你给的朋友转钱,有可能你的数据库是在北京,而你的朋友的钱是存在上海,所以我们依然无法保证他们能同时成功

     

    本地消息表

    本地消息表这个方案最初是ebay提出的 ebay的完整方案https://queue.acm.org/detail.cfm?id=1394128。

    此方案的核心是将需要分布式处理的任务通过消息日志的方式来异步执行。消息日志可以存储到本地文本、数据库或消息队列,再通过业务规则自动或人工发起重试。人工重试更多的是应用于支付场景,通过对账系统对事后问题的处理。 

    这个图看似已经把所有流程都画出来了,其实不是,很多地方不太确定, 具体的做法也可以各种各样。

    当我们 本地消息表实现分布式事务 的最终一致性的时候, 我们其实需要明白 我们首先需要在本地数据库 新建一张本地消息表,然后我们必须还要一个MQ(不一定是mq,但必须是类似的中间件)

    消息表怎么创建呢?这个表应该包括这些字段: id, biz_id, biz_type, msg, msg_result, msg_desc,atime,try_count。分别表示uuid,业务id,业务类型,消息内容,消息结果(成功或失败),消息描述,创建时间,重试次数, 其中biz_id,msg_desc字段是可选的。

    具体怎么做呢?消息生产方(也就是发起方),需要额外建一个消息表,并记录消息发送状态。消息表和业务数据要在一个事务里提交,也就是说他们要在一个数据库里面。然后消息会经过MQ发送到消息的消费方。如果消息发送失败,会进行重试发送。

    消息消费方(也就是发起方的依赖方),需要处理这个消息,并完成自己的业务逻辑。此时如果本地事务处理成功,表明已经处理成功了,如果处理失败,那么就会重试执行。如果是业务上面的失败,可以给生产方发送一个业务补偿消息,通知生产方进行回滚等操作。

    生产方和消费方定时扫描本地消息表,把还没处理完成的消息或者失败的消息再发送一遍。如果有靠谱的自动对账补账逻辑,这种方案还是非常实用的。

    实现思路:

    实现由多种方式,一般来说是这样的 (每一个步骤就是一个方法): 

    1.生产方  首先 执行我们的业务,成功后向MQ 同步发送消息,消息内容是什么?消息内容是业务信息,至少是包括了一些跨服务调用的参数,然后获取结果r1, 成功后向本地消息表新增一行,主要需要记录消息内容,消息结果r1,业务信息(可选)—— 发消息、两个数据库操作 都必须要在同一个事务内部完成;
    3.消费方  监听MQ的某个业务dest,然后,发现消息被生产了,那么就消费之,调用4, 4成功后就算消费成功,然后从mq 删除对应的消息;4如果失败则等待少数时间后重试,4 放入一个循环里面,循环3次,3次失败后发通知,然后人工处理;
    4.消费方  开始消费,怎么消费呢? 就是直接执行对应的本地事务逻辑;

    为什么  业务操作要先于发消息(这里只讨论同步发送),而发消息 必须要先于 本地消息表 操作? 其实也是不一定的。这样做的原因在于,我们需要发消息,业务操作的结果,可能需要作为消息内容传递。 这样做的麻烦之处在于, 如果前两步成功了,但最后的本地消息表操作失败,那么事务回滚,但是消息已经发送, 是不能回滚的。这个时候 怎么办呢?我们也可以在 回滚 事务的时候,根据消息id,手动删除 已发送的消息。

    另外,消息发送失败怎么办呢? 那就是应该 直接结束事务。

    为什么  发消息、两个数据库操作 都必须要在同一个事务内部完成? 数据库操作可以通过事务进行处理, 但是事务限制不了消息。如果数据库操作失败,或者消息发送失败(消息同步发送失败的意思就是 mq 由于某些原因 没有确认收到消息)那么事务回滚,那么数据一致。

    为什么我们需要本地消息表呢(这个表增加不少的工作,而且是非业务的工作, 有些难以接受, 是否可以把这个工作作出通用的方法呢?)? 因为,我们可以保证消息发送出去,但是不是说消息发送出去就完了,因为消息可能被mq弄丢了啊等等。如果消息能够确保被mq 接收而且 永久保存,那么我们其实是不需要本地消息表的,本地消息表的作用,无非就是 永久化 消息。

    上面的步骤1 也可以分开为2步, 也就是没必要把 发送消息和数据库操作放一起

    1.生产方  向MQ来发送消息,消息内容是什么? 消息内容至少是包括了一些跨服务调用的参数。我们需要同步还是异步获取结果呢?一般选择 同步,获取结果r1,调用2;
    2.生产方  执行我们的业务,同时向本地消息表新增一行,主要需要记录消息内容,消息结果r1—— 这两个数据库操作必须要在同一个事务内部完成;
     

    我们需要同步还是异步获取结果呢?一般选择 同步,其实我们也可以把发消息的过程做成异步的:

    1 进行本地事务+本地消息表新增(需要在同一个事务),成功后 异步发消息
    或者 反掉顺序:
    1 异步发消息,然后 进行本地事务+本地消息表新增

    2 本地定时任务,检查本地消息表,看是否发生成功,怎么看呢?就是去mq peek一下消息是否存在,不存在则说明之前没有发送成功。否则本地消息表状态 更新为成功。同时考虑检查次数。

    我们后面可以具体讨论这个情况以及更多的具体的备选方案。

    说明: 这种方案的话,我们的每一个微服务就需要一张本地表,需要编程一些非业务的内容。

    正常的操作逻辑就是这样的,但是,这么多步骤,每一步都是可能出现失败的。失败不要紧,我们来看看:

    如何保证数据一致性的:

    如果1 失败,消息都发送不出去,或者发出去了,但是获取不到结果。两种情况都是个大问题,系统都用不了了,玩不下去了,得赶紧看看原因, 一般这种情况 也不会是程序逻辑错误,很可能网络问题了,比如网关发生变化了,ip 变化了,防火墙啊,或者是mq 本身问题了,比如mq或mq集群都挂掉了。虽然是大问题,但是没有事务发生,自然数据保持一致性。

    如果2 失败,表明事务回滚了,数据仍然保持一致。如果程序、业务逻辑正确,这种失败情况不应该出现, 罕见,不过也有可能是 数据库本身挂了,或者数据库 或应用程序 内存啊,容量啊 不够了。

    如果3 失败,不涉及数据操作,数据仍然保持一致。这种失败情况不应该出现,一般是后面步骤比如消息处理出错。

    如果4 失败,本地数据仍然保持一致,但是整体而言,数据已经不一致了! 那怎么办?那就重试。N次失败后发通知,然后人工处理。

    如果消费方 服务挂掉了呢? 那么也不要紧,消息是 未消费状态,消费方服务恢复之后 可以预期达到最终一致性,当然, 恢复之前确实是不一致了!消费方 服务 挂掉这种情况也少见,通常是可能是由于消费方所在的机器挂掉了,或者 消费方服务内存溢出啊等原因, 整个进程异常退出了。这个一般就是运维的责任了。 出现了则需要立即 运维介入,依据 具体原因或者 运维自动化处理,或者人工处理。

    备选方案


    生产方的第1、2步的时候,我们也可以这样做:

    1 同步发送消息( 消息内容其实不重要,简单记录一下生产方 业务情况即可,因为这个时候 我们的业务id 可能没有生成出来),成功后 记录本地消息表, 内容包括消息id, 业务基本数据. 调用2

    2 执行业务逻辑, 更新本地消息表,更新哪些内容呢? 就是 业务标明 业务执行状态 为成功。然后 如果有必要 再把业务内容 发送一条消息到mq。更新本地消息表和再次发送业务消息的顺序也可以倒过来。(这样做显得非常繁琐, 最后不要再次发送mq了)

    3 生产方本地启动 定时任务,扫描本地消息表,如果发现 有失败(包括未执行的)的情况,说明生产方的业务逻辑都执行失败了,那么 重新调用 2。

    —— 这个适合 生产方非常不稳定,生产方需要反复重试来保证成功 或者 生产方业务和 消费方业务需要并行运行 的情况。而且最好 生产方和消费方没有数据依赖的情况,也就是说, 仅仅是简单的 通知一下。

    —— 生产方业务没有成功,为什么消费方可以消费呢? 这样的情况也是有的, 我们期望他非常少。如果发生了,通过本地定时任务保证就好了。

    为什么  发消息要先于 任何数据操作?这样做是有好处的。因为我们需要mq 确认收到了消息,收到了才继续,否则会比较麻烦,没有继续的意义了,因为如果消息都没有发送成功,那么问题变得复杂起来,因为可能事务可以回滚,消息不能回滚。比如tx1成功,msg1发送失败,那么事务将回滚,然后tx1可以回滚,这时无大碍。但是,比如tx1成功,msg1发送成功,tx2失败,那么事务将回滚,然后tx1可以回滚,但是msg1是不能回滚的,这就比较麻烦了,你可能会说,我们先写本地日志吧,写日志成功后再发消息, 然后通过日志来比对是否发送消息成功。这样当然也可以,但是复杂度比较高。

    消费方的第三、四步的时候,我们也可以这样做:

    3.消费方  消费消息,同步调用4,4成功则删除消息,失败则重新消费,然后重复调用4; (需要mq 能够支持重复消费)
    4.消费方  怎么处理消息呢? 就是直接 执行对应的本地事务;

    或者我们也可以这样做:

    3.消费方  消费消息,然后 同步调用4,把4的成功或失败的结果 记录到本地消费消息表,写一条数据; (没有循环)
    4.消费方  怎么处理消息呢? 就是直接 执行对应的本地事务;
    5.消费方  本地运行定时任务,定时扫描 本地消费消息表,扫描到失败记录,根据失败的具体原因,重新调用4 (怎么调用呢? 可以这样,先把消息解析出来,获取具体的内容(也就是生产方提供的参数),然后获取方法4所在的service单例,然后使用消息内容作为参数 调用4。这里的4,肯定是有参数的,最好service类是单例的,而且不要充血模型);(记录本地消息的时候呢,我们也有多个方案,我们可以把消息的业务类型记录下来,然后根据业务类型找到service类和方法,也可以直接把service类和方法 记录下来。或者记录service类,然后方法作为类型记录下来。)

    —— 这种方案的话,我们的每一个微服务就需要两张本地表,一张是本地消费表,也就是本地消息生产表,一个是本地消息消费表,分别记录 生产和消费情况。然后还要 消费方的本地定时任务。。。我看到 很多一些博客都这样做, 我感觉这样更加麻烦了, 因为还要 定时任务。。。

    上面的3或者我们也可以这样做:

    3.消费方  消费消息,然后 先记录到本地消费消息表,重试次数为0,再异步调用4,再删除消息;// 过程如果出错,那么根据情况 可能需要重新消费消息
    4.消费方  怎么处理消息呢? 就是直接 执行对应的事务, 同时更新 本地消费消息表的重试次数为1、状态为成功 —— 这两个操作应该放入一个事务内完成
    5 消费方  本地的定时器,定时扫描本地消费消息表;发现失败的记录则重试。重试成功则重试次数为2、状态为成功;如果重试失败呢?那么需要改为 重试次数为1、状态为失败,以此类推。如果 重试次数大于3, 那么发邮件或短信通知,然后可能需要人工介入。

    总结

    生产方 为什么会失败? 消息发送都失败了,是否需要消息再推送一次?

    消费方 处理消息为什么会失败? 从业务角度来考虑, 可能就是 资源不够了,资源不满足条件了。 像这种情况,我们也可以在前期做一些预处理、校验啊, 即所谓的“资源预留”,也就是 给资源加锁。 比如 发起者 首先要 同步通知 消费者 先预留资源, ok后才 进行下一步,如发送消息之类的。 这里的检验,是否可以异步? 是否 一定 需要一个 本地的 定时任务调度? 具体情况具体分析。

    另外,如果消费方有多个,各个消费方没有依赖顺序,那么它们可以同时去消费,如果有依赖顺序,那么我们需要做一个 调用链, 也就是 消费者也生产消息,消费者也同时是生产者。

    总之,分布式高并发环境下,我们需要仔细设计,仔细权衡每个方法调用,是异步还是同步, 是否需要设计成幂等, 是否需要写数据库,是否需要mq,是否需要拆分业务,是否需要多个表,是否需要多个数据库,是否需要这样的业务流程?

     每一步都可能出错,要保证稳健的程序,我们需要考虑很多很多,特别需要仔细考虑当前方法是应该自己处理还是抛出,考虑各种问题,要做最全面而且详细的错误处理。

     参考:

    https://www.cnblogs.com/bigben0123/p/9453830.html

    https://segmentfault.com/a/1190000012415698

    http://www.cnblogs.com/zhangliwei/p/9984129.html

  • 相关阅读:
    .net系统自学笔记——自定义特性及反射
    .net系统自学笔记——内存管理与指针
    .net系统自学笔记——动态语言扩展(又一个没听过没学过的,空,以后会了再补充吧)
    .net系统自学笔记——Linq
    思维的惰性
    论演员的自我修养2
    职场有影帝出没,屌丝们请当心!
    论演员的自我修养
    道与术
    关注细节但不陷入细节
  • 原文地址:https://www.cnblogs.com/FlyAway2013/p/10124283.html
Copyright © 2011-2022 走看看