MQ 实现分布式事务

 

注:RocketMQ 3.0.8支持事务,以后版本不支持事务

分布式事务的场景

1.场景1

支付宝转1万元到余额宝,如果支付宝扣除1万后,如果系统挂掉了,余额宝并没有增加1万,数据出现不一致情况。

2.场景2

电商系统中,当有用户下单后,除了在订单表插入一条记录外,对应商品表的这个商品数量必须 减1,怎么保证??在 搜索广告系统中,当用户点击某广告后,除了在点击事件表中 增加一条
记录 外 ,还得去商家账户表中找到这个商家扣除广告费吧,怎么保证??

实现方式

1.本地事务

支付宝账户表:A(id,userId,amount)

1.支付宝账户表:update A set amount = amount - 10000 where userId = 1;
2.余额宝账户表: update B set amount = amount +10000 where userId = 1;

余额宝账户表:B(id,userId,amount)
用户的userId = 1,从支付宝转账1万元钱倒余额宝的动分为两个部分:
如何确保支付宝余额宝收支平衡呢?可以用事务解决
 

Begin transaction
update A set amount = amount - 10000 where userId = 1
update B set amount = amount + 10000 where userId = 1
End transaction
commit;

如果用spring的话一个注解(@Transactional)就能搞定上述事务功能。

如果系统规模较小,数据表都在一个数据库实例上,上述本地事务方式可以很好的运行,但是如果系统规模较大,比如支付宝账户表和余额宝账户 表显然不会在同一个数据库实例上,他们往往
分布在 不同的物理节点上,这时本地事务就已经失去用武之地了。
 

2.两阶段提交

两阶段提交协议(Two-phase Commit,2PC) 经常被用来实现分布式事务,一般 分为协调器C和若干 事务执行者SI两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。

缺点:
1)两阶段提交涉及多次两点间的网络通信,通信时间太长。
2)事务时间相对于变长了,锁定的资源时间也变长了,造成资源等到时间增加好多!!

正式由于分布式事务 存在很严重的 性能问题,大部分高并发服务器都在避免使用,往往通过其他途径来解决数据一致性问题。
 

3.使用MQ实现分布式事务


当 支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加一万”,只要这个凭证(消息)能可靠保证,我们最终是可以拿着这个凭证(消息)让余额宝账户 增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。

 

3.1 如何可靠保存凭证

  • 业务与消息耦合的方式

支付宝在完成扣款的同时,同时记录消息数据,这个消息数据与业务数据保存在同一数据库实例里(消息记录表名为message).

Begin transtration

update A set amount = amount -10000 where userId = 1;

insert into message(userId,amount,status) values (1,10000,1);

End transaction

commit;

上述事务能保证只要支付宝账户里被扣了钱,消息一定能保存下来。

当上述事务提交成功后,我们通过实时消息服务将此消息通知余额宝,余额宝处理成功后发送回复成功消息后,支付宝收到回复后删除该条消息数据。

  • 业务与消息解耦

为了解耦,可以采取以下方式:

  1. 支付宝在扣款事务提交之前,向实时消息服务请求发送消息,实时消息服务只记录消息数据,而不真正发送(只是知道有这一条消息),只有消息发送成功后才会提交事务。

 

     //支付宝 - 10000 (业务需求)

     //先把(支付宝-10000)封装成一个消息(new Message()))

     //然后把这个消息提交到MQ服务器上send(producer.send(new Message(),callback(里面处理本地事务)))

     //在callback处理本地事务:在callback方法里:

     update A set amount = amount - 10000 where userId = 1;

      ..............

       //当本地事务操作完成了以后

      1.要么成功:(给MQ一个标识:COMMIT)

      2.要么失败:(给MQ一个标识:ROLLBACK)

2. 当支付宝扣款事务被提交成功后,向实时 消息服务确认发送,只有在得到确认发送指令后,实时消息服务才真正发送该消息。

3. 当支付宝扣款事务提交失败回滚后,向实时 消息服务取消发送。在得到取消发送指令后, 该消息将不会被发送。

4. 对于那些未确认的消息或者取消的消息,需要有一个消息状态确认系统定时去支付宝系统查询这个消息的 状态进行更新。为什么需要这一个步骤:假设在支付宝扣款事务被成功提交后,系统挂了,此时消息状态并未被更新为“确认发送”,从而导致消息不能被发送。

优点:消息数据独立存储 ,降低业务系统与消息系统之间的耦合。

缺点:一次消息发送需要两次请求;业务处理服务需要实现消息状态回查接口

 

 

 

相关代码如下:

RocketMQ版本:3.2.6

Producer:

public class Producer {
	public static void main(String[] args) throws MQClientException, Exception {
		// 声明并初始化一个producer
		// 需要一个producer group 名字作为构造方法的参数,这里为producer1
		final TransactionMQProducer producer = new TransactionMQProducer("transaction_producer");
		// 设置NameServer地址,此处应改为实际NameServer地址,多个地址之间用;分隔
		// NameServer的地址必须有,但是也可以通过环境变量的方式设置,不一定非得写死在代码里
		// 发送消息失败后重试多少次
		producer.setNamesrvAddr("192.168.50.112:9876;192.168.50.113:9876");
		// 事务回 查最小并发数
		producer.setCheckThreadPoolMinSize(5);
		// 事务回查最大并发数
		producer.setCheckThreadPoolMaxSize(20);
		// 队列数
		producer.setCheckRequestHoldMax(2000);
		// producer对象在使用之前必须调用start初始化,初始化一次即可
		producer.start();

		// 服务器回调producer,检查本地事务分支成功 还是失败
		producer.setTransactionCheckListener(new TransactionCheckListener() {
			public LocalTransactionState checkLocalTransactionState(MessageExt msg) {
				//查询数据库 再次进行检查
				System.out.println("state --" + new String(msg.getBody()));
				return LocalTransactionState.COMMIT_MESSAGE;
			}
		});

		for (int i = 1; i <= 3; i++) {
			try {
				// topic tag key消息关键词,多个key,用MessageConst.KEY_SEPARATOR隔开
				Message message = new Message("TopicTransaction", "Transaction" + i, "key",
						("Hello Rocket" + i).getBytes());
				SendResult sendResult = producer.sendMessageInTransaction(message, new TransactionExecuterImpl(), "sq");
				System.err.println(sendResult);
				// producer.se
			} catch (Exception e) {
				e.printStackTrace();
			}
			TimeUnit.SECONDS.sleep(1);
		}
		// 发送完消息之后,调用shutdown()方法关闭producer
		producer.shutdown();
	}

}

 

PushConsumer:

 

public class PushConsumer {
	/**
	 * 当前例子是PushConsumer用法,使用方式给用户感觉是消息从RocketMQ服务器推到了应用客户端。<br>
	 * 但是实际PushConsumer内部是使用长轮询Pull方式从Broker拉消息,然后再回调用户Listener方法<br>
	 * 
	 * @throws MQClientException
	 */
	public static void main(String[] args) throws InterruptedException, MQClientException {
		/**
		 * 一个应用创建一个Consumer,由应用来维护此对象,可以设置为全局对象或者单例<br>
		 * 注意:ConsumerGroupName需要由应用来保证唯一
		 */
		DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("transaction_consumer");
		consumer.setNamesrvAddr("192.168.50.112:9876;192.168.50.113:9876");

		/**
		 * 订阅指定topic下tags分别等于TagA或TagB
		 */
		consumer.subscribe("TopicTransaction", "*");

		/**
		 * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费<br>
		 * 如果非第一次启动,那么按照上次消费的位置继续消费
		 */
		consumer.setConsumeMessageBatchMaxSize(3);
		consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
		// 设置批量拉取消息
		// consumer.setPullBatchSize(1);

		consumer.registerMessageListener(new MessageListenerConcurrently() {

			// 顺序消费:

			/**
			 * 默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息
			 */
			public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
				// 执行TopicTest1的消费逻辑
				// 执行TagA的消费
				MessageExt message = msgs.get(0);
				try {
					String topic = message.getTopic();
					String msgBody = new String(message.getBody(), "utf-8");
					String tags = message.getTags();
					System.out.println("收到消息:" + " topic:" + topic + ", tags:" + tags + ", msg: " + msgBody);
				} catch (Exception e) {
					e.printStackTrace();
					if (message.getReconsumeTimes() == 2) {
						// 记录日志,持久化操作
						System.out.println("重试2次以后,还是没有成功...记录日志");
						return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
					}
					// 1s 2s 5s ....
					return ConsumeConcurrentlyStatus.RECONSUME_LATER;
				}

				// 消费者向mq服务器返回消费成功的消息
				return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
			}

		});

		/**
		 * Consumer对象在使用之前必须要调用start初始化,初始化一次即可<br>
		 */
		consumer.start();

		System.out.println("Consumer Started.");
	}

TransactionExecuterImpl:

/**
 * 执行本地事务,由客户端回调
 * 
 * @author Administrator
 *
 */
public class TransactionExecuterImpl implements LocalTransactionExecuter {

	public LocalTransactionState executeLocalTransactionBranch(Message msg, Object arg) {
		System.err.println("msg==" + new String(msg.getBody()));
		System.out.println("arg==" + arg);
		String tag = msg.getTags();
		if (tag.equals("Transaction1")) {
			// 这里有一个分阶段提交任务的概念
			System.out.println("这里处理业务逻辑,比如操作数据库,失败情况下 进行ROLLBACK");
			return LocalTransactionState.ROLLBACK_MESSAGE;// 与MQ建立通信,如果网络断了
		}

		// return LocalTransactionState.COMMIT_MESSAGE;

		// return LocalTransactionState.ROLLBACK_MESSAGE;
		return LocalTransactionState.UNKNOW;
	}

}

 

 

 

 

 

 

已标记关键词 清除标记
相关推荐
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页