对于请求处理,先入库,再发消息,没你想得这么简单

2023年 8月 12日 26.4k 0

聊技术,不止于技术

微服务架构下,各个微服务间的通信方式是首先需要决定的事。微服务间的通信方式主要有REST、RPC和消息这三种。这三种通信方式各有优缺点,各有其适合的场景,关于它们的比较及分析今天就先不讲了。

今天主要讲的是基于消息的通信方式下,先入库在发送消息的问题。

基于消息的通信方式下,各个微服务间通过消息驱动来完成业务逻辑。一个典型的例子如下:

上例中,用户服务处理用户注册请求,先入库,然后发送用户注册事件,邮件服务监听用户注册事件,然后发送欢迎邮件。

那么就上述场景而言,对于用户服务,我们的业务代码该如何写呢?为什么我说先入库再发消息,没你想得这么简单呢?下面一起来看看。

1

先入库,再发消息,简单又直接的方式

简单又直接的入库发消息伪代码如下:

1. var content = processRequest(httpRequest);2. var message = prepareMessage(httpRequest);3. DB.insert(content);4. Message.publish(message);

对于先入库,再发消息的业务逻辑,简单直接的代码如上,那么上述代码有什么问题吗?

考虑以下场景:

(1) 数据库入库成功,发送消息失败,即步骤3成功,步骤4失败

数据可能不一致。因为发送消息失败的原因总的来说有两个,一是消息发送失败(消息总线未接收到消息),此时数据不一致,因为数据库中有数据,但消息未发送。二是消息发送成功(消息总线接收到),但回包的时候失败,对于系统来说,此时数据反而是一致的,数据入库,消息发送了。

(2) 数据库入库失败

数据可能不一致。有人可能会想数据库操作失败,数据库的事务ACID特性可以保证数据一致性。但实际这里也可能有两种情况,一是数据库操作失败,事务未提交,此时数据一致,数据库会回滚事务。二是事务已提交,数据库回包失败,数据不一致(数据库和消息总线中的数据不一致),数据库中有数据,消息却没发送。

2

简单直接的方式并不好使,那该怎么办?

其实上述问题的本质是分布式事务问题,数据库和消息总线实际是两个资源。想要保持两个或者多个资源间的数据一致性,以及操作的原子性,这正是分布式事务要解决的问题。

让我们尝试解决此类问题。

首先要问的一个问题是,我们的系统需要强一致性吗?在上述例子中如果数据库和消息总线中的数据需要保持强一致,则在任一时刻数据库与消息总线中的数据都需要保持一致。

显然并不需要。

实际在分布式系统中,只要保持弱一致性就可以了,也就是说终一致性,对应上例,也就是说在任一时刻,数据库与消息总线中的数据可以暂时不一致,但终需要一致,只不过中间有一些间隔。

所以现在我们只要让系统具备终一致性就可以了,那么如何具备终一致性呢?

在上例中,把问题具体化,其实就是处理完请求并且入库后,必须发送消息,也就是数据库中有的数据,消息总线中也必须有。问题进一步抽象定义,即解决数据库入库和发送消息的原子性问题,这两个操作要么都成功,要么都失败。并且现在我们的系统只需要满足弱一致性就可以,所以问题可以更进一步定义为这两个操作要么终都成功,要么终都失败。

看到这里,有一个方案应该能够浮现出来——本地消息表。

本地消息表的伪代码如下:

1. var content = processRequest(httpRequest);2. var message = prepareMessage(httpRequest);3. DB.begin();4. DB.insert(content);5. DB.insert(message);6. DB.commit();7. Message.publish(message);8. DB.delete(message);// 定时任务,补偿发送消息,这里查询的消息注意避免时间存在过短的问题,以防重复发送Executor.execute(new Task() {            public void run() {                while (true) {                    var message = DB.selectMessage();                    Message.publish(message);                    DB.delete(message);                    }                } });

该方案本质上是利用了本地数据库事务的特性,将消息和业务逻辑处理放在一个事务里持久化,利用事务特性可以保证业务处理和消息能够同时存储成功或失败,然后在发送消息。

同样,让我们考虑下述场景:

(1)数据库入库成功,消息发送失败,即步骤3~6成功,步骤7失败

数据终一致。定时任务会补偿消息投递,当然这里也可能会存在消息重复发送的问题。

(2)数据库入库失败,即步骤3~6失败。

数据终一致。数据库在事务提交前失败,数据一致。数据库在事务提交后,但回包前失败,数据终一致,数据已存在,定时任务会补偿消息投递。

(3)数据库入库成功,消息发送成功,消息删除失败,即步骤3~7成功,步骤8失败。

数据终一致。消息未删除,定时任务补偿发送消息,会导致消息重复发送。消息已删除,但数据库回包前失败,补偿任务不做处理,数据终一致。

可以看到本地消息表除了会导致消息重复投递,几乎没有别的问题。

那么消息重复投递怎么办?

如果消息重复投递,这里只看数据库跟消息总线,其实数据是不一致的,消息总线中数据多了。但从整个系统的层面来看呢?如果消费端能够实现幂等,那么整个系统的数据还是终一致的。所以采用本地消息表,下游消费端需要实现幂等。而且现在有的消息中间件也能够实现发送消息的幂等(比如Kafka 0.11版本以上,Broker可以通过发送的消息id进行去重,保证发送消息的幂等),即重复投递的消息在消息中间件中只会存在一份,这样系统也是没有问题的。

3

先入库,再发消息原来并不简单,那么符合上述场景的业务是不是都得这么做呢?

有同学看到这里可能会觉得原来处理请求,先入库再发消息的场景不是这么简单呀,看来以前用错了?

那是不是此场景下所有的应用都需要按照此类方案来呢?

我这里的建议是看具体业务需求。

大流量大规模的分布式系统,从可靠性及可维护性来讲,必须这么做。至于那些用户少,规模小的应用,从故障发生的概率、发生故障后人员维护的成本来考虑,你可以不遵守上述方案。

当然,能够看到这些问题然后选择一个适合的方案,和不知道自己在做什么完全是两码事。

先知道规则,然后再知道什么时候可以打破规则。

写在后

对于请求处理,先入库再发消息的场景并没有看起来的这么简单。该问题实际是一个分布式事务问题,涉及到两个资源间的数据一致性,入库与发消息原子性问题。

对于大多数分布式应用,能够满足数据终一致性就可以。

所以上述场景可以采用本地消息表的方案,本地消息表实质上是利用了本地数据库的事务特性,保证业务处理与消息存储的事务特性。

本地消息表可能会存在消息重复发送的问题,所以需要实现消费端的幂等。

先知道规则,然后再知道什么时候可以打破规则。

后留一个问题,对于消费端来说,接收消息,然后处理入库,如何保持幂等?如果是接收消息,然后处理入库,再然后再发消息的场景呢?如果是接收消息,然后远程调用的场景呢?

如果能够回答清楚上述问题,不光光是对这些场景有很深的理解,相信你对整个分布式系统的设计与实现都有很深的理解。

后面的文章,说一说我对上述场景以及分布式系统设计与实现的理解。欢迎大家关注。

推荐阅读:《重拾面向对象软件设计:把复杂的事情做简单》

聊技术,不止于技术。

在这里我会分享技术文章、管理知识以及个人的思想感悟,欢迎点击关注。

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论