- 之前的文章解析了rocketmq的事务(关于producer的), 由于自己使用的是rabbit(界面友好,虽然不是分布式的 但也基本ok), 所以简单实现了一个。
思路
-
核心的思想参考了rocketmq, 主要分为两个阶段, 事务执行阶段和消息发送阶段。
*1. 如果事务执行出问题,那么消息不会发送,可以利用报警或者日志去处理
*2. 如果事务执行成功,消息发送失败, 则事务进行回滚操作 -
保证消息能够正常发送到rabbitmq, 主要使用了rabbit的特性,基于硬盘和发送者事务, 硬盘可以保证数据的不丢失(如果物理损坏可以利用镜像集群,
当然如果恰好几个物理机都坏了那就尴尬了), 发送者事务则可以保证消息到达rabbit并被持久化后返回成功。 -
而对于消费者则启用消费者事务,在消费后手动ack(需要消费者自行写相关代码) 从而保证整体消息的不丢失。 至于消费的幂等性、顺序性还是需要共同去做一些方案。
-
我个人还是喜欢在发送消息前/后,或者消息出现异常、超时、确认之后做一些其他操作,比如持久化到数据库中,以确保之后做统计或者找问题。 这么做的理由是虽然中间件会将消息存储在磁盘, 首先不会存储很久,例如kafka默认只存一周(甚至不足), 或者当消息确认后进行物理删除(消息量大的时候很容易打满硬盘导致服务崩掉)。 当然写库会产生新的问题,例如写并发瓶颈、执行的延迟等等, 具体使用请酌情选择。
流程图
- 流程图和rocketmq比起来简直就是小巫见大巫了, 主要因为rabbit内部的原理没有画(erlang读不懂 囧。。)
- 类比三个阶段
- try消息 — 发送一个约定的不含逻辑的消息, 消费方只需要确保接受即可,不需要做逻辑操作。 这一步还在考虑。
- 本地事务阶段 — 通过listener的execute执行,进行容错处理(go 没有try-catch,所以使用error作为返回值)。 出现问题即回滚, 不会发送后续消息
- 消息阶段 — 此时确认本地事务执行成功, 只需要确保消息发送即可。 此时如果rabbitmq挂掉接受不了消息或者拒绝 会尝试重试几次, 超过重试次数,则说明确定有问题,执行事后方案并且通知报警
show my code (附录2有使用demo)
1. 接口类
// 事务监听接口
type TransactionListener interface {
Execute() error // 执行事务方法
Check() int // 查询方法 主要用于 消息队列无法通信时作为回调查询
RollBack() error // 回滚方法
}
-
定义了一个接口, 主要是本地事务的实现接口。 Execute()方法是自己实现, 具体的参数还在考虑能否抽象出来。 Rollback()则是在出现问题时即时回滚,(如果可以的话我希望永远不会调用这个方法)。 Check()则是作为消费者的检查,例如prodcuer和rabbit出现链接问题导致收不到对应的ack(mq频频重试等问题)。
-
由于go的interface不能定义属性(可能我没找到方法,确实没找到,希望知道的大佬告诉我下)。 如果可以的话, 我个人比较希望有个int的变量来存储 这个接口的结果状态(状态码自己定义), 或者使用map[string]interface{}来存储更多的变量。(
好怀念concurrentHashMap和HashMap)
2. struct
- 定义了事务类和消息类。
- 消息类主要是保持各个端通信的约定,因为rabbitmq的真正的消息体只有body([]byte类型)。 需要彼此之间约定好一些字段,才方便共同处理(rocketmq自己有Message和MessageExt类)
- 事务类主要是处理事务消息,类似于producer-client, 做一些消息的调度处理操作。所以也没有做序列化的处理。
- 可能有人注意到了 transaction是小写,而message是首字母大写。 在go中,纯小写一般代表私有(protected/private), 首字母大写代表公用(public)。 所以transaction是不能在包外直接使用的。 具体使用见下方
type transaction struct {
ExchangeName string // 专门处理事务的交换器
RouteKey string // 专门处理事务的路由key
Listener TransactionListener // 对应的listener
NeedTry bool // 是否发送try消息 确认存活 默认关闭
}
// 消息类
type Message struct {
Id int `json:"id"`
Action string `json:"action"` // 消息动作
Content map[string]interface{
} `json:"content"` // 具体的消息内容
Callback string `json:"callback"` // 消费成功后 调用的回调函数
}
3. 一些方法
3.1 初始化
/**
rabbitMq 事务类初始化
@param listener TransactionListener 事务监听实现
*/
func NewTransaction(listener TransactionListener) *transaction {
trans := new(transaction)
trans.ExchangeName = "" ; // 交换器名
// viper.GetString(rabbitPrefix + "transaction.exchangeName")
trans.RouteKey = ""; // 对应的routekey
// viper.GetString(rabbitPrefix + "transaction.routeKey")
trans.Listener = listener // 事务接口
trans.NeedTry = true // 是否需要try
// 交换器类型
// var exchangeType string = viper.GetString(rabbitPrefix + "transaction.exchangeType")
// 队列名
// var queueName string = viper.GetString(rabbitPrefix + "transaction.queueName")
// 创建exchange
go exchangeInit(trans.ExchangeName, exchangeType)
// 创建队列
go queueInit(queueName, "", "")
// 队列绑定
go queueBind(queueName, trans.RouteKey, trans.ExchangeName)
return trans
}
- 使用NewTransaction来 对外提供transaction类, 写起来有点像传统的singleton的方式。 具体自己的考虑请看文末的附录1.
- 当然这种方式有很多。 可以用命名返回值。 也可以直接干脆return一个 &transaction{…}。 (
个人仅仅想尝试下新的写法) - 注释掉的部分 我个人使用了viper去读取配置(一个读取配置文件的包, 个人用的yml)。
- 下面的go 初始化和bind的方法,只是自己简单的封了一层rabbit的初始化和bind方法。 采用了协程, 不影响主流程。 可能会在机器第一次初始化的时候会报错, 我个人会选择在项目初始化脚本里写一个队列初始化。 在这里再写一次是由于个人 遇到过 队列和交换器突然失效的情况(可能是rabbit突然宕机又迅速重启), 导致消息传递不到。 所以在这再初始化一次, rabbit允许在 主要核心参数相同的情况下, 覆盖初始化(核心参数变更就会直接报错。)
3.2 流程方法
- 核心方法
/**
发送事务处理的消息
@param trulyMessage Message 消息
@return error
*/
func (trans *transaction) MakeMessageTransaction(trulyMessage Message) error {
var err error
// 1. 如果需要 发送try 消息
if trans.NeedTry {
err = trans.Try()
if err != nil {
return err
}
}
// 2. 执行事务 -- 本地事务执行
err = trans.Listener.Execute()
if err != nil {
return err
}
// 3. 发送消息
confirmation, err := SendMessage(trulyMessage, trans.ExchangeName, trans.RouteKey)
// 4. 出现异常情况 || ack = false (拒绝消息)
if err != nil || (confirmation != nil && confirmatio