go+rabbitmq实现一个简单的tcc

之前的文章解析了rocketmq的事务(关于producer的), 由于自己使用的是rabbit(界面友好,虽然不是分布式的 但也基本ok), 所以简单实现了一个。核心的思想参考了rocketmq, 主要分为两个阶段, 事务执行阶段和消息发送阶段。*1. 如果事务执行出问题,那么消息不会发送,可以利用报警或者日志去处理*2. 如果事务执行成功,消息发送失败, 则事务进行回滚操作...
摘要由CSDN通过智能技术生成

  • 之前的文章解析了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
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值