事务 跨库事务 分布式事务及解决方案

什么是事务?

本地事务

事务传播行为:

事务的四大特性 ACID

并发事务产生的问题可以分为4类

事务的隔离级别

什么是分布式事务

分布式涉及到的原理:

CAP原理:

BASE理论

柔性事务和刚性事务

柔性事务分为:

分布式一致性协议

XA接口      

Jta规范

两阶段提交协议 2PC

三阶段提交协议 3PC

XA与TCC 的比较:

分布式事务解决方案

方案1:全局事务(DTP模型)

分布式跨库事务:

方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)

方案3 .可靠消息最终一致性

方案4 .最大努力通知方案:


 


什么是事务?

事务由一组指令操作构成,我们希望这组操作能够全部正确执行,如果这一组操作中的任意一个步骤发生错误,那么就需要回滚之前已经完成的操作。也就是同一个事务中的所有操作要么全都正确执行要么全都不要执行

方案:

悲观锁 for update

乐观锁 version 字段版本控制

 


 
 
  1. @Autowired
  2. private TransactionTemplate txTemplate;
  3. public String sendOrderByTxTemplate(Order order) {
  4. Long orderId = order.getId();
  5. Boolean lockStatus = txTemplate.execute(new TransactionCallback< Boolean>() {
  6. @Override
  7. public Boolean doInTransaction(TransactionStatus transactionStatus) {
  8. Order order = new Order();
  9. order.setId(orderId);
  10. order.setOrderStatus( "4"); //处理中...
  11. order.setVersion( 0);
  12. return 1 == orderMapper.updateByVersion(order);
  13. }
  14. });
  15. //j只有第一个进来的线程返回true
  16. //基于状态机的乐观锁..
  17. if (lockStatus) { //只允许第一个线程调用发货接口
  18. String flag = transService.invoke(url, orderId); //10s
  19. //只作用于代码块<===
  20. txTemplate.execute(new TransactionCallback< Boolean>() {
  21. @Override
  22. public Boolean doInTransaction(TransactionStatus transactionStatus) {
  23. Order orderFin = new Order();
  24. orderFin.setId(orderId);
  25. orderFin.setOrderStatus(flag); //处理中...
  26. orderMapper.update(orderFin);
  27. return null;
  28. }
  29. });
  30. }

本地事务

众所周知,数据库实现本地事务,也就是在同一个数据库中,你可以允许一组操作要么全都正确执行,要么全都不执行。这里特别强调了本地事务,也就是目前的数据库只能支持同一个数据库中的事务

事务传播行为:

事务的四大特性 ACID

我们首先看下一些书籍中的官方描述:

数据库:

原子性(Atomicity) 
原子性要求: 事务是一个不可分割的执行单元,事务中的所有操作要么全都正确执行,要么全都不执行。

一致性 (Consistency)
一致性要求: 事务在开始前结束后,数据库的完整性约束没有被破坏

隔离性 (Isolation)
隔离性要求: 事务的执行是相互独立的,它们不会相互干扰,一个事务不会看另一个正在运行过程中的事务的数据

持久性(Durability) 
持久性要求: 一个事务完成之后,事务的执行结果必须是持久化保存的。即使数据库发生崩溃,在数据库恢复后事务提交的结果仍然不会丢失

注意:事务只能保证数据库的高可靠性,即数据库本身发生问题后,事务提交后的数据仍然能恢复;而如果不是数据库本身的故障,如硬盘损坏了,那么事务提交的数据可能就丢失了。这属于『高可用性』的范畴。因此,事务只能保证数据库的『高可靠性』,而『高可用性』需要整个系统共同配合实现。

--《Spring攻略》

  • 原子性(Atomictiy):事务是一个包含一系列操作的原子操作。事务的原子性确保这些操作全部完成或者全部失败。
  • 一致性(Consistency):一旦事务的所有操作结束,事务就被提交。然后你的数据和资源将处于遵循业务规则的一直状态。
  • 隔离性(Isolation):因为同时在相同数据集上可能有许多事务处理,每个事务应该与其他事务隔离,避免数据破坏。
  • 持久性(Durability):一旦事务完成,他的结果应该能够承受任何系统错误(想象一下在事务提交过程中机器的电源被切断的情况)。通常,事务的结果被写入持续性存储。

 

--《企业应用架构模式》

  • 原子性(Atomictiy):在一个事务里,动作序列的每一个步骤都必须是要么全部成功,要么所有的工作都将回滚。部分完成不是一个事务的概念。
  • 一致性(Consistency):在事务开始和完成的时候,系统的资源都必须处于一致的、没有被破坏的状态。
  • 隔离性(Isolation):一个事务,直到它被成功提交之后,它的结果对于任何其他的事务才是可见的。
  • 持久性(Durability):一个已提交事务的任何结果都必须是永久性的,即“在任何系统崩溃的情况下都能保存下来”。

那这四个属性,我们自己到底该如何理解呢。个人理解如下:
如果给事务下一个定义:事务是一个有边界的指令操作序列,开始和结束都有明确的定义。

  1. 原子性
    举例1-比如现在有一个事务,包含3个sql语句(工作序列,或者是指令序列),sql-1,sql-2,sql-3,这3个sql语句,每一个在执行的时候,都是一个单元,这个单元的执行结果,有且仅有两种可能:成功和失败。
    举例2-再比如,我从账户1中转出1000人民币到账户2,当我从账户1中把钱转出来之后,系统就崩溃了。那么系统应该将我的账户状态置成我还没有转出钱之前的状态。
  2. 一致性
    举例1-有一个在线商务网站系统,有两张表,一张用户账户表(用户名、个人余额),一张商品库存表(商品ID,库存数量),用户花费30元购买1件商品,商品库存减1,账户余额减30.那么这样的结果就是一致的。否则,如果商品库存减1,账户余额没有变化,那么这样的结果就是不一致的。
    举例2-银行账户转账的例子,也一样,账户1中的钱减少1000,账户2中的钱就增加1000,这样的是一致的,否则不一致。
  3. 隔离性
    没有隔离性就没有一致性
    举例1-我们现在有两个事务方法,一个方法是查询数据的库存,一个是购买下单,那么这两个事务方法应该互不影响,不然会造成一系列的问题,个人一直认为,事务造成的下面这些问题是跟并发密不可分的,没有并发操作,单一的请求事务是不会有这样的问题的。

    并发事务产生的问题可以分为4类

    <p>我们先来看一下在不同的隔离级别下,数据库可能会出现的问题:</p>
    
    <p><span style="color:#f33b45;"><strong>脏读&nbsp; (我未提交你就读)</strong></span><br><span style="color:#3399ea;"><strong>事务1</strong></span>读到<span style="color:#3399ea;"><strong>事务2尚未提交un-commit</strong></span>的事务中的<span style="color:#3399ea;"><strong>数据</strong></span>。该数据<span style="color:#3399ea;"><strong>可能会被回滚从而失效</strong></span>。&nbsp;<br>
    如果<span style="color:#3399ea;"><strong>事务1</strong></span>拿着<span style="color:#3399ea;"><strong>失效的数据</strong>去处理</span>那就<span style="color:#3399ea;"><strong>发生错误</strong></span>了。</p>
    
    <p><span style="color:#f33b45;"><strong>更新丢失&nbsp; (我改你也改)</strong></span><br>
    当有<span style="color:#3399ea;">两个<strong>并发执行的事务</strong>,<strong>更新同一行数据</strong></span>,那么有可能<span style="color:#3399ea;"><strong>一个事务会把另一个事务</strong>的<strong>更新覆盖掉</strong></span>。&nbsp;<br>
    当数据库<span style="color:#3399ea;"><strong>没有加任何锁操作</strong></span>的情况下<span style="color:#3399ea;"><strong>会发生</strong></span>。</p>
    
    <p><strong>不可重复读 <span style="color:#f33b45;">(我读中你捣乱[2])</span></strong><br>
    不可重复度的含义:一个事务对同一行数据<span style="color:#3399ea;">读了两次</span>,却得到了<span style="color:#3399ea;">不同的结果</span>。它具体分为如下两种情况:</p>
    
    <p><span style="color:#f33b45;"><strong>虚读</strong></span>:在<span style="color:#3399ea;">事务1两次</span>读取<span style="color:#3399ea;">同一记录</span>的<span style="color:#3399ea;">过程中</span>,<span style="color:#3399ea;">事务2</span>对<span style="color:#f33b45;">该记录</span>进行<span style="color:#f33b45;">修改操作</span>,从而<span style="color:#3399ea;">事务1第二次</span>读到了<span style="color:#3399ea;">不一样</span>的<span style="color:#3399ea;">记录</span>。<br><span style="color:#f33b45;"><strong>幻读</strong></span>:<span style="color:#3399ea;">事务1</span>在<span style="color:#3399ea;">两次</span>查询的<span style="color:#3399ea;">过程中</span>,<span style="color:#3399ea;">事务2</span>对<span style="color:#f33b45;">该表</span>进行了<span style="color:#f33b45;">插入、删除操作</span>,从而<span style="color:#3399ea;">事务1第二次</span>查询的<span style="color:#3399ea;">结果</span>发生了<span style="color:#3399ea;">变化</span>。<br>
    &nbsp;</p>
    
    <p><strong>不可重复读 与 脏读 的区别?&nbsp;</strong><br><strong>脏读读到</strong>的是<strong>尚未提交的数据</strong>,而<strong>不可重复读</strong>读到的是<strong>已经提交的数据</strong>,只不过在两次读的过程中数据<strong>被另一个事务改过</strong>了。</p>
    
    <h3 id="%E4%BA%8B%E5%8A%A1%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB"><a name="t5"></a><a name="t5"></a>事务的隔离级别</h3>
    
    <p>对事务的隔离性做一个详细的解释。</p>
    
    <p>在事务的<span style="color:#3399ea;"><strong>四大特性ACID</strong></span>中,<strong><span style="color:#3399ea;">要求的隔离性</span></strong>是一种严格意义上的隔离,也就是<span style="color:#3399ea;"><strong>多个事务是串行执行</strong></span>的,<span style="color:#3399ea;"><strong>彼此之间互不干扰</strong></span>。这确实能够完全<strong>保证数据的安全性</strong>,<strong>但</strong>在<strong>实际业务系统</strong>中,这种方式<strong>性能不高</strong>。<strong>因此</strong>,数据库定义了<strong>四种隔离级别</strong>,<strong>隔离级别</strong>和数据库的<strong>性能</strong>是<strong>呈反比</strong>的,<strong>隔离级别越低</strong>,数据库<strong>性能越高</strong>,而隔离<strong>级别越高</strong>,数据库<strong>性能越差</strong>。</p>
    
    <p>数据库的<strong>四种隔离级别</strong><br>
    数据库一共有如下四种隔离级别:</p>
    
    <p><strong>从低到高</strong></p>
    
    <p><strong><span style="color:#3399ea;">Read uncommitted 读未提交</span></strong>&nbsp; <strong><span style="color:#f33b45;">(①</span><span style="color:#86ca5e;">我写</span><span style="color:#f33b45;">未完成时,你禁写</span><span style="color:#86ca5e;">可读</span><span style="color:#f33b45;">; ②我读时,你</span><span style="color:#86ca5e;">可写可读</span><span style="color:#f33b45;">)</span></strong><br>
    在该级别下,<span style="color:#3399ea;"><strong>一个事务</strong></span>对<span style="color:#3399ea;"><strong>一行数据修改</strong></span>的<span style="color:#3399ea;"><strong>过程中</strong></span>,<strong><span style="color:#3399ea;">不允许另一个事务</span></strong>对<span style="color:#3399ea;"><strong>该行数据</strong></span>进行<span style="color:#3399ea;"><strong>修改操作</strong></span>,<strong><span style="color:#3399ea;">但允许</span></strong>对<strong><span style="color:#3399ea;">该行数据</span>进行<span style="color:#3399ea;">读操作</span></strong>。&nbsp;<br>
    因此本级别下,<strong>不会</strong>出现<strong>更新丢失,</strong>但会出现<strong>脏读、不可重复读(虚读/幻读)</strong>问题。</p>
    
    <p><span style="color:#3399ea;"><strong>Read committed 读已提交&nbsp; </strong></span><span style="color:#f33b45;"><strong>(</strong></span><strong><span style="color:#f33b45;">①我写</span></strong><span style="color:#f33b45;"><strong>时未提交,你禁写读;</strong></span><strong><span style="color:#f33b45;">②</span><span style="color:#f33b45;"><strong> </strong></span><span style="color:#f33b45;">我读</span></strong><span style="color:#f33b45;"><strong>时,</strong></span><strong><span style="color:#f33b45;">你</span><span style="color:#86ca5e;">可写可读</span><span style="color:#f33b45;">,[</span></strong><span style="color:#f33b45;"><strong>不管[爱咋咋地],想读写都可以])</strong></span><br>
    在该级别下,<span style="color:#3399ea;"><strong>未提交的写事务</strong></span><strong>不允许</strong>其他事务<span style="color:#3399ea;">访问该行</span>,因此<strong>不会</strong>出现<strong>脏读</strong>;但是读取数据的事务允许其他事务的访问该行数据,因此<strong>会</strong>出现<strong>不可重复读(虚读/幻读)</strong>的情况。</p>
    
    <p><strong><span style="color:#3399ea;">Repeatable read 可重复读&nbsp; </span><span style="color:#f33b45;">(①</span><span style="color:#f33b45;">我写</span><span style="color:#f33b45;">时,你禁</span></strong><span style="color:#f33b45;"><strong>写读</strong></span><strong><span style="color:#f33b45;">; ②</span><span style="color:#86ca5e;">我读</span><span style="color:#f33b45;">时,你</span><span style="color:#7c79e5;">禁写</span><span style="color:#86ca5e;">可读</span><span style="color:#f33b45;">) [</span><span style="color:#3399ea;">MySQL默认隔离级别:可重复读</span><span style="color:#f33b45;">]</span></strong><br>
    在该级别下,<strong>读事务</strong><span style="color:#3399ea;">禁止</span><strong>写事务</strong>,<strong>但</strong>允许<strong>读事务</strong>,因此<strong>不会</strong>出现同一事务两次读到<strong>不同</strong>的数据的情况(不可重复读<strong>(虚读/幻读)</strong>),且<strong>写事务</strong><span style="color:#3399ea;">禁止</span><strong>其他一切事务</strong>。</p>
    
    <p><span style="color:#3399ea;"><strong>Serializable 序列化(串行化)&nbsp; </strong></span><span style="color:#f33b45;"><strong>(①</strong></span><strong><span style="color:#f33b45;">我写</span></strong><span style="color:#f33b45;"><strong>时和②</strong></span><strong><span style="color:#f33b45;">我读</span></strong><span style="color:#f33b45;"><strong>时,你禁止一切)</strong></span><br>
    该级别要求<strong>所有事务</strong>都<strong>必须串行执行</strong>,因此能<strong>避免一切</strong>因<strong>并发</strong>引起的<strong>问题</strong>,但<strong>效率很低</strong>。</p>
    
    <p>隔离<strong>级别越高</strong>,越能保证<strong>数据</strong>的<strong>完整性和一致性</strong>,<strong>但</strong>是对<strong>并发性能</strong>的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够<strong>避免脏读取</strong>,而且具有较好的<strong>并发性能</strong>。尽管它<strong>会导致不可重复读、幻读和第二类丢失更新</strong>这些并发问题,在可能出现这类问题的个别场合,可以<strong>由应用程序</strong>采用<strong>悲观锁</strong>或<strong>乐观锁</strong>来控制。</p>
    
    <p>在《企业应用架构模式》一书中,有一句话描述就是:处理并发最主要的工具就是事务。</p>
    </blockquote>
    </li>
    <li>持久性<br>
    对于数据库来讲,我的理解是这样,当我把sql-1、sql-2、sql-3提交之后,这个结果就一定会保存到数据库中,那如果提交-到写入这中间,突然断电,也没有关系,数据库服务器在重新启动之后一样会把数据写入磁盘,应该是通过日志的方式-仅个人理解。<br>
    数据库一般都是通过事务日志的方式,<strong>write-ahead transaction log</strong>来保证持久性。write-ahead transaction log的意思是,事务中对数据库的改变在写入到数据库之前,<strong>首先写入到事务日志</strong>中。而事务日志是按照顺序排号的(LSN)。当数据库崩溃或者服务器断点时,重启动数据库,<strong>首先会检查日志顺序号</strong>,将本应对数据库<strong>做更改而未做的部分持久化到数据库</strong>,从而<strong>保证了持久性</strong>.</li>
    

什么是分布式事务

  • 分布式事务就是指事务的资源分别位于不同的分布式系统的不同节点之上的事务
  • 上面所介绍的事务都是基于单数据库的本地事务,目前的数据库仅支持单库事务,并不支持跨库事务。而随着微服务架构的普及,一个大型业务系统往往由若干个子系统构成,这些子系统又拥有各自独立数据库。往往一个业务流程需要由多个子系统共同完成,而且这些操作可能需要在一个事务中完成。在微服务系统中,这些业务场景是普遍存在的。此时,我们就需要在数据库之上通过某种手段,实现跨数据库事务支持,这也就是大家常说的“分布式事务”。
  • 分布式事务涉及到操作多个数据库的事务,分布式事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于分布式系统的不同节点上。一个分布式事务可以看作是由多个分布式的操作序列组成的,通常可以把这一系列分布式的操作序列称为子事务,但由于在分布式事务中,各个子事务的执行是分布式的,因此要实现一种能够保证 ACID 特性的分布式事务处理系统就显得格外复杂。

分布式事务产生的原因

  • 数据库分库分表

​    在单库单表场景下,当业务数据量达到单库单表的极限时,就需要考虑分库分表,将之前的单库单表拆分成多库多表;分库分表之后,原来在单个数据库上的事务操作,可能就变成跨多个数据库的操作,此时就需要使用分布式事务。

  • 业务服务化

​    业务服务化即业务按照面向服务(SOA)的架构拆分整个网站系统,所有的业务操作都以服务的方式对外发布,跨应用、跨服务的操作需要使用分布式事务才能保证数据的一致性。

 

这里举一个分布式事务的典型例子——用户下单过程。 
当我们的系统采用了微服务架构后,一个电商系统往往被拆分成如下几个子系统:商品系统、订单系统、支付系统、积分系统.仓储服务等。整个下单的过程如下:

用户通过商品系统浏览商品,他看中了某一项商品,便点击下单
此时订单系统会生成一条订单
订单创建成功后,支付系统提供支付功能
当支付完成后,由积分系统为该用户增加积分
上述步骤2、3、4需要在一个事务中完成。对于传统单体应用而言,实现事务非常简单,只需将这三个步骤放在一个方法A中,再用Spring的@Transactional注解标识该方法即可。Spring通过数据库的事务支持,保证这些步骤要么全都执行完成,要么全都不执行。但在这个微服务架构中,这三个步骤涉及三个系统,涉及三个数据库,此时我们必须在数据库和应用系统之间,通过某项黑科技,实现分布式事务的支持。

分布式涉及到的原理:

CAP原理:

  

 由于对系统或者数据进行了拆分,我们的系统不再是单机系统,而是分布式系统,针对分布式系统的CAP原理包含如下三个元素:

概述
  分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。

Consistency 一致性
  一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。

Availability 可用性
  可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。

Parttition Tolerance 分区容忍性
  网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。

权衡
  在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
通常采取的策略是保证可用性,牺牲部分一致性,只确保最终一致性。

       当然,牺牲一致性,并不是完全不管数据的一致性,否则数据是混乱的,那么系统可用性再高分布式再好也没有了价值。牺牲一致性,只是不再要求关系型数据库中的强一致性,而是只要系统能达到最终一致性即可,考虑到客户体验,这个最终一致的时间窗口,要尽可能的对用户透明,也就是需要保障“用户感知到的一致性”。通常是通过数据的多份异步复制来实现系统的高可用和数据的最终一致性的,“用户感知到的一致性”的时间窗口则取决于数据复制到一致状态的时间。

  

BASE理论


        BASE理论是指,Basically Available(基本可用)、Soft-state( 软状态/柔性事务)、Eventual Consistency(最终一致性)。是基于CAP定理演化而来,是对CAP中一致性和可用性权衡的结果。核心思想:即使无法做到强一致性,但每个业务根据自身的特点,采用适当的方式来使系统达到最终一致性。

1、基本可用  BA:(Basically Available ):

       指分布式系统在出现故障的时候,允许损失部分可用性保证核心可用。但不等价于不可用。比如:搜索引擎0.5秒返回查询结果,但由于故障,2秒响应查询结果;网页访问过大时,部分用户提供降级服务等。简单来说就是基本可用

2、软状态  S:( Soft State):

        软状态是指允许系统存在中间状态,并且该中间状态不会影响系统整体可用性。即允许系统在不同节点间副本同步的时候存在延时。简单来说就是状态可以在一段时间内不同步

3、最终一致性  E:(Eventually Consistent ):

       系统中的所有数据副本经过一定时间后,最终能够达到一致的状态不需要实时保证系统数据的强一致性。最终一致性是弱一致性的一种特殊情况。BASE理论面向的是大型高可用可扩展的分布式系统,通过牺牲强一致性来获得可用性。ACID是传统数据库常用的概念设计,追求强一致性模型。简单来说就是在一定的时间窗口内, 最终数据达成一致即可。

柔性事务和刚性事务

 

  1. 刚性事务满足ACID理论
  2. 柔性事务满足BASE理论(基本可用,最终一致)

柔性事务分为:

两阶段型 2PC(two phase commitment)
补偿型TCC (try confirn/caclen
异步确保型 (通过信息中间件)
最大努力通知型。
 

酸碱平衡
ACID能够保证事务的强一致性,即数据是实时一致的。这在本地事务中是没有问题的,在分布式事务中,强一致性会极大影响分布式系统的性能,因此分布式系统中遵循BASE理论即可。但分布式系统的不同业务场景对一致性的要求也不同。如交易场景下,就要求强一致性,此时就需要遵循ACID理论,而在注册成功后发送短信验证码等场景下,并不需要实时一致,因此遵循BASE理论即可。因此要根据具体业务场景,在ACID和BASE之间寻求平衡。

分布式一致性协议


XA接口
      

XA是由X/Open组织提出的分布式事务的规范。XA规范主要定义了(全局)事务管理器(Transaction Manager)(局部)资源管理器(Resource Manager)之间的接口。XA接口是双向的系统接口,在事务管理器(Transaction Manager)以及一个或多个资源管理器(Resource Manager)之间形成通信桥梁。XA之所以需要引入事务管理器是因为,在分布式系统中,从理论上讲(参考Fischer等的论文),两台机器理论上无法达到一致的状态,需要引入一个单点进行协调事务管理器控制着全局事务,管理事务生命周期,并协调资源资源管理器负责控制和管理实际资源(如数据库或JMS队列)

Jta规范


       作为java平台上事务规范JTA(Java Transaction API)也定义了对XA事务的支持,实际上,JTA是基于XA架构上建模的,在JTA 中,事务管理器抽象为javax.transaction.TransactionManager接口,并通过底层事务服务(即JTS)实现。像很多其他的java规范一样,JTA仅仅定义了接口,具体的实现则是由供应商(如J2EE厂商)负责提供,目前JTA的实现主要由以下几种:


 
 
  1. 1 .J2EE容器所提供的 JTA实现( JBoss)
  2. 2.独立的 JTA实现:如 JOTMAtomikos.这些实现可以应用在那些不使用 J2EE应用服务器的环境里
  3. 用以提供分布事事务保证。如 Tomcat, Jetty以及普通的 java应用。

两阶段提交协议 2PC

两阶段提交协议 2PC
分布式系统的一个难点是如何保证架构下多个节点在进行事务性操作的时候保持一致性。为实现这个目的,二阶段提交算法的成立基于以下假设:

该分布式系统中,存在一个节点作为协调者(Coordinator),其他节点作为参与者(Cohorts)。且节点之间可以进行网络通信。
所有节点都采用预写式日志,且日志被写入后即被保持在可靠的存储设备上,即使节点损坏不会导致日志数据的消失
所有节点不会永久性损坏,即使损坏后仍然可以恢复


1. 第一阶段(投票(准备)阶段)

协调者节点向所有参与者节点询问是否可以执行提交操作(vote),并开始等待各参与者节点的响应。
参与者节点执行询问发起为止的所有事务操作,并将Undo信息和Redo信息写入日志。(注意:若成功这里其实每个参与者已经执行了事务操作)
各参与者节点响应协调者节点发起的询问。如果参与者节点的事务操作实际执行成功,则它返回一个”同意”消息;如果参与者节点的事务操作实际执行失败,则它返回一个”中止”消息。
2. 第二阶段(提交执行阶段)

当协调者节点从所有参与者节点获得的相应消息都为”同意”时:

协调者节点向所有参与者节点发出”正式提交(commit)”的请求。
参与者节点正式完成操作,并释放在整个事务期间内占用的资源。
参与者节点向协调者节点发送”完成”消息。
协调者节点受到所有参与者节点反馈的”完成”消息后,完成事务。
如果任一参与者节点在第一阶段返回的响应消息为”中止”,或者 协调者节点在第一阶段的询问超时之前无法获取所有参与者节点的响应消息时:

协调者节点向所有参与者节点发出”回滚操作(rollback)”的请求。
参与者节点利用之前写入的Undo信息执行回滚,并释放在整个事务期间内占用的资源。
参与者节点向协调者节点发送”回滚完成”消息。
协调者节点受到所有参与者节点反馈的”回滚完成”消息后,取消事务。
不管最后结果如何,第二阶段都会结束当前事务。

二阶段提交看起来确实能够提供原子性的操作,但是不幸的事,二阶段提交还是有几个缺点的:

执行过程中,所有参与节点都是事务阻塞型的。当参与者占有公共资源时,其他第三方节点访问公共资源不得不处于阻塞状态。
参与者发生故障。协调者需要给每个参与者额外指定超时机制,超时后整个事务失败。(没有多少容错机制)
协调者发生故障。参与者会一直阻塞下去。需要额外的备机进行容错。(这个可以依赖后面要讲的Paxos协议实现HA)
二阶段无法解决的问题:协调者再发出commit消息之后宕机,而唯一接收到这条消息的参与者同时也宕机了。那么即使协调者通过选举协议产生了新的协调者,这条事务的状态也是不确定的,没人知道事务是否被已经提交。
为此,Dale Skeen和Michael Stonebraker在“A Formal Model of Crash Recovery in a Distributed System”中提出了三阶段提交协议(3PC)。

三阶段提交协议 3PC


与两阶段提交不同的是,三阶段提交有两个改动点。

引入超时机制。同时在协调者参与者中都引入超时机制
在第一阶段和第二阶段中插入一个准备阶段保证了在最后提交阶段之前各参与节点状态一致的。
也就是说,除了引入超时机制之外,3PC把2PC的准备阶段再次一分为二,这样三阶段提交就有CanCommit、PreCommit、DoCommit三个阶段。

1. CanCommit阶段

3PC的CanCommit阶段其实和2PC的准备阶段很像。协调者向参与者发送commit请求,参与者如果可以提交就返回Yes响应,否则返回No响应。

事务询问 
协调者向参与者发送CanCommit请求。询问是否可以执行事务提交操作。然后开始等待参与者的响应。
响应反馈 
参与者接到CanCommit请求之后,正常情况下,如果其自身认为可以顺利执行事务,则返回Yes响应,并进入预备状态。否则反馈No
2. PreCommit阶段

协调者根据参与者的反应情况来决定是否可以记性事务的PreCommit操作。根据响应情况,有以下两种可能。 
假如协调者从所有的参与者获得的反馈都是Yes响应,那么就会执行事务的预执行。

发送预提交请求 
协调者向参与者发送PreCommit请求,并进入Prepared阶段。

事务预提交 
参与者接收到PreCommit请求后,会执行事务操作,并将undo和redo信息记录到事务日志中。

响应反馈 
如果参与者成功的执行了事务操作,则返回ACK响应,同时开始等待最终指令。

假如有任何一个参与者向协调者发送了No响应,或者等待超时之后,协调者都没有接到参与者的响应,那么就执行事务的中断。

发送中断请求 
协调者向所有参与者发送abort请求。

中断事务 
参与者收到来自协调者的abort请求之后(或超时之后,仍未收到协调者的请求),执行事务的中断。

3. doCommit阶段 
该阶段进行真正的事务提交,也可以分为以下两种情况。

  • 3.1 执行提交

发送提交请求 
协调接收到参与者发送的ACK响应,那么他将从预提交状态进入到提交状态。并向所有参与者发送doCommit请求。
事务提交 
参与者接收到doCommit请求之后,执行正式的事务提交。并在完成事务提交之后释放所有事务资源。
响应反馈 
事务提交完之后,向协调者发送Ack响应。
完成事务 
协调者接收到所有参与者的ack响应之后,完成事务。

  • 3.2 中断事务 

协调者没有接收到参与者发送的ACK响应(可能是接受者发送的不是ACK响应,也可能响应超时),那么就会执行中断事务。

发送中断请求 
协调者向所有参与者发送abort请求

事务回滚 
参与者接收到abort请求之后,利用其在阶段二记录的undo信息来执行事务的回滚操作,并在完成回滚之后释放所有的事务资源。

反馈结果 
参与者完成事务回滚之后,向协调者发送ACK消息

中断事务 
协调者接收到参与者反馈的ACK消息之后,执行事务的中断。

 

2PC与3PC提交区别
增加了一个询问阶段,询问阶段可以确保尽可能早的发现无法执行操作而需要中止的行为,但是它并不能发现所有的这种行为,只会减少这种情况的发生在准备阶段以后,协调者和参与者执行的任务中都增加了超时,一旦超时,协调者和参与者都继续提交事务,默认为成功,这也是根据概率统计上超时后默认成功的正确性最大。
三阶段提交协议与两阶段提交协议相比,具有如上的优点,但是一旦发生超时,系统仍然会发生不一致,只不过这种情况很少见罢了,好处就是至少不会阻塞和永远锁定资源。

 

XA与TCC 的比较:

 

分布式事务解决方案

 

方案1:全局事务(DTP模型)


全局事务基于DTP模型实现。DTP是由X/Open组织提出的一种分布式事务模型——X/Open Distributed Transaction Processing Reference Model。它规定了要实现分布式事务,需要三种角色:

AP:Application 应用系统 
它就是我们开发的业务系统,在我们开发的过程中,可以使用资源管理器提供的事务接口来实现分布式事务。

TM:Transaction Manager 事务管理器

分布式事务的实现由事务管理器来完成,它会提供分布式事务的操作接口供我们的业务系统调用。这些接口称为TX接口。
事务管理器还管理着所有的资源管理器,通过它们提供的XA接口来同一调度这些资源管理器,以实现分布式事务。
DTP只是一套实现分布式事务的规范,并没有定义具体如何实现分布式事务,TM可以采用2PC、3PC、Paxos等协议实现分布式事务。
RM:Resource Manager 资源管理器

能够提供数据服务的对象都可以是资源管理器,比如:数据库、消息中间件、缓存等。大部分场景下,数据库即为分布式事务中的资源管理器。
资源管理器能够提供单数据库的事务能力,它们通过XA接口,将本数据库的提交、回滚等能力提供给事务管理器调用,以帮助事务管理器实现分布式的事务管理。
XA是DTP模型定义的接口,用于向事务管理器提供该资源管理器(该数据库)的提交、回滚等能力。
DTP只是一套实现分布式事务的规范,RM具体的实现是由数据库厂商来完成的。
有没有基于DTP模型的分布式事务中间件?

参考文献
大规模SOA系统中的分布事务处理_程立
Life beyond Distributed Transactions: an Apostate’s Opinion
关于如何实现一个TCC分布式事务框架的一点思考
How can a requestor ensure a consistent outcome across multiple, independent providers
关于分布式事务、两阶段提交协议、三阶提交协议
Three-phase commit protocol

分布式跨库事务:

pom依赖:

DataSource:

事务过程:

对atomikos的最终提交commit的模拟过程:

 

方案2 .TCC 两阶段补偿方案(Try -- Confirm/Cancel)


 

TCC即为Try Confirm Cancel,它属于补偿型分布式事务。顾名思义,TCC实现分布式事务一共有2个步骤:

 

1 预留资源阶段

Try:尝试待执行的业务 
这个过程并未执行业务,只是完成所有业务的一致性检查,并预留好执行所需的全部资源

2 确认资源阶段

  • Confirm:执行业务 

这个过程真正开始执行业务,由于Try阶段已经完成了一致性检查,因此本过程直接执行,

而不做任何检查。并且在执行的过程中,会使用到Try阶段预留的业务资源。

  • Cancel:取消执行的业务 

若业务执行失败,则进入Cancel阶段,它会释放所有占用的业务资源,并回滚Confirm阶段执行的操作。

TCC原理:

 

案例:

优缺点及使用场景:

TCC框架:

TCC全局事务必须基于RM本地事务来实现全局事务
TCC服务是由Try -- Confirm/Cancel业务构成的, 
其Try/Confirm/Cancel业务在执行时,会访问资源管理器(Resource Manager,下文简称RM)来存取数据。这些存取操作,必须要参与RM本地事务,以使其更改的数据要么都commit,要么都rollback。

 

再考虑一下如下场景:A==>B转账,余额系统和红包系统是两个独立的系统

假设服务B没有基于RM本地事务(以RDBS为例,可通过设置auto-commit为true来模拟),那么一旦[B:Try]操作中途执行失败,TCC事务框架后续决定回滚全局事务时,该[B:Cancel]则需要判断[B:Try]中哪些操作已经写到DB、哪些操作还没有写到DB:假设[B:Try]业务有5个写库操作,[B:Cancel]业务则需要逐个判断这5个操作是否生效,并将生效的操作执行反向操作。

不幸的是,由于[B:Cancel]业务也有n(0<=n<=5)个反向的写库操作,此时一旦[B:Cancel]也中途出错,则后续的[B:Cancel]执行任务更加繁重。因为,相比第一次[B:Cancel]操作,后续的[B:Cancel]操作还需要判断先前的[B:Cancel]操作的n(0<=n<=5)个写库中哪几个已经执行、哪几个还没有执行,这就涉及到了幂等性问题。而对幂等性的保障,又很可能还需要涉及额外的写库操作,该写库操作又会因为没有RM本地事务的支持而存在类似问题。。。可想而知,如果不基于RM本地事务,TCC事务框架是无法有效的管理TCC全局事务的。

反之,基于RM本地事务的TCC事务,这种情况则会很容易处理:[B:Try]操作中途执行失败,TCC事务框架将其参与RM本地事务直接rollback即可。后续TCC事务框架决定回滚全局事务时,在知道“[B:Try]操作涉及的RM本地事务已经rollback”的情况下,根本无需执行[B:Cancel]操作。

换句话说,基于RM本地事务实现TCC事务框架时,一个TCC型服务的cancel业务要么执行,要么不执行,不需要考虑部分执行的情况。

TCC事务框架应该提供Confirm/Cancel服务的幂等性保障
一般认为,服务的幂等性,是指针对同一个服务的多次(n>1)请求和对它的单次(n=1)请求,二者具有相同的副作用

在TCC事务模型中,Confirm/Cancel业务可能会被重复调用,其原因很多。比如,全局事务在提交/回滚时会调用各TCC服务的Confirm/Cancel业务逻辑。执行这些Confirm/Cancel业务时,可能会出现如网络中断的故障而使得全局事务不能完成。因此,故障恢复机制后续仍然会重新提交/回滚这些未完成的全局事务,这样就会再次调用参与该全局事务的各TCC服务的Confirm/Cancel业务逻辑。

既然Confirm/Cancel业务可能会被多次调用,就需要保障其幂等性。 
那么,应该由TCC事务框架来提供幂等性保障?还是应该由业务系统自行来保障幂等性呢? 
个人认为,应该是由TCC事务框架来提供幂等性保障。如果仅仅只是极个别服务存在这个问题的话,那么由业务系统来负责也是可以的;然而,这是一类公共问题,毫无疑问,所有TCC服务的Confirm/Cancel业务存在幂等性问题。TCC服务的公共问题应该由TCC事务框架来解决;而且,考虑一下由业务系统来负责幂等性需要考虑的问题,就会发现,这无疑增大了业务系统的复杂度。

 

方案3 .可靠消息最终一致性

1 . 普通消息队列中间件

2 . 基于RabbitMQ/RocketMQ消息中间件

这种实现分布式事务的方式需要通过消息中间件来实现。假设有A和B两个系统,分别可以处理任务A和任务B。此时系统A中存在一个业务流程,需要将任务A和任务B在同一个事务中处理。下面来介绍基于消息中间件来实现这种分布式事务。

在系统A处理任务A前,首先向消息中间件发送一条消息
消息中间件收到后将该条消息持久化,但并不投递。此时下游系统B仍然不知道该条消息的存在。
消息中间件持久化成功后,便向系统A返回一个确认应答;
系统A收到确认应答后,则可以开始处理任务A;
任务A处理完成后,向消息中间件发送Commit请求。该请求发送完成后,对系统A而言,该事务的处理过程就结束了,此时它可以处理别的任务了。 
但commit消息可能会在传输途中丢失,从而消息中间件并不会向系统B投递这条消息,从而系统就会出现不一致性。这个问题由消息中间件的事务回查机制完成,下文会介绍。
消息中间件收到Commit指令后,便向系统B投递该消息,从而触发任务B的执行;
当任务B执行完成后,系统B向消息中间件返回一个确认应答,告诉消息中间件该消息已经成功消费,此时,这个分布式事务完成。
上述过程可以得出如下几个结论: 
1. 消息中间件扮演者分布式事务协调者的角色。 
2. 系统A完成任务A后,到任务B执行完成之间,会存在一定的时间差。在这个时间差内,整个系统处于数据不一致的状态,但这短暂的不一致性是可以接受的,因为经过短暂的时间后,系统又可以保持数据一致性,满足BASE理论。

上述过程中,如果任务A处理失败,那么需要进入回滚流程,如下图所示: 


若系统A在处理任务A时失败,那么就会向消息中间件发送Rollback请求。和发送Commit请求一样,系统A发完之后便可以认为回滚已经完成,它便可以去做其他的事情。
消息中间件收到回滚请求后,直接将该消息丢弃,而不投递给系统B,从而不会触发系统B的任务B。
此时系统又处于一致性状态,因为任务A和任务B都没有执行。

上面所介绍的Commit和Rollback都属于理想情况,但在实际系统中,Commit和Rollback指令都有可能在传输途中丢失。那么当出现这种情况的时候,消息中间件是如何保证数据一致性呢?——答案就是超时询问机制。

系统A除了实现正常的业务流程外,还需提供一个事务询问的接口,供消息中间件调用。当消息中间件收到一条事务型消息后便开始计时,如果到了超时时间也没收到系统A发来的Commit或Rollback指令的话,就会主动调用系统A提供的事务询问接口询问该系统目前的状态。该接口会返回三种结果:

提交 
若获得的状态是“提交”,则将该消息投递给系统B。
回滚 
若获得的状态是“回滚”,则直接将条消息丢弃。
处理中 
若获得的状态是“处理中”,则继续等待。


消息中间件的超时询问机制能够防止上游系统因在传输过程中丢失Commit/Rollback指令而导致的系统不一致情况,而且能降低上游系统的阻塞时间,上游系统只要发出Commit/Rollback指令后便可以处理其他任务,无需等待确认应答。而Commit/Rollback指令丢失的情况通过超时询问机制来弥补,这样大大降低上游系统的阻塞时间,提升系统的并发度。

下面来说一说消息投递过程的可靠性保证。 
当上游系统执行完任务并向消息中间件提交了Commit指令后,便可以处理其他任务了,此时它可以认为事务已经完成,接下来消息中间件一定会保证消息被下游系统成功消费掉!那么这是怎么做到的呢?这由消息中间件的投递流程来保证。

消息中间件向下游系统投递完消息后便进入阻塞等待状态,下游系统便立即进行任务的处理,任务处理完成后便向消息中间件返回应答。消息中间件收到确认应答后便认为该事务处理完毕!

如果消息在投递过程中丢失,或消息的确认应答在返回途中丢失,那么消息中间件在等待确认应答超时之后就会重新投递,直到下游消费者返回消费成功响应为止。当然,一般消息中间件可以设置消息重试的次数和时间间隔,比如:当第一次投递失败后,每隔五分钟重试一次,一共重试3次。如果重试3次之后仍然投递失败,那么这条消息就需要人工干预。 


有的同学可能要问:消息投递失败后为什么不回滚消息,而是不断尝试重新投递

这就涉及到整套分布式事务系统的实现成本问题。 
我们知道,当系统A将向消息中间件发送Commit指令后,它便去做别的事情了。如果此时消息投递失败,需要回滚的话,就需要让系统A事先提供回滚接口,这无疑增加了额外的开发成本,业务系统的复杂度也将提高。对于一个业务系统的设计目标是,在保证性能的前提下,最大限度地降低系统复杂度,从而能够降低系统的运维成本。

不知大家是否发现,上游系统A向消息中间件提交Commit/Rollback消息采用的是异步方式,也就是当上游系统提交完消息后便可以去做别的事情,接下来提交、回滚就完全交给消息中间件来完成,并且完全信任消息中间件,认为它一定能正确地完成事务的提交或回滚。然而,消息中间件向下游系统投递消息的过程是同步的。也就是消息中间件将消息投递给下游系统后,它会阻塞等待,等下游系统成功处理完任务返回确认应答后才取消阻塞等待。为什么这两者在设计上是不一致的呢?

首先,上游系统和消息中间件之间采用异步通信是为了提高系统并发度。业务系统直接和用户打交道,用户体验尤为重要,因此这种异步通信方式能够极大程度地降低用户等待时间。此外,异步通信相对于同步通信而言,没有了长时间的阻塞等待,因此系统的并发性也大大增加。但异步通信可能会引起Commit/Rollback指令丢失的问题,这就由消息中间件的超时询问机制来弥补。

那么,消息中间件和下游系统之间为什么要采用同步通信呢?

异步提升系统性能,但随之会增加系统复杂度;而同步虽然降低系统并发度,但实现成本较低。因此,在对并发度要求不是很高的情况下,或者服务器资源较为充裕的情况下,我们可以选择同步来降低系统的复杂度。 
我们知道,消息中间件是一个独立于业务系统的第三方中间件,它不和任何业务系统产生直接的耦合,它也不和用户产生直接的关联,它一般部署在独立的服务器集群上,具有良好的可扩展性,所以不必太过于担心它的性能,如果处理速度无法满足我们的要求,可以增加机器来解决。而且,即使消息中间件处理速度有一定的延迟那也是可以接受的,因为前面所介绍的BASE理论就告诉我们了,我们追求的是最终一致性,而非实时一致性,因此消息中间件产生的时延导致事务短暂的不一致是可以接受的。

方案4 .最大努力通知方案:

最大努力通知也被称为定期校对,其实在方案二中已经包含,这里再单独介绍,主要是为了知识体系的完整性。这种方案也需要消息中间件的参与,其过程如下:

上游系统在完成任务后,向消息中间件同步地发送一条消息,确保消息中间件成功持久化这条消息,然后上游系统可以去做别的事情了;
消息中间件收到消息后负责将该消息同步投递给相应的下游系统,并触发下游系统的任务执行;
当下游系统处理成功后,向消息中间件反馈确认应答,消息中间件便可以将该条消息删除,从而该事务完成。
上面是一个理想化的过程,但在实际场景中,往往会出现如下几种意外情况:

消息中间件向下游系统投递消息失败
上游系统向消息中间件发送消息失败
对于第一种情况,消息中间件具有重试机制,我们可以在消息中间件中设置消息的重试次数和重试时间间隔,对于网络不稳定导致的消息投递失败的情况,往往重试几次后消息便可以成功投递,如果超过了重试的上限仍然投递失败,那么消息中间件不再投递该消息,而是记录在失败消息表中,消息中间件需要提供失败消息的查询接口,下游系统会定期查询失败消息,并将其消费,这就是所谓的“定期校对”。

如果重复投递和定期校对都不能解决问题,往往是因为下游系统出现了严重的错误,此时就需要人工干预。

对于第二种情况,需要在上游系统中建立消息重发机制。可以在上游系统建立一张本地消息表,并将 任务处理过程 和 向本地消息表中插入消息 这两个步骤放在一个本地事务中完成。如果向本地消息表插入消息失败,那么就会触发回滚,之前的任务处理结果就会被取消。如果这量步都执行成功,那么该本地事务就完成了。接下来会有一个专门的消息发送者不断地发送本地消息表中的消息,如果发送失败它会返回重试。当然,也要给消息发送者设置重试的上限,一般而言,达到重试上限仍然发送失败,那就意味着消息中间件出现严重的问题,此时也只有人工干预才能解决问题。

对于不支持事务型消息的消息中间件,如果要实现分布式事务的话,就可以采用这种方式。它能够通过重试机制+定期校对实现分布式事务,但相比于第二种方案,它达到数据一致性的周期较长,而且还需要在上游系统中实现消息重试发布机制,以确保消息成功发布给消息中间件,这无疑增加了业务系统的开发成本,使得业务系统不够纯粹,并且这些额外的业务逻辑无疑会占用业务系统的硬件资源,从而影响性能。

因此,尽量选择支持事务型消息的消息中间件来实现分布式事务,如RocketMQ/RabbitMQ。

COOKIE方面:

  •                     <li class="tool-item tool-active is-like "><a href="javascript:;"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#csdnc-thumbsup"></use>
                        </svg><span class="name">点赞</span>
                        <span class="count"></span>
                        </a></li>
                        <li class="tool-item tool-active is-collection "><a href="javascript:;" data-report-click="{&quot;mod&quot;:&quot;popu_824&quot;}"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-Collection-G"></use>
                        </svg><span class="name">收藏</span></a></li>
                        <li class="tool-item tool-active is-share"><a href="javascript:;"><svg class="icon" aria-hidden="true">
                            <use xlink:href="#icon-csdnc-fenxiang"></use>
                        </svg>分享</a></li>
                        <!--打赏开始-->
                                                <!--打赏结束-->
                                                <li class="tool-item tool-more">
                            <a>
                            <svg t="1575545411852" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5717" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><defs><style type="text/css"></style></defs><path d="M179.176 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5718"></path><path d="M509.684 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5719"></path><path d="M846.175 499.222m-113.245 0a113.245 113.245 0 1 0 226.49 0 113.245 113.245 0 1 0-226.49 0Z" p-id="5720"></path></svg>
                            </a>
                            <ul class="more-box">
                                <li class="item"><a class="article-report">文章举报</a></li>
                            </ul>
                        </li>
                                            </ul>
                </div>
                            </div>
            <div class="person-messagebox">
                <div class="left-message"><a href="https://blog.csdn.net/u010565545">
                    <img src="https://profile.csdnimg.cn/B/6/B/3_u010565545" class="avatar_pic" username="u010565545">
                                            <img src="https://g.csdnimg.cn/static/user-reg-year/2x/7.png" class="user-years">
                                    </a></div>
                <div class="middle-message">
                                        <div class="title"><span class="tit"><a href="https://blog.csdn.net/u010565545" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}" target="_blank">sh_c</a></span>
                                            </div>
                    <div class="text"><span>发布了21 篇原创文章</span> · <span>获赞 24</span> · <span>访问量 1万+</span></div>
                </div>
                                <div class="right-message">
                                            <a href="https://im.csdn.net/im/main.html?userName=u010565545" target="_blank" class="btn btn-sm btn-red-hollow bt-button personal-letter">私信
                        </a>
                                                            <a class="btn btn-sm  bt-button personal-watch" data-report-click="{&quot;mod&quot;:&quot;popu_379&quot;}">关注</a>
                                    </div>
                            </div>
                    </div>
    </article>
    
  • 0
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值