一、引子
《事务注解(@Transactional)引起的数据覆盖故障》一文收到不少反馈。事务里不要有rpc,基本原则,sb封装的太好了,把很多人养傻了,function级别的事务,坑太大。网友一这个是mysql数据库并发更新问题,update未提交,并不能阻塞mq读取后的select操作。很明显select会取到未提交的快照,在a事务提交之后b提交覆盖原值。并且事务中间是绝对不应该有外部调用的
网友二
去哪qmq的玩法是利用数据库的事务 所有库上加一个mq的逻辑库 mq实际先提交一条数据到mq库 再用统一的消费者消费mq库 实现消息的隔离和最终一致
网友三
随着公司业务的增长,单体应用架构很难满足业务快速迭代以及性能方面的需求,都会进行服务化改造,按照业务等要素将原来庞大的单体应用拆分成不同的服务。那么在进行服务化改造之前首先就是面临是服务化基础设施的技术选型,其中最重要的就是服务之间的通信中间件。服务之间的通信可以分为同步方式和异步方式。同步的方式的代表就是 RPC,异步方式一般会选用mq。
二、问题的现实意义
如果我们要在服务化拆分中使用消息队列,那么我们需要解决哪些问题呢?首先去哪儿网提供了旅游产品在线预订服务,那么就涉及电商交易,在电商交易中我们认为数据的一致性是非常关键的要素。那么我们的 MQ 必须提供一致性保证。
MQ 提供一致性保证又分为两个方面。发消息时我们如何确保业务操作和发消息是一致的,也就是不能出现业务操作成功消息未发出或者消息发出了但是业务并没有成功的情况。举例来说,支付服务使用消息通知出票服务,那么不能出现支付成功,但是消息没有发出,这会引起用户投诉;但是也不能出现支付未成功,但是消息发出最后出票了,这会导致公司损失。总结一下就是发消息和业务需要有事务保证。一致性的另一端是消费者,比如消费者临时出错或网络故障,我们如何确保消息最终被处理了。那么我们通过消费 ACK 和重试来达到最终一致性。
三、利用数据库事务解决一致性问题
提到一致性,大家肯定就想到事务,而一提到事务,肯定就想到关系型数据库,那么我们是不是可以借助关系型 DB 里久经考验的事务来实现这个一致性呢。我们以 MySQL 为例,对于 MySQL 中同一个实例里面的 db,如果共享相同的 Connection 的话是可以在同一个事务里的。以下图为例,我们有一个 MySQL 实例监听在 3306 端口上,然后该实例上有 A,B 两个 DB,那么下面的伪代码是可以跑在同一个事务里的
有了这层保证,我们就可以透明的实现业务操作和消息发送在同一个事务里了,首先我们在公司所有 MySQL 实例里初始化出一个 message db,这个可以放到自动化流程中(据说在去哪儿由运维团队完成),对应用透明。然后我们只要将发消息与业务操作放到同一个 DB 事务里即可。
我们来看一个实际的场景,在支付场景中,支付成功后我们需要插入一条支付流水,并且发送一条支付完成的消息通知其他系统。那么这里插入支付流水和发送消息就需要是一致的,任何一步没有成功最后都会导致问题。那么就有下面的代码
上面的代码可以用下面的伪代码解释
实际上在 producer.sendMessage 执行的时候,消息并没有通过网络发送出去,而仅仅是往业务 DB 同一个实例上的消息库插入一条记录,然后注册事务的回调,在这个事务真正提交后消息才从网络发送出去,这个时候如果发送到 server 成功的话消息会被立即删除掉。而如果消息发送失败则消息就留在消息库里,这个时候我们会有一个补偿任务会将这些消息从消息库里捞出然后重新发送,直到发送成功。整个流程就如下图所示
1、begin tx 开启本地事务
2、do work 执行业务操作
3、insert message 向同实例消息库插入消息
4、end tx 事务提交
5、send message 网络向 server 发送消息
6、reponse server 回应消息
7、delete message 如果 server 回复成功则删除消息
8、scan messages 补偿任务扫描未发送消息
9、send message 补偿任务补偿消息
10、delete messages 补偿任务删除补偿成功的消息
四、更多的东西
去哪儿完整的方案,还包括了消息的存储模型以及延迟队列的存储模型,分别解决性能及业务问题。
当然消息存储模型这部分,本公众号之前关于IM的文章的做法也值得参考。