如何基于MQ实现分布式事务

文章目录

在分布式事务的实现中,有很多种方案,其中比较常用的就是基于MQ来实现,在MQ 的具体实现中,又有很多种具体的方案,从大的方面来说,于MQ的分布式事务基可以分为两种:

  • 可靠消息最终一致性
  • 最大努力通知

1.可靠消息最终一致性

可靠消息最终一致性,顾名思义就是依赖可靠的消息,来实现一种最终一致性的模型,

他的大致流程就是:

  • 1、事务的发起方执行本地事务
  • 2、事务的发起方向事务的参与方发送MQ消息
  • 3、事务的参与方接收到MQ消息后执行自己的本地事务

这里面事务的发起方和参与方都需要各自执行本地事务,他们之间,通过可靠消息来保障最终一致

那么,怎么样的消息算可靠呢。

直接依赖kafka、rocketMQ发送一个消息就可靠了么?

显然是不行的,因为我们知道,在出现网络分区、网络延迟等情况时,是没办法保证消息一定可以发出去的,也没办法保证消息发出去就一定能被成功消费。

那么想要做到让这个消息可靠,一般有两种做法:

  • 本地消息表
  • 事务消息

下面我们一个个来了解一下

1.1 本地消息表

这个方案的主要思想是将分布式事务拆分为本地事务消息事务两个部分,本地事务在本地数据库中进行提交或回滚,而消息事务则将消息写入消息中间件中,以实现消息的可靠投递和顺序性

一般来说的做法是,在发送消息之前,先创建一条本地消息,并且保证写本地业务数据的操作和写本地消息记录的操作在同一个事务中 。这样就能确保只要业务操作成功,本地消息一定可以写成功。

整体流程如上图所示,消息发出去之后,等待消费者消费,在消费者端,接收到消息之后,做业务处理,处理成功后再修改本地消息表的状态。

这个过程中,可能有几个步骤都可能发生失败,那么如果失败了怎么办呢?

  • 1、2如果失败,因为在同一个事务中,所以事务会回滚,3及以后的步骤都不会执行。数据是一致的。
  • 3如果失败,如果是同步,可以直接根据发送状态,如果发送失败,直接去重试,如果是异步,可以监听异常回调和发送结果,如果失败,也是立刻同步重试(注意最大重试次数),同时还需要有一个定时任务,不断的扫描本地消息数据,对干未成功的消息进行重新投递,大致代码如下:
java 复制代码
        Mq.sendAsync(topic.getTopic(), message,
                new CallBack() {
                    @Override
                    public void onException(Throwable exception) {
                       retry(topic, message, 2);
                    }

                    @Override
                    public void onResult(SendResponse res) {
                        if (!res.isSucceed()) {
                          retry(topic, message, 2);
                        }
                    }
                });
    private static boolean retry(MqTopicEnum topic, SimpleMessage message, int retry) {
        int times = 0;
        boolean rst = false;
        for (; retry > 0; retry--) {
            times++;
            rst = SendOnce(topic, message);
            if (rst) {
                break;
            }
        }
        if (!rst) {
            //重试3次仍失败,要发出告警
            AlarmUtil.metric(AlarmCode.MQ_SEND_ERROR, topic.getTopic(), message.getKey(), null);
        }
        return rst;
    }
  • 4、5如果失败,则依靠消息的重投机制,不断地重试
  • 6、7如果失败,那么就相当于两个分布式系统中的业务数据已经一致了,但是本地消息表的状态还是错的。这种情况也可以借助定时任务继续重投消息,让下游幂等消费再重新更改消息状态,或者本系统也可以通过定时任务去查询下游系统的状态,如果已经成功了,则直接推进消息状态即可。

1.1.1 本地消息表的优缺点

优点:

  • 1.可靠性高:基于本地消息表实现分布式事务,可以将本地消息的持久化和本地业务逻辑操作,放到一个事务中执行进行原子性的提交,从而保证了消息的可靠性。
  • 2.可扩展性好:基于本地消息表实现分布式事务,可以将消息的发送和本地事务的执行分开处理,从而提高了系 自统的可扩展性。
  • 3.适用范围广:基于本地消息表实现分布式事务,可以适用于多种不同的业务场景,可以满足不同业务场景下的需求。

但是其缺点也非常明显,因为我们是通过定时任务去扫表,所以会遇到以下几个问题:

  • 1.消息堆积后,通过扫表去消息是否发送以及下游是否消费成功的效率极低
  • 2.集中式扫表,会影响正常业务
  • 3.定时扫表会存在延迟问题

知道了问题,我们逐一去解决

1.消息堆积,扫表慢

随着本地消息表中的数据量越来越大,通过定时任务扫表的方式会越来越慢,那么想要解决这个问题,首先可以考虑加索引。

我们可以在state字段上增加一个素引,虽然这个字段的区分度不高,但是一般来说,这张表中,success的数据量占90%,而init的数据量只占10%,而我们扫表的时候只关心init即可,所以增加索引后,扫表的效率是可以大大提升的。

里面涉及到一个知识点:就是针对区分度不高的字段加索引的问题

  • 如果是加聚合索引,尽可能让区分布不高的字段排后
  • 如果是针对单一字段加索引,如果字段区分度不高,索引大概率不会生效,还是会去全表查询,但是有一种场景例外,如男女比例是95:5,那么,这时候,如果我用"女'作为性别的查询条件的话,还是可以走索引,并且有很大的性能提升的,原因就是因为他可以过滤掉大部分数据。走索引可以大大提升效率。这种一般在任务表中比较多,比如任务表中有状态,两种情况: INIT和SUCCESS,大多数情况下,任务都是SUCCESS的,只有一少部分是INIT,这时候就可以给这个字段加索引。这样当我们扫描任务表执行任务的时候,还是可以大大提升查询效率的。

只加索引肯定是不能够满足大数据量的情况下遇到的高并发问题,此时我们还需要引入多线程并发扫表,在扫表的时候通过分段的思想进行数据隔离即可,整体流程如下:

假设有10个线程,那么第一个线程就扫描D处于0-1000的数据,第二个线程扫描1001-2000的数据,第三个线程扫描2001-3000的数据。这样以此类推,线程之间通过分段的方式就做好了隔离,可以避免同一个数据被多个线程扫描到。

这个做法,有个小问题,那就是INIT的数据的ID可能不是连续的,那么就需要考虑其他的分段方式,比如在时间表中增加一个业务ID,然后根据这个biz id做分片也可以。

java 复制代码
    public void fn() {
        Long minId = messageService.getMinTinitId();
        for (int i = 1; i <= threadPool.size(); i++) {
            Long maxId = minId + segmentSize() * i;
            List<Message> messages = messageService.scanInitMessages(minId, maxId);
            proccee(messages);
            minId = maxId + 1;
        }
    }
sql 复制代码
SELECT * FROM RETRY_MESSAGE 
WHERE STATE ="INIT"
AND 
BIZ_ID between min and max

虽然解决了扫表慢的问题,但是又带来另外一个问题:如果业务量比较大的话,集中式的扫描数据库势必给数据库带来一定的压力,那么就会影响到正常的业务。

2.集中式扫表,会影响正常业务

因为数据量大的话会一直扫表做查询,数据量大的时候查询就会很慢,那么数据库连接数就会被占满。导致应用的正常请求拿不到连接.

那么想要解决这个问题,首先可以考虑,不扫主库,而是扫描备库。

当然很多人说主从库之间的数据同步都是依赖binlog,由于Binlog的刷盘策略,可能会出现数据丢失或者同步延迟的问题,但是我们这个场景之所以能这么做,是因为这个业务场景一般都是可以接受一定的数据延迟的,那么备库带来延迟就可以忽略,但是备库是没有业务操作的,所以对备库的扫描是不会对业务造成影响的。

当然,这里还要考虑一个问题,那就是备库扫描数据之后的执行,执行完该如何同步到主库,这里可以直接修改主库,主备库数据ID一致的,直接去修改主库的就行了。不建议直接在备库上修改。不光此类业务,其他的主从读写逻辑也是建议:所有的更新、删除以及新增操作一定要在主库执行,从库只做读或只做数据副本逻辑

但是不管怎么样,备库还是可以分担扫表的这个大量高峰请求的

除了扫备库,还有一个方案,那就是做分库了。把原来集中在同一个数据库的数据分散到不同的数据库中。这样用 自集群代替单库来整体对外提供服务,可以大大的提升吞吐量。

因为多个数据库的话,每个库提供的连接数就会多,并且多个实例的话,CPU、IO、LOAD这些指标也可以互相分担

3.定时扫表的延迟问题

定时任务都是集中式的定时执行的,那么就会存在延迟的问题。随着数据库越来越大,延时会越来越长。

想要降低延迟,那就要抛弃定时任务的方案,可以考虑延迟消息,基于延迟消息来做定时执行。

基于RocketMq类似的消息队列做延迟消息之后,可以做到流量削峰,解耦,还可以缓解数据库的压力。这种做法比定时扫表的性能要好,实时性也更高。

1.1.2 本地消息表的代码实践

1.表结构设计

索引设计:

  • state作为普通索引,用于高效扫表。这个也可以和 retry_count 、next_retry_at 等字段一起建个联合索引。
  • message key + message type 作为唯一索引,防止重复插入

lock _version,主要用于乐观锁处理,避免出现并发问题导致更新错误

next_retry_atlast_retry_time 这两个字段可以有一个也行,都没有也不是不行,看你的实际业务情况,有的任务是可以主动设定下次执行时间,比如特殊的消息就是要3小时执行一次,那么就可以在每次执行后,如果失败了,则把当前时间加上3小时,设置到 next_retry_at 上面去。

last_retry_time 这个是方便我们扫表的时候可以设置特殊的过滤条件,比如只针对没重试过的消息( last_retry_time is null )进行扫描,或者针对10分钟之前的消息处理( last_retry_time <now -10 min)等等。

2.具体业务实现
java 复制代码
    @Transactional
    public void order(rderDTO orderDTO) {
        orderServive.createOrder(orderDTO);
        messageService.createMessage(orderDTO);
    }

但是这里只是记录了本地消息,还需要把本地消息通过MQ发出去。这里就可以有很多办法了

  • 一种是异步扫表还可以直接同步发消息
  • 也可以借助Spring Event来异步处理,都是可以的。

但是如果是同步发的话时效性肯定更好,但是同步发消息需要注意,要把调MQ发消息的地方放到事务外,要不然自会因为MQ网络延迟等问题导致回滚。

所以就可以用编程式事务,当然你也可以在发送MQ发消息的方法上,事务传播机制设置为Propagation.NOT_SUPPORT,这样就不会影响事务

java 复制代码
    @Autowired
    TransactionTemplate transactionTemplate;

    public void order(OrderDTO orderDTO) {
        boolean transactionSuccess = transactionTemplate.execute(new TransactionCallback<Boolean>() {
            @Override
            public Boolean doInTransaction(TransactionStatus status) {
                try {
                    orderServive.createOrder(orderDTO);
                    messageService.createMessage(orderDTO);
                    return true; // 表示事务执行成功
                } catch (Exception e) {
                    status.setRollbackOnly();
                    return false; // 表示事务执行失败
                }
            }
        });
        if (transactionSuccess) {
            // 事务执行成功,可以执行 
            mgService.send(orderDTO);
            messageService.updateSuccess(orderDTO);
        } else {
            // 事务执行失败的处理逻辑
            // 可以抛出异常,添加告警、记录日志等
            throw new RuntimeException("事务执行失败");
        }
    }

在事务中写入本地业务数据+本地消息,然后在事务外发MQ消息,如果发送失败了,也不影响事务的commit,如果发送成功了,把本地消息表的状态推进一下。

如果失败,下一次再通过定时任务扫表把需要处理的事件查出来重发就行了

所以本地消息表中还需要有一个定时任务,还需要提供一个接口给下游回调,但是上面说到,定时任务会导致扫表任务

读到这儿,同学们可能就会发现,我们目前所有的流程都没有提到下游服务失败了,但是我们上游执行成功了,怎么保证业务一致性的问题,如:在整个交易链路里,乘客发单命中风控,然后走预付逻辑,乘客预付成功之后,就会走发单逻辑,此处就会有个问题,如果发单失败,是走重新发单,还是走退款逻辑呢,当然里面涉及的业务链路也比较复杂,我们只谈创建交易单和创建乘客单这两个逻辑,两个的顺序一定是先A在B的逻辑,对于本地消息表的方案,首先,我们需要知道,本地消息表的方案并不适合用在这种需要回滚的场景,而是适合用在哪种不需要回景。什么场景不需要回滚呢?

在举个例子,我们都知道,用户下单之后,会给用户创建一个运费险,那么这个场景,一般运费险的投保过程前置条件就是下单成功,而且,下单的时候给用户表达了有运费险,那么就意味着,一旦下单成功了,就必须要功。不能因为投保未成功而导致订单回滚。包括我上面这个例子,为了整个业务的完单量和GMV,一旦成功支付成功,我们就必须订单创建成功也是一样的道理

所以,这就是典型的本地消息表的,或者是事务消息适合的场景!!!即不回滚,必须成功

但是,如果面试官问了这个问题,你回答了上面的内容,他还是问你,我就要回滚,该咋做呢?

那么我们只能参考类似Mysql innodb引擎下的undoLog的回滚机制,即补偿机制,当然这个也是在分布式事务的范畴,这个就是咱们常说的:SAGA 事务

SAGA 的核心思想一句话就可以说明白,就是把业务分成一个个步骤,当某一个步骤失败的时候,就反向补偿前面的步骤,SAGA的核心:反向补偿

以A:交易 B:行程两个服务为例

流程为:A创建交易单,乘客支付成功之后,B服务创建建订单为例

首先本地消息表保证的是消息一定能发送成功且一定能够被下游服务消费到,这个控制的逻辑总结起来就一句话:先一直尝试重试,重试达到上限,通过延迟消息或者定时任务去读本地消息表,将未投递成功的消息重新投递,或未消费的消息以及消费失败的消息重新发送

如果B服务遇到问题,如创建订单失败,先对B服务进行回滚,然后在业务层面,执行补偿,如直接执行退款流程等

但是上面还是有个我们之前提到的问题,就是定时任务当遇到消息量过大,消息表数量太多的时候,会遇到因扫表时间过长无效占用数据库连接等问题,所以我们可以通过以下方案去解决:

首先,下游服务处理消息时发生业务失败,如网络异常、数据校验不通过、依赖服务不可用等等,需要先明确返回个失败的响应,这样MQ就会继续投递这个消息。所以,失败的时候,我们优先考虑的是重试,而不是回滚

之后如果多次重试都不成功,那么就可以借助死信队列,把这条失败的消息放进死信队列中,然后上游可以再监听这个死信队列的消息,做本地事务的回滚。

当然,为了避免出现错误。回滚前建议反查一下下游的接口,避免实际成功了的情况

其实,这个方案,看上去挺好的,但是实际上就很麻烦,因为这么做了之后,两个系统之间的耦合就很严重,互相依赖消息,互相依赖接口查询。而且一旦有多个消息监听者都要去操作,比如下单后要依次运费险投保、给用户发消息、给用户加积分等等,就很麻烦如果部分成功,部分失败,又怎么处理呢?实际上系统根本没法处理。。。。

但是如果面试官一定要,那有没有更好的方案了。当然,对于很多面试官来说,你不说他也不知道有这些问题,那就给他这个他想听的答案好了。

实际大部分公司,包括我们公司采用的是人工介入,当一个消息多次投递都不成功的话,记录到DB,并且当这种问题环比或者同比增长超过阈值时,人工介入。

同时也可以依靠对帐机制做准实时对账,发现不一致的情况人工介入。

这种场景发生的概率很低,一旦发生了可能就是有一些特殊的原因,可能需要人工介入才能解决。

1.2 事务消息

很多人读到这儿会一脸问号,上面不是一直在提到用MQ去解耦流程吗,怎么又提到一个事务消息,如果不用事务消息,就用本地消息的话,那么一次操作一般是这样的流程:

  • 1、执行本地事务
  • 2、发送MQ消息
  • 3、消费MQ消息

如果一切顺利,那么没啥说的,双方都能处理成功,最终是一致的,但是实际情况是,因为网络延迟、网络抖动服务器本身的稳定性、MQ自身的稳定性等原因,这个过程会出现各种各样的问题。

一旦在第一个参与者本地事务操作之后,如果出现了MQ发送失败、或者发送成功了,但是MQ自己存储失败了等原因,可能就会导致不一致了。

有人会问了,那这里如果MQ发失败了,本地事务回滚不就行了么?

有问题,因为会出现一种极端情况,那就是当出现网络抖动的时候,发送MQ因为网络超时返回了失败,本地事务回滚之后,但是网络超时不一定是MQ没有接收到,有可能处理成功了,但是返回的时候超时了。这时候就会出现:本地事务回滚了、但是MQ发送成功了的问题。这时候下游正常消费MQ之后,就又出现不一致了。

而且,MQ自身也不一定可靠,不管是哪种MQ,在极端情况下,都是有可能丢消息的,也就说,可能会出现本地事务成功之后,发送MQ成功了,但是因为MQ自身原因,导致消息丢了,还是会出现不一致。

所以,总之就是引入MQ之后,会因为各种原因导致不一致,那怎么解决这个问题呢?

解决方案就是能有一个机制保证MQ一定可以发送成功,或者是如果失败了,也有机制能够重试让他成功。

秉持这个思路,上面的本地消息表其实就是在做这个事,但是执行起来又会遇到其它问题,消息量增大,分布分表改造会较大,不分库分表,会出现性能瓶颈的问题

所以引入了事务消息 (RocketMQ中的那种,非Kafka中的那种),即把一个发送消息的过程拆成2步,先发一个半消息,确保成功之后,在执行本地事务,本地事务成功后,再发第二个半消息。

1.2.1 事务消息的三个阶段

MQ的事务主要分为三个阶段:

阶段1:事务准备与消息预提交

1.发送Half消息

  • 事务主动方(系统A)先发送half消息(半消息) 到MQ
  • MQ持久化但不立即投递(对下游不可见)
  • MQ返回持久化成功ACK(确保消息不丢失)

半消息

Half消息是指已经发送到MQ服务器并持久化,但暂时不能投递给消费者的消息。

半消息是发消息到指定的topic:RMQ_SYS_TRANS_HALF_TOPIC

半消息里存储的数据结构大致如下:

java 复制代码
RMQ_SYS_TRANS_HALF_TOPIC
├── 半消息1(原始Topic: order_topic, 原始消息内容)
├── 半消息2(原始Topic: inventory_topic, 原始消息内容)
├── 半消息3(原始Topic: payment_topic, 原始消息内容)
└── ...
  • 正常消息:发送 -> 持久化 -> 立即投递
  • Half消息:发送 -> 持久化 -> 等待确认 -> 决定是否投递
阶段2:执行本地事务与最终提交

1.本地事务处理

  • 系统A收到ACK后执行本地业务逻辑(如扣减库存/创建订单)

2.提交二次确认

  • 根据事务结果向MQ发送指令:
    • ✅ Commit → 半消息标记为可投递状态
    • ❌ Rollback → MQ删除半消息
    • ⚠️ 超时未响应 → 触发消息回查

⚠️ 设计重点:Commit/Rollback可能因网络、宕机丢失,需超时回查兜底

3.事务回查API

生产者需实现 TransactionListener:

  • executeLocalTransaction():执行业务逻辑
  • checkLocalTransaction():响应回查请求
java 复制代码
public class OrderTransactionListener implements TransactionListener {
    // 执行本地事务(如创建订单)
    @Override
    public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
        try {
            createOrder(msg.getOrderId()); // DB操作
            return LocalTransactionState.COMMIT_MESSAGE;
        } catch (Exception e) {
            return LocalTransactionState.ROLLBACK_MESSAGE;
        }
    }

    // Broker 回查时触发(如超时未收到Commit)
    @Override
    public LocalTransactionState checkLocalTransaction(MessageExt msg) {
        OrderStatus status = orderDao.getStatus(msg.getOrderId());
        return status == PAID ? COMMIT_MESSAGE : ROLLBACK_MESSAGE;
    }
}
阶段3:消息投递与事务完成

MQ投递消息

  • 仅处理Commit状态的半消息 → 投递给消费者(系统B)

消费者处理

  • 系统B执行本地事务(如更新账户余额)
  • 返回处理结果ACK给MQ:
    • ✅ 成功 → MQ删除消息(事务完结)
    • ❌ 失败 → 触发消息重投机制
      🔁 重试保障:MQ按配置间隔(如2s/5s/10s)重试,上限N次后进死信队列人工介入

2.异常场景与容错机制

场景1:事务主动方提交超时

  • 问题:MQ未收到Commit/Rollback指令
  • 解决方案:消息回查

在RocketMQ的事务消息中,如果半消息发送成功后,RocketMQ Broker在规定时间内没有收到COMMIT或者ROLLBACK消息。

RocketMQ会向应用程序发送一条检查请求,应用程序可以通过回调方法返回是否要提交或回滚该事务消息。如果应用程序在规定时间内未能返回响应,RocketMQ会将该消息标记为"UNKNOW"状态

在标记为"UNKNOW"状态的事务消息中,如果应用程序有了明确的结果,还可以向MQ发送COMMIT或者ROLLBACK.

但是MQ不会一直等下去,如果过期时间己到,RocketMQ会自动回滚该事务消息,将其从事务消息日志中删除.

场景2:消费者处理失败

  • 问题:系统B消费异常(网络中断/业务异常)
  • 解决方案:
    • MQ自动重试投递(至少一次语义)
    • 消费者幂等设计:通过唯一事务ID避免重复消费

场景3:MQ服务端故障

  • 预防措施:
    • 半消息持久化磁盘(Broker高可用部署)
    • 同步刷盘+主从复制(防单点故障)

场景4:第一次半消息发送失败怎么办

在事务消息的一致性方案中,我们是先发半消息,再做业务操作的

所以,如果半消息发失败了,那么业务操作也不会进行,不会有不一致的问题。

遇到这种情况重试就行了。(可以自己重试,也可以依赖上游重试)

1.2.2 总结

RocketMQ的事务消息是通过ransactionListener接口T和半消息机制来来实现的。

在发送事务消息时,首先向RocketMQ Broker发送一条"half消息"(即半消息),半消息将被存储在Broker端的事务消息日志中,但是这个消息还不能被消费者消费。

接下来,在半消息发送成功后,应用程序通过执行本地事务来确定是否要提交该事务消息。

如果本地事务执行成功,就会通知RocketMQ Broker提交该事务消息,使得该消息可以被消费者消费;否则,就会通知RocketMQ Broker回滚该事务消息,该消息将被删除,从而保证消息不会被消费者消费。

2.最大努力通知

2.1 最大努力通知流程

所谓最大努力通知,换句话说就是并不保证100%通知到。这种分布式事务的方案,通常也是借助异步消息进行通知的。

采用这种方式一般是下游业务失败了对整体逻辑也不会影响,如乘客支付成功给乘客发短信或者邮件的逻辑,即使发送消息失败也不会影响后续流程。

具体逻辑如下:

发送者将消息发送给消息队列,接收者从消息队列中消费消息。在这个过程中,如果出现了网络通信故障或者消息队列发生了故障,就有可能导致消息传递失败,即消息被丢失。因此,最大努力通知无法保证每个接收者都能成功接收到消息,但是可以尽最大努力去通知。

下面是一个简单的例子来说明最大努力通知的过程。假设有一个在线商城系统,顾客可以下订单购买商品。当顾客成功下单后,通知顾客订单已经确认。这个通知就可以采用最大努力通知的方式。

  • 顾客下单后,商城订单系统会生成订单并记录订单信息。
  • 商城订单系统通过最大努力通知机制,将订单确认通知发送给用户通知服务.
  • 用户通知服务把下单消息通过电子邮件发送给用户。
  • 商城系统不会等待顾客的确认,而是将通知放入消息队列中,并尽力发送通知。
  • 如果通知发送成功,那就很好,顾客会尽快收到订单确认邮件。但如果由于网络问题、电子邮件服务器问题或其他原因导致通知发送失败,商城系统可能会做一些尝试,尽可能的通知,重试多次后还是不成功,则不再发送

需要注意的是,在最大努力通知的过程中,可能会出现消息重复发送的情况,也可能会出现消息丢失的情况。因此,在设计最大努力通知系统时,需要根据实际业务需求和风险承受能力来确定最大努力通知的策略和重试次数以及对消息进行幂等等处理。

最大努力通知这种事务实现方案,一般用在消息通知这种场景中,因为这种场景中如果存在一些不一致影响也不大

2.2 最大努力通知和本地消息表区别?

方案 可靠性 性能 复杂度 适用场景
本地消息表 中等 中等 强一致性要求
事务消息 简单场景
最大努力通知 通知类场景

本地消息表相对于最大努力通知而言,引入了本地消息表,通过本地事务来保证消息可以发送成功。相对来说,具有更强的可靠性,可以在一定程度上保证消息的传递不丢失。但是,本地消息表也会带来额外的存储开销和网络通信成本。

而最大努力通知这种方案比较简单,但是可能存在丢消息的情况。其实,一般业务中,也会通过对账来解决的,并不会完全放任消息丢失,只不过对账的机制会有一定的延时,并且可能需要人工介入。

3.面试挑战

3.1 系统设计题

面试官:设计一个订单系统,如何保证订单创建和库存扣减的一致性?

您的回答框架:

  • 问题分析:说明分布式事务的挑战
  • 方案对比:本地消息表 vs 事务消息 vs 最大努力通知
  • 技术选型:根据业务场景选择合适方案
  • 实现细节:提供具体的代码实现
  • 优化策略:多线程扫描、索引优化、主从分离

3.2 技术深度题

面试官:RocketMQ事务消息是如何保证一致性的?

您的回答要点:

  • 三个阶段:半消息 → 本地事务 → 最终确认
  • 异常处理:回查机制、重试策略
  • 容错设计:超时处理、死信队列

3.3 C. 性能优化题

面试官:消息表数据量大时如何优化扫描性能?

您的回答策略:

  • 索引优化:状态字段索引、复合索引
  • 并发控制:多线程分段扫描
  • 架构优化:主从分离、分库分表

3.4 面试回答技巧

STAR法则应用:

  • Situation: "我们系统遇到了消息堆积问题..."
  • Task: "需要设计一个可靠的分布式事务方案..."
  • Action: "采用了本地消息表 + 多线程扫描..."
  • Result: "性能提升了80%,消息可靠性达到99.9%..."

3.5 技术深度展示

java 复制代码
// 展示您的技术深度
@Transactional
public void createOrder(OrderRequest request) {
    // 1. 业务逻辑
    Order order = createOrderLogic(request);
    
    // 2. 消息记录(同一事务)
    LocalMessage message = createMessageRecord(order);
    
    // 3. 事务提交后异步发送
    asyncSendMessage(message);
}

3.6 面试中需要注意的点

✅ 要强调的亮点:

  • 问题分析能力:从业务场景到技术方案的推导
  • 技术选型思维:不同方案的优缺点对比
  • 性能优化经验:从索引到架构的全面优化
  • 异常处理能力:各种失败场景的应对策略

⚠️ 要避免的问题:

  • 不要死记硬背:理解原理比记忆代码更重要
  • 不要过度复杂:根据面试官水平调整技术深度
  • 不要忽略业务:技术方案要结合业务场景

3.7 面试准备建议

1.准备核心知识点:

  • 分布式事务的CAP理论
  • 本地消息表的实现原理
  • RocketMQ事务消息的机制
  • 性能优化的具体策略

2.准备实际案例:

  • 项目中遇到的具体问题
  • 解决方案的选择过程
  • 优化效果的量化数据

3.准备代码示例:

  • 关键代码片段要能现场手写
  • 理解每行代码的作用
  • 能解释设计思路

建议在面试前:

  • 重点准备几个核心概念的解释
  • 准备1-2个具体的项目案例
  • 练习现场画图或写代码
  • 准备一些性能优化的量化数据

这样就能在面试中很好地展示您的技术能力和项目经验!

3.8 面试话术模板

java 复制代码
"在分布式系统中,我们经常遇到数据一致性问题。

比如订单创建和库存扣减,如果不在一个事务中,
可能会出现订单创建成功但库存扣减失败的情况。

我们采用了本地消息表的方案:
1. 在同一个事务中创建订单和消息记录
2. 事务提交后异步发送消息
3. 通过定时任务保证消息最终发送成功

这个方案的优势是..."
相关推荐
退役小学生呀2 小时前
十九、云原生分布式存储 CubeFS
分布式·docker·云原生·容器·kubernetes·k8s
smileNicky2 小时前
Kafka 为什么具有高吞吐量的特性?
分布式·kafka
小白不想白a8 小时前
【Hadoop】HDFS 分布式存储系统
hadoop·分布式·hdfs
随心............9 小时前
Spark面试题
大数据·分布式·spark
Hello.Reader11 小时前
用一根“数据中枢神经”串起业务从事件流到 Apache Kafka
分布式·kafka·apache
找不到、了15 小时前
常用的分布式ID设计方案
java·分布式
AKAMAI1 天前
在分布式计算区域中通过VPC搭建私有网络
人工智能·分布式·云计算
面带微笑向前走2 天前
分布式集群压测+grafana+influxdb+Prometheus详细步骤
分布式·grafana·prometheus
何中应2 天前
分布式事务的两种解决方案
java·分布式·后端
诸葛务农2 天前
人形机器人——电子皮肤技术路线:光学式电子皮肤及MIT基于光导纤维的分布式触觉传感电子皮肤
分布式·机器人·wpf