Go微服务: 分布式之通过可靠消息实现最终一致性

2024-06-09 08:12

本文主要是介绍Go微服务: 分布式之通过可靠消息实现最终一致性,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

通过可靠消息实现最终一致性

  • 可靠消息,就是靠普消息,还是基于之前的这个案例
  • 比如这个订单服务,无论你是先发送消息,还是先新建订单,它其实都是发送的不可靠消息
  • 就是说如果这个消息,像mysql事务那样,只要订单服务不确认,下游就没办法消费
  • 如果你这个订单服务挂了,就可以取消这个消息,就不用做这个本地消息表了
  • 本地消息表,要有一个这个循环的这么一个查询,高并发的时候,你本地的数据库本身压力就大
  • 再弄这么一个查询,一直循环的查,它这个压力也不小,现在看一下基于事务消息,也称为可靠消息
  • 生产者就可以理解为这个订单服务,消费者就可以理解为积分服务,还有库存服务
  • 先看第一个,生产者先发一个半消息给消息队列,消息队列就回我这个半消息成功的信息
  • 这是一个半消息,然后拿到这个半消息成功的结果之后,我们往数据库里写一个Transaction事务
  • 我们就把本地事务执行了,执行之后,也就是到了第4步, 成功情况下就是确认这个半消息
  • 只要到第4步这个commit成功了,消费者就可以消费了,因为你的这个消息已经在这个消息队列中了
  • 这个解决方式是解决了只要我们能发出去的这个消息就是可靠的,只要能提交的消息,本地消息就一定是成功的
  • 因为你本地事务都已经执行成功了,先发半消息,消息队列回给我,回给我之后,我就能收到了,说明已经成功了
  • 然后这个时候你就开始干你本地的事儿,比如订单生产者自己开始建订单,建订单产品表,这都是你的事
  • 我本地能成功了之后,1,2,3步是为了能让消费者成功消费的准备工作
  • 这个思路就是说只要一提交,那我本地这边全部ok, 然后我这个消息队列, 肯定能保证我我的最终一致性
  • 如果在3之前出错,那不会做事务,那相当于准备工作没做好,那下游也不会做相应的这个事情
  • 同时,我们还要思考,有没有其他方面的情况会导致问题
    • 比如,更复杂的网络传输问题,别人的服务宕机了或有bug了
    • 因为微服务在开发的时候,每个小组可以时刻发布自己的服务,它不受控制
  • 如果上述分布式服务出问题了,消息队列也会有一个回查事务消息状态的机制
  • 我会问你这个生产者,哎,你这个状态是啥?然后生产者就会查询这个本地事务状态
  • 第5步和第6步,就是又一次为这个返回事务状态做commit和rollback,就是为下一步的工作做准备
  • 就是消息队列,不知道你这个消息要不要投递,也不能知道别人的状态是什么样的
  • 那消息队列就会问这个生产者,你这个消息事务是啥状态,在第6步得到查询的事务状态是commit
  • 那我消息队列就commit消息投递,一旦消息投递了,这个消费者就可以进行消费了
  • 假如说,返回的这个事务状态是rollback,消息队列就可以把消息扔了
  • 在这整个链路里,有成功的,让消费者消费消息;也有失败,让这个消息队列去丢弃消息
  • 还有中间状态,就是说我不确定这个事务是不是正确的?那询问还是有结果的,就是 commit 或 rollback
  • 其实我们说走到第5步,第6步,还有第7步的时候,他就可以再一次确认我们的消息是否要投递还是丢弃
  • 到这里,一直没有看到说有锁的存在,在高并发的情况下,消息队列就保证了我们最终的一致性
  • 就是说锁的存在,它一定是和高并发是这个对立的,我们尽量不要用锁的方式去考虑我们的并发

积分和库存业务场景的对比

  • 再回过头来看一下这个模型,如果你的生产者是订单,而消费者是库存的话
  • 如果库存不足,消费者是库存服务,库存不足,虽然成功的发送了到这个RocketMQ里
  • 但是库存没有办法成功消费这条消息,这个和其他业务形态上是不一样的
  • 比如订单服务,可以说是送积分,从技术角度来说,积分是没有上限的
  • 还有一种形态,就是我们说发短信,你成功的购买了某某产品等等
  • 这种业务形态, 你的生产者只要把消息放到了消息队列, 消息队列一定是可以保障的
  • 就是说你消息队列是集群吧,你在不挂的情况下,是一定能送达到消费者应用队列里的
  • 比如说, 积分服务,短信服务,你这边已经是commit了
  • 无论你是在第4步commit的,还是说第7步commit的下游是一定能消费到的
  • 但是库存服务不一样,如果库存不足了,你这边又没办法返回
  • 左边是库存服务和订单服务,右边是消息队列
  • 从正向来说,如果服务的提供方发送消息到这个消息队列,也就是生产者发送消息到消息队列
  • 只要消息队列集群不挂,那么我们的消费者是一定能收到这个消息的
  • 就是实现最终的一致性对于积分服务,短信服务的使用都是没有问题的
  • 因为积分理论上是无上限的,我们的短信是一定能发上去的,只要你短信账户里,有足够的余额
  • 但是当库存为零的时候,你的这个消息队列仍然收到了我们发送成功的消息
  • 但下游库存服务是没有办法消费成功的,我们不可能凭空多出来这么多库存
  • 让你去消费,因为没有那么多库存,我们又不能把这个消息队列成功投递库存不足的消息
  • 再返回给这个生产者,这个是不可能的,所以,我们能不能先发送一个归还库存的半消息
  • 这个半消息, 对于库存服务来说, 它是见不到的,我们发完半消息之后
  • 去调用扣减库存的Srv服务,这就是一个Grpc的这么一个调用,先看这个返回失败的情况
  • 如果调用库存失败了,这个时候返回一个rollback,因为一开始就是调用的是归还
  • 那你这个时候就调用rollback,如果我们库存执行失败了,那说明我们库存这个数据是没有变的
  • 既然你库存调用是失败,订单就不会创建, 因为我本地这个数据库就不会变
  • 那么我们两边的这个数据都没变,这个业务也是可以接受的
  • 就是说没有造成数据不一致,那你就不要给我发这个消息来告诉我,你要去归还库存了
  • 那我们直接rollback这个消息,然后这个消息队列, 就可以把这个归还库存的消息给它扔掉了
  • 扔掉之后,看左边这一边就没问题了,数据都没变
  • 你执行失败了,我这边又没执行,或者说,这两边的数据都保持一致,这就是OK的
  • 好,我们再看下一张执行成功的图,看我们订单服务发送一个半消息
  • 然后调用了Grpc扣减那个库存,然后它成功了
  • 成功之后, 我们就会执行这个本地 mysql 事务服务, 其实它可能成功,也可能失败
  • 因为我们如果在微服务里, 由于网络原因或宕机,Bug,停电等各种问题
  • 它都可能导致一个服务的运行失败
  • 如果我们先说这个执行本地mysql事务成功,在第4步成功,执行rollback
  • 还是从数据的角度来看,库存扣减成功,订单执行成功,我们本地也执行成功
  • 那就是说我们两边数据都改变了,这个业务上也是可以接受的
  • 订单生成成功了,库存也扣减了,那就相当于交易成功
  • 那你就不要给我发送这个归还库存的半消息了,所以是 rollback这个半消息
  • 然后,我们把这个消息就扔掉了
  • 那我们再看看,如果第4步执行失败了,就是说我们有任何情况
  • 订单服务有bug,断电或者其他场景,执行失败,那就执行一个commit
  • 因为订单执行失败了,相当于订单数据没有变,那你的库存现在是变了
  • 因为之前第三步已经执行成功了,那这个时候就要告诉库存,说给我扣减了,因为我执行失败了
  • 失败以后,提交一个commit之后,然后,这个库存服务就可以监听到这个消息队列里
  • 因为它 commit 了嘛,这个消息就能看到了,订阅了这个消息之后,就能去归还库存了
  • 如果你这个订单服务,还有一些其他问题,怎么办?其实这个消息队列还提供一种机制
  • 就是回查这个消息,比如说我现在不确定你这个是要提交还是rollback
  • 这个回查机制,就查这个订单的服务,那你还去你这个本地事务的数据库库里去捞数据
  • 你捞对了,就rollback,捞错了,还是commit, 你commit之后
  • 我还是能调用到这个库存服务,然后你再给我归还
  • 这张图就是看到了为了保持这个数据的一致性,我们这边整个业务流程的保证就是这样了
  • 再看上面这张图,还有问题,是在原来的基础上增加了第8条发送延迟消息和监听延迟消息。
  • 就是说我这个库存有一百个,我这个用户买完以后,就是不支付
  • 如果他一周不支付或者一个月不支付,你的库存永远不释放,别人永远买不了
  • 那不就把这个商城的库存给锁死了
  • 当我库存执行成功,这个本地的事务也执行成功的时候,我就把它发送一条延迟消息。
  • 假如我们规定半个小时,时间一到,这个延迟消息就会投递
  • 然后,我们就会根据这个消息去看,这个订单是 支付成功了,还是支付失败了
  • 如果是未支付或者是支付失败都可以,如果你执行失败了,那我就归还库存
  • 因为对商城来说,如果没收到钱,那我就归还库存,半个小时之后,仍然其他的用户就可以买
  • 这样就完美的解决了库存,订单和这个订单下单成功后不支付的这么一个场景
  • 这里的核心重点是:在一开始发送了一个归还库存的半消息
  • 执行commit和rollback的情况是反着来的

这篇关于Go微服务: 分布式之通过可靠消息实现最终一致性的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1044634

相关文章

SpringBoot3实现Gzip压缩优化的技术指南

《SpringBoot3实现Gzip压缩优化的技术指南》随着Web应用的用户量和数据量增加,网络带宽和页面加载速度逐渐成为瓶颈,为了减少数据传输量,提高用户体验,我们可以使用Gzip压缩HTTP响应,... 目录1、简述2、配置2.1 添加依赖2.2 配置 Gzip 压缩3、服务端应用4、前端应用4.1 N

Go标准库常见错误分析和解决办法

《Go标准库常见错误分析和解决办法》Go语言的标准库为开发者提供了丰富且高效的工具,涵盖了从网络编程到文件操作等各个方面,然而,标准库虽好,使用不当却可能适得其反,正所谓工欲善其事,必先利其器,本文将... 目录1. 使用了错误的time.Duration2. time.After导致的内存泄漏3. jsO

SpringBoot实现数据库读写分离的3种方法小结

《SpringBoot实现数据库读写分离的3种方法小结》为了提高系统的读写性能和可用性,读写分离是一种经典的数据库架构模式,在SpringBoot应用中,有多种方式可以实现数据库读写分离,本文将介绍三... 目录一、数据库读写分离概述二、方案一:基于AbstractRoutingDataSource实现动态

Python FastAPI+Celery+RabbitMQ实现分布式图片水印处理系统

《PythonFastAPI+Celery+RabbitMQ实现分布式图片水印处理系统》这篇文章主要为大家详细介绍了PythonFastAPI如何结合Celery以及RabbitMQ实现简单的分布式... 实现思路FastAPI 服务器Celery 任务队列RabbitMQ 作为消息代理定时任务处理完整

Java枚举类实现Key-Value映射的多种实现方式

《Java枚举类实现Key-Value映射的多种实现方式》在Java开发中,枚举(Enum)是一种特殊的类,本文将详细介绍Java枚举类实现key-value映射的多种方式,有需要的小伙伴可以根据需要... 目录前言一、基础实现方式1.1 为枚举添加属性和构造方法二、http://www.cppcns.co

使用Python实现快速搭建本地HTTP服务器

《使用Python实现快速搭建本地HTTP服务器》:本文主要介绍如何使用Python快速搭建本地HTTP服务器,轻松实现一键HTTP文件共享,同时结合二维码技术,让访问更简单,感兴趣的小伙伴可以了... 目录1. 概述2. 快速搭建 HTTP 文件共享服务2.1 核心思路2.2 代码实现2.3 代码解读3.

MySQL双主搭建+keepalived高可用的实现

《MySQL双主搭建+keepalived高可用的实现》本文主要介绍了MySQL双主搭建+keepalived高可用的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,... 目录一、测试环境准备二、主从搭建1.创建复制用户2.创建复制关系3.开启复制,确认复制是否成功4.同

Java实现文件图片的预览和下载功能

《Java实现文件图片的预览和下载功能》这篇文章主要为大家详细介绍了如何使用Java实现文件图片的预览和下载功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... Java实现文件(图片)的预览和下载 @ApiOperation("访问文件") @GetMapping("

使用Sentinel自定义返回和实现区分来源方式

《使用Sentinel自定义返回和实现区分来源方式》:本文主要介绍使用Sentinel自定义返回和实现区分来源方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Sentinel自定义返回和实现区分来源1. 自定义错误返回2. 实现区分来源总结Sentinel自定

Java实现时间与字符串互相转换详解

《Java实现时间与字符串互相转换详解》这篇文章主要为大家详细介绍了Java中实现时间与字符串互相转换的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录一、日期格式化为字符串(一)使用预定义格式(二)自定义格式二、字符串解析为日期(一)解析ISO格式字符串(二)解析自定义