空谈发件箱模式(outbox pattern)
之前聊过很多次分布式事务这个话题,本文继续聊一聊如何用发件箱模式实现分布式事务。
1
基于微服务架构模式(当然不限于)的应用系统,常常会利用消息中间件(kafka,rabbitmq等)来实现各个微服务之间的通信。对于用户的某个操作,一个微服务可能需要执行“存数据库”和“发送event”两个步骤。
举个例子,用户创建一个订单,订单服务需要存数据库和发送订单created event,如下:
begin transactionsave(order) to dbsend(order_created_event) to kafkacommit/rollback
对于上面这个例子,由于牵扯到两个系统,数据库事务和kafka事务并不能保证整个操作的事务性(ACID),至多能保证各个子系统的事务性,最终可能导致出现如下数据不一致的情况:
- 发送event成功,然后commit到数据库失败(原因可能是数据库不available,或者是违反数据库表的constraints)。
- 发送event成功到commit到数据库之间有一定延迟,如果consumer消费到event,接着call订单服务查找这个order,结果就可能会查不到。
有同学会说,可以把发送event放在存数据库transaction后面,如下:
begin transactionsave(order) to dbcommit/rollbacksend(order_created_event) to kafka
即使这样,又会出现另外一种不一致的情况,即:存数据库成功,发event失败,比如由于kafka临时不available。这直接导致用户的操作失败,必须要重新提交请求,在某些场景下对用户来说可能是不可接受的;并且数据库里面还有一条脏数据存在。
那么,如何保证存数据库和发event是一个transaction呢?
其实,抽象这个问题,其本质就是一个分布式事务问题,如何实现分布式事务,网上有很多文章介绍,可能最常被提到的就是两阶段提交2PC(2 phase commit),但其需要各个子系统都支持两阶段提交协议(比如XA),很多数据库都有支持,但是很多消息中间件都不支持,所以2PC不适用这里的场景,并且由于2PC的一些缺点(参见我的另外一篇文章:关于分布式系统数据一致性的那些事(二)),它并不是实现分布式事务的常规选择。
基于此,这里介绍的发件箱模式可以有效地解决这个问题。
2
发件箱模式,简单讲就是在数据库里面额外增加一个outbox表用于存储需要发送的event,把直接发送event的步骤换成先把event存储到数据库outbox表;另外,程序启动一个scheduler job不断去抓取outbox表里面的记录,发送给Kafka,最后删除发送成功的记录。如下:
主逻辑:
begin transactionsave(order) to dbsave(order_created_event) to db outboxcommit/rollback
scheduler job:
begin transactionget(order_created_event) from db outboxsend(order_created_event) to kafkadelete(order_created_event) to db outboxcommit/rollback
基于这样的实现,存order和event就可以通过数据库的transaction来保障,这样即使是Kafka不available,也不影响用户的行为,只是说后续处理可能会有一些延迟而已;此外,如果scheduler job删除event失败,最坏的行为也就是重新再发一次这个event,即是重发消息的问题,这就需要消费端实现幂等性处理。
3
发件箱模式主要有两种实现方式:
- Transaction log tailing
- Polling publisher
Transaction log tailing,又称CDC(change data capture),就是依赖于数据库的transaction log,一般数据库在成功执行完一条语句之后就会记录一条log,那么这个方式就是不断地增量抓取数据库log,然后发送到消息系统。目前,已经有一些支持这种方式的open source可以直接使用,比如:Debezium。另外,采用这种方式,可能需要数据库装相关的插件以支持CDC。
Polling publisher,其实就是自己实现一个轮询的sheduler job,不断抓取outbox表里面的event,然后发送到消息系统,最后删除event。在实现层面,可能需要注意下面几点:
- 一般来说,为了保证event的顺序,在抓取event的时候,需要把event按时间排序。
- 如果是分布式的环境,为了保证scheduler job同时只在一个instance上run,需要实现分布式锁;或者借助于成熟的scheduling的library,比如:quartz。
相关阅读
References
- https://microservices.io/patterns/data/transactional-outbox.html
- https://mp.weixin.qq.com/s/PMIWr8j3cN-K0FbS6qGW0A
- 终结 finalize()和垃圾回收(garbage collection)
- 代码+实战:TensorFlow Estimator of Deep CTR —— DeepFM/NFM/AFM/FNN/PNN
- 【SSH测试整合Demo】企业人事管理系统
- Hybris 项目工程配置
- 购物车案例【简单版】
- 再学习之MyBatis.
- 用户登陆注册【JDBC版】
- 学习思考之《编程之美》.
- 干货 | 深度剖析服务发现组件Netflix Eureka
- 多线程编程学习一(Java多线程的基础).
- TensorFlow | 自己动手写深度学习模型之全连接神经网络
- 多线程编程学习二(对象及变量的并发访问).
- ASM基本配置问题(r5笔记第89天)
- 如何上手使用 Facebook 的开源平台 Detectron?
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 如何造个android Flow流式响应的轮子
- Flutter混合开发详解
- Android 单双击实现的方法步骤
- Android多渠道打包时获取当前渠道的方法
- Flutter Dio二次封装的实现
- android开发通过Scroller实现过渡滑动效果操作示例
- Android9.0 静默安装源码的实现
- Flutter路由框架Fluro使用简介
- Android倒计时神器(CountDownTimer)
- Kotlin如何安全访问lateinit变量的实现
- Android实现加载对话框
- Android RecyclerView实现悬浮吸顶、分隔线、到底提示效果
- Android监听键盘状态获取键盘高度的实现方法
- android实现小音频频繁播放
- 每日一个知识点:什么时候会触发Full GC