ActiveMQ使用与心得(二):分布式事务

基于ActiveMQ的分布式事务

由于容器技术,Spring Boot,Spring Cloud等技术的蓬勃发展,如今微服务已经大行其道,然而分布式事务却是微服务在落地过程中最具挑战性又不得不面对的技术难题。目前常见的解决分布式事务问题的方案有:两阶段提交(2PC)、补偿事务(TCC)、本地时间表加消息队列、MQ事务消息等。

1 场景介绍

以用户注册场景为例,需求是新用户注册之后给该用户新增一条积分记录。假设有两个用户和积分两个服务,用户服务使用数据库DB1,积分服务使用数据库DB2。服务调用者只需使用新增用户服务,该服务内部既保证在DB1新增了用户记录,又在DB2新增了积分记录。显而易见,这是一个分布式事务。
其实,问题的核心是DB1中的事务完成之后需要协调通知DB2执行事务,这可以通过消息队列来实现。比如在用户服务成功保存用户记录之后,向消息队列的某个主题中发送一条用户创建消息,积分系统需要监听该主题,一旦接收到用户创建的消息,积分系统就在DB2中为该用户创建一条记录。

2 问题分析

下面请考虑一下两个问题:

  • 用户服务在保存用户记录后还没有来得及向消息队列发送消息就宕机了,如何保证新增的用户记录一定会将消息发送到消息队列?
  • 积分服务接受到消息后还没来得及保存记录就宕机了,如何保证系统重启后不会丢失积分记录?
    透过现象看本质,这两个问题描述的都是如何让数据库和消息队列的操作是一个原子操作。

3 解决方案

3.1 案例分析

下面看一下事件表的定义:

字段名 字段类型 描述
id int(16) 主键
type varchar(32) 时间类型,比如新增用户、新增积分
process varchar(32) 表示事件进行到的环节,比如新建、发布、已处理
content text 事件内容,用于保存该事件发生时需要传递的数据
create_time datetime 创建时间
update_time datetime 更新时间

需要在DB1和DB2中都新建t_event表,这能保证每个数据库中的业务数据和事件表的操作都在同一个事务里面。假设DB1中的用户表是t_user,DB2中积分表是t_point,新增用户服务的具体步骤如下:

  1. 用户服务接收到请求后在t_user表中创建一条用户记录,并在t_event表中新增一条process为NEW的事件记录,同时要创建的积分数据以JSON字符串的形式保存到t_event表的content中,提交事务。
  2. 在用户系统中开启一个定时任务定时查询t_event表中所有process为NEW的记录,一旦有记录则向消息队列发送消息,消息的内容就是t_event表的content,消息发送成功后把process改为PUBLISHED,提交事务。
  3. 积分系统接收到信息后在DB2的t_event表中新增一条process为PUBLISHED的记录,content保存接收到的消息内容,保存t_event成功后返回,提交事务。
  4. 在积分系统中开启一个定时任务定时查询t_event表中所有process为PUBLISHED的记录,拿到表记录后将content字段的内容转换成积分对象,保存积分记录,保存成功后修改t_event的process为PROCESSED,提交事务。
    操作步骤图

3.2 宕机故障点分析

3.2.1 用户服务

假如在用户服务中把创建服务和发布服务分成两步,如果在第1步宕机,因为新增事件和业务操作(添加用户记录),业务操作也会失败;如果在第2步时宕机,则系统重启后定时任务会重新将之前没有发布成功的的事件记录继续发送消息。

3.2.1 积分服务

假如在积分服务中把接收事件和处理事件分成两步,如果在第3步时宕机,那就是在接收事件时宕机了,消息还没接收完成,由于消息队列的特性,会保证重新将事件发送给对应的服务。如果在第4步时宕机,就是在事件接收成功但在处理事件时宕机了,则系统重启后定时任务会重新对之前没有处理成功的事件进行处理。
这样就保证两个数据源之间数据状态的最终一致性

4 本地事件表+ActiveMQ优缺点分析

首先,有一点是显而易见的。本地事件表+ActiveMQ的方式实际上是将事务变成了异步执行。

该方案的优点:

  • 吞吐量大,因为不需要等待其他数据源响应。
  • 容错性好,A服务在发布事件时,B服务甚至可以不在线。

该方案的缺点:

  • 为了协调两个系统的数据一致,会出现很多中间态,编程比较复杂。
  • 使用了定时任务,需要轮询事件表,受到定时任务的制约。

总的来说,最终一致性是比较适合实际业务场景的分布式事务。

5 思考题

5.1 服务器集群部署与ActiveMQ点对点方式存在的问题

假设要集群部署用户系统和积分系统,现在用户系统部署3个服务,积分系统部署2个服务,用户系统中轮询事件表部署1个服务,积分系统中轮询事件表部署1个服务(quartz可以进行集群,有兴趣的读者可以看一下xxl-job),ActiveMQ也要集群部署,使用Point-to-Point的方式。在高并发情况下,每个用户系统的服务都会往broker里面写消息,那么积分系统是怎样正确消费消息的?
温馨提示:Point-to-Point模式里面,如果多个消费者监听统一队列,会进行资源抢占(无法确定消息会被那一个消费者消费),因为点对点模式里面,一条消息只能被一个消费者使用。

5.2 幂等性

细心的小伙伴可能会发现,以上解决方式存在漏洞,存在信息重复消费的可能性。这就延伸出了消费队列幂等性的问题。
重复消费可能会这样产生:(消息应答模式设置设置为客户端手动确认)
当消费者监听到消息,数据库写入成功的时候,由于网络波动,broker没有接收到ack,那么这条消息对于ActiveMQ来说就没有消费成功,由于消息队列的特性,broker会重新把消息发给消费者。
那么如何保证ActiveMQ幂等性呢?
其实都是按照业务需求在保证幂等性。我这里列举个思路:

  • 比如你要把数据写库,先根据主键查一下,如果这数据都有了,你就别插入了,直接update。
  • 比如你是写 Redis,那更没问题了,反正每次都是 set,天然幂等性。
  • 也可以基于数据库的唯一索引约束来保证数据不会重复插入

那上面的系统如何做到幂等性?
可以把事件表的记录(除去时间)都写到broker里面,因为事件表里面有自增id作为主键,每次添加到另外系统的事件表的时候,先进行查询,如果这条记录已经存在,就是重复消费;否则就是第一次消息,进行正常的操作即可。

发布了16 篇原创文章 · 获赞 5 · 访问量 3295

猜你喜欢

转载自blog.csdn.net/qq_32573109/article/details/100023120