一、普通消息
1、消息生产者Producer
1.1、同步生产
package com.lihua.rocketmq.producer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
/**
* 生产者发送同步消息
* @author 15594
*/
public class SyncProducer {
public static void main(String[] args) throws Exception {
//实例化消息生产者producer,group1表示这个生产者属于哪个集群
DefaultMQProducer producer = new DefaultMQProducer("group1");
//注册到注册中心,如果注册中心是集群,那么使用分号分割多个(IP:端口号)
producer.setNamesrvAddr("39.96.52.225:9876");
//设置失败重发次数,默认2次
producer.setRetryTimesWhenSendFailed(3);
//设置发送超时时限,默认3s,单位是毫秒
producer.setSendMsgTimeout(5000);
//启动producer实例
producer.start();
for (int i = 0; i < 10; i++) {
//创建消息,并指定Topic,Tag和消息体。topic:主题,标志了消息的类型 tags:标签,第二主题,子主题 keys:消息关键词。messagebody:具体的消息内容。
//注意:在rocketmq里面所有消息都必须以二进制传输
Message msg = new Message("someTopic" , "TagA2" , ("Hello RocketMQ2 " + i).getBytes(RemotingHelper.DEFAULT_CHARSET));
//设置key
msg.setKeys("kay"+i);
// 发送消息到一个Broker
SendResult sendResult = producer.send(msg);
// 通过sendResult返回消息是否成功送达
System.out.printf("%s%n", sendResult);
}
// 如果不再发送消息,关闭Producer实例。
producer.shutdown();
}
}
注意:在rocketmq里面所有消息都必须以二进制数组传输,发送时需要(“消息体(消息内容)”).getBytes(RemotingHelper.DEFAULT_CHARSET)
1.1、异步生产
package com.lihua.rocketmq.producer;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendCallback;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.common.message.Message;
/**
* 生产者异步发送消息
* @author 15594
*/
public class NSyncProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("39.96.52.225:9876");
// 指定异步发送失败后不进行重试发送
producer.setRetryTimesWhenSendAsyncFailed(0);
// 指定新创建的Topic的Queue数量为2,默认为4
producer.setDefaultTopicQueueNums(2);
producer.start();
//生产10条消息
for (int i = 0; i < 10; i++) {
//指定消息内容,并将内容转换成二进制数组
byte[] body = ("Hi," + i).getBytes();
try {
Message msg = new Message("someTopic", "myTag", body);
// 异步发送。指定结果回调内容
producer.send(msg, new SendCallback() {
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
System.out.println("异步发送完毕,等待回调");
// end-for
// sleep一会儿
// 由于采用的是异步发送,所以若这里不sleep,
// 则消息还未发送就会将producer给关闭,报错
Thread.sleep(3000);
producer.shutdown();
}
}
1.2、单向发送
单向发送——生产者向broker生产发送消息后,对于成功与否不给予回应。也就是broker没有向生产者返回回应信息
package com.lihua.rocketmq.producer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* //单向发送
* @author 15594
*/
public class OnewayProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("39.96.52.225:9876");
producer.start();
for (int i = 0; i < 10; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("single", "someTag", body);
// 单向发送,不会有响应。
producer.sendOneway(msg);
}
producer.shutdown();
System.out.println("producer shutdown");
}
}
2、消息消费者Consumer
2.1、消费者pull主动拉取
package com.lihua.rocketmq.consumer;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.PullResult;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.message.MessageQueue;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* 信息消费者
* pull消费者,消费者主动拉取消息
* 默认集群消费
* @author 15594
*/
public class MQPullConsumer {
//用户保存消费进度,(本地用map保存)
private static final Map<MessageQueue,Long> OFFSE_TABLE = new HashMap<MessageQueue,Long>();
public static void main(String[] args) throws Exception {
//指定消费者属于哪个集群
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("cg");
//指定注册中心
consumer.setNamesrvAddr("39.96.52.225:9876:9876");
/**
* 指定消费模式:默认集群,
* BROADCASTING(广播)
* CLUSTERING(集群)
* */
consumer.setMessageModel(MessageModel.CLUSTERING);
consumer.start();
// 从指定topic中拉取所有消息队列
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("someTopic");
System.out.println(mqs.size());
//这个实际上(一个线程)只遍历了一遍,因为一个消费者只能消费一个队列。
for(MessageQueue mq:mqs){
System.out.println(mq.getQueueId());
try {
// 获取消息的offset(消费进度),指定从store中获取
long offset = consumer.fetchConsumeOffset(mq,true);
System.out.println("broker上保存的消费进度:"+offset);
System.out.println("consumer from the queue:"+mq+":"+offset);
//(死循环)定时去一个队列拉取
while(true){
System.out.println(System.currentTimeMillis());
//定时(阻塞)获取消息,一次性最多获取32条
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null,
//调用方法获取上次消费的消息的下标,用自己定义的map保存(key为mq,value为offset)OFFSE_TABLE
getMessageQueueOffset(mq),
4);
//记录该mq队列下一个消费的offset(进度)用自己定义的map保存(key为mq(队列),value为offset)OFFSE_TABLE
putMessageQueueOffset(mq,pullResult.getNextBeginOffset());
System.out.println("本次从队列里拉取消息到:"+OFFSE_TABLE.get(mq)+"这个位置,下次从这个开始拉取,防止重复拉取消费");
switch(pullResult.getPullStatus()){
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
System.out.println(new String(m.getBody()));
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
consumer.shutdown();
}
// 保存上次消费的消息下标,(记录下次该从哪里开始继续拉取消息消费)
private static void putMessageQueueOffset(MessageQueue mq,
long nextBeginOffset) {
OFFSE_TABLE.put(mq, nextBeginOffset);
}
/**
* 获取上次消费的消息的下标,(获取mq队列上次消费位置,从该位置继续消费,因为指定了一次性只能拉取32条)
* */
private static Long getMessageQueueOffset(MessageQueue mq) {
Long offset = OFFSE_TABLE.get(mq);
if(offset != null){
return offset;
}
return 0L;
}
}
2.2、broker向消费者push推送
package com.lihua.rocketmq.consumer;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import java.util.List;
/**
*
* 信息消费者
* 默认集群消费
* @author 15594
*/
public class PushConsumer {
public static void main(String[] args) {
//push消费者,由Broker推送消息
try {
pushConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void pullConsumer() {
//定义一个pull消费者
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("cg");
//指定nameserver
consumer.setNamesrvAddr("39.96.52.225:9876");
//指定消费的topic和tag
}
private static void pushConsumer() throws Exception {
//定义一个push消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//指定nameserver
consumer.setNamesrvAddr("39.96.52.225:9876");
// 指定从第一条消息开始消费,指定消费offset
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 指定消费topic与tag *号表示全部tag
consumer.subscribe("someTopic", "*");
// 指定采用“广播模式”进行消费,默认为“集群模式”
// consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册消息监听器 ,
consumer.registerMessageListener(new MessageListenerConcurrently() {
// 一旦broker中有了其订阅的消息就会触发该方法的执行,
// 其返回值为当前consumer消费的状态
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// 逐条消费消息
for (MessageExt msg : msgs) {
System.out.println(msg);
}
// 返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者消费
consumer.start();
System.out.println("Consumer Started");
}
}
二、顺序消息
顺序消息指的是,严格按照消息的发送顺序进行消费的消息,因为有一些消息必须按照顺序来消费(执行).比如商城购物必须按照以下流程消费:
订单T0000001:未支付 --> 订单T0000001:已支付 --> 订单T0000001:发货中 --> 订单T0000001:发货,所以在发送、消费消息时必须按照顺序来消费。
如何确保消息的顺序性:
通过一定的策略,将消息放置在Queue中,然后消费者再采用一定的(相同的)策略从队列里面将消息取出来,这样才能够保证消费的顺序性。
1、全局有序 —— 一个只有一个队列Topic
发送和消费参与的Queue只有一个时所保证的有序是整个Topic中消息的顺序, 称为全局有序。
在创建Topic时指定Queue的数量。有三种指定方式:
1)在代码中创建Producer时,可以指定其自动创建的Topic的Queue数量
2)在RocketMQ可视化控制台中手动创建Topic时指定Queue数量
3)使用mqadmin命令手动创建Topic时指定Queue数量
全局有序性只需要在生产者里指定一个唯一的队列,按照顺序就行存放消息就好。
package com.lihua.rocketmq.producer;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.exception.RemotingException;
/**
* 全局有序
* @author 15594
*/
public class OnewayProducer {
public static void main(String[] args) throws Exception{
DefaultMQProducer producer = new DefaultMQProducer("group1");
producer.setNamesrvAddr("39.96.52.225:9876");
//只指定一个队列存放
producer.setDefaultTopicQueueNums(1);
producer.start();
for (int i = 0; i < 10; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("single", "someTag", body);
// 单向发送,不会有响应。
producer.sendOneway(msg);
}
producer.shutdown();
System.out.println("producer shutdown");
}
}
2、分区有序 —— 通过选择算法实现
- 生产者:
/**
* 分区有序性
* @author 15594
*/
public class OrderedProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("39.96.52.225:9876:9876");
producer.start();
//选择key(根据选择key和选择算法可以选出一个Queue,用来存放顺序消息A),这个key由用户自己指定.比如时订单id
Integer orderId = 124;
//假设要按顺序发生4条订单消息,分别是订单T0000001:未支付 (状态0) --> 订单T0000001:已支付 (状态1)--> 订单T0000001:发货中 (状态2)--> 订单T0000001:发货 (状态3)
for (int i = 0; i <4 ; i++) {
byte[] body = ("订单状态:" + i).getBytes();
Message msg = new Message("OrderedTopic", "TagA", body);
//前面虽然选出了Queue,但是这个Queue里面可以有不属于A的其他的消息,因为当有很多选择key时,再好的选择算法也会发生碰撞(跟集合set一样),
// 所以我们要将选择key放到消息里面.在消费消息时,加一个判断条件if(isKey)就能判断出这个消息是否属于这个选择key的
msg.setKeys(orderId.toString());
SendResult sendResult = producer.send(msg, new MessageQueueSelector() {
public MessageQueue select(List<MessageQueue> mqs, Message msg, Object arg){
System.out.println(arg);
Integer id = (Integer) arg;
int index = id % mqs.size();
return mqs.get(index);
}
}, orderId);
System.out.println(sendResult);
}
producer.shutdown();
}
}
- 消费者
class OrderedConsumer{
public static void main(String[] args) throws Exception {
//指定消费者属于哪个集群
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("cg");
//指定注册中心
consumer.setNamesrvAddr("39.96.52.225:9876:9876");
consumer.start();
Set<MessageQueue> mqs = consumer.fetchSubscribeMessageQueues("OrderedTopic");
//1.采用相同的选择算法,根据订单id计算出所在队列Queue
//订单号
Integer orderId = 126;
//利用相同的选择算法,计算出该订单号对于的顺序消息在哪个Queue队列里
int index = orderId % mqs.size();
Object[] objects = mqs.toArray();
MessageQueue mq = (MessageQueue) objects[index];
//2. 当选择key很多时(也就是订单号很多时,有可能存在不是这个订单的消息),因此根据订单号就行筛选
int i = 0;
while(true){
//定时(阻塞)获取消息,一次性最多获取32条
PullResult pullResult = consumer.pullBlockIfNotFound(mq, null,
//调用方法获取上次消费的消息的下标,用自己定义的map保存(key为mq,value为offset)OFFSE_TABLE
i,
4);
i= i +4;
switch(pullResult.getPullStatus()){
case FOUND:
List<MessageExt> messageExtList = pullResult.getMsgFoundList();
for (MessageExt m : messageExtList) {
//当很多选择key时,判断这个消息是否属于这个订单号的
if (m.getKeys().equals(orderId.toString())){
//进行消费
System.out.println(new String(m.getBody()));
}
}
break;
case NO_MATCHED_MSG:
break;
case NO_NEW_MSG:
break;
case OFFSET_ILLEGAL:
break;
}
}
}
}
三、延迟消息
1、什么是延迟消息
一般普通消息,在被消费者获取了以后就会立刻消费,而延迟消息在消费者获取消息后,会根据指定的时间进行延后消费。
2、延时等级(延迟时间)
延时消息的延迟时长不支持随意时长的延迟,是通过特定的延迟等级来指定的。延时等级定义在RocketMQ服务端的MessageStoreConfig类中的如下变量中:
messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d
3、延迟消息处理过程
Producer将消息发送到Broker后,Broker会首先将消息写入到commitlog文件,然后需要将其分发到相应的consumequeue。不过,在分发之前,系统会先判断消息中是否带有延时等级。若没有,则直接正常分发;若有则需要经历一个复杂的过程:
- 修改消息的Topic为SCHEDULE_TOPIC_XXXX
- 根据延时等级,在consumequeue目录中SCHEDULE_TOPIC_XXXX主题下创建出相应的queueId
目录与consumequeue文件(如果没有这些目录与文件的话)。
延迟等级delayLevel与queueId的对应关系为queueId = delayLevel -1
需要注意,在创建queueId目录时,并不是一次性地将所有延迟等级对应的目录全部创建完毕,而是用到哪个延迟等级创建哪个目录
-
修改消息索引单元内容。索引单元中的Message Tag HashCode部分原本存放的是消息的Tag的Hash值。现修改为消息的投递时间。投递时间是指该消息被重新修改为原Topic后再次被写入到commitlog中的时间。投递时间 = 消息存储时间 + 延时等级时间。消息存储时间指的是消息被发送到Broker时的时间戳。
-
将消息索引写入到SCHEDULE_TOPIC_XXXX主题下相应的consumequeue中
SCHEDULE_TOPIC_XXXX目录中各个延时等级Queue中的消息是如何排序的? 是按照消息投递时间排序的。一个Broker中同一等级的所有延时消息会被写入到consumequeue目录中SCHEDULE_TOPIC_XXXX目录下相同Queue中。即一个Queue中消息投递时间的延迟等级时间是相同的。那么投递时间就取决于于消息存储时间了。即按照消息被发送到Broker的时间进行排序的。
4、代码测试
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 延时消息<br>
* 延时时间:<br>
* messageDelayLevel = 1s 5s 10s 30s 1m 2m 3m 4m 5m 6m 7m 8m 9m 10m 20m 30m 1h 2h 1d
* 对应延时等级<br>
* 1 2 3 4 5 6 7...
* 0级默认没有延迟
* @author 15594
*/
public class DelayProducer {
public static void main(String[] args) throws Exception {
DefaultMQProducer producer = new DefaultMQProducer("pg");
producer.setNamesrvAddr("192.168.109.101:9876");
producer.start();
for (int i = 0; i < 10; i++) {
byte[] body = ("Hi," + i).getBytes();
Message msg = new Message("TopicB", "someTag", body);
// 指定消息延迟等级为3级,即延迟10s
msg.setDelayTimeLevel(3);
SendResult sendResult = producer.send(msg);
// 输出消息被发送的时间
System.out.print(new SimpleDateFormat("mm:ss").format(new Date()));
System.out.println(" ," + sendResult);
}
producer.shutdown();
}
}
四、分布式事务
什么是事务
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、一致性(consistency)、隔离性(isolation)和持久性(durability)。
- 原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功一部分失败那么成功的操作要全部回滚到执行前的状态。
- 一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。
- 隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
- 持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
1、分布式事务的cap理论
分布式系统在设计时只能在一致性(Consistency)、可用性(Availability)、分区容忍性(Partition Tolerance)中满足两种,无法兼顾三种。
-
可用性: 通过搭建集群提供高可用,防止单点故障。
-
一致性: 集群中的节点数据保持一致。(当需要高可用时,就必须搭建集群,集群节点越多可用性越高,但是节点间的数据一致性很难得到保证。)
-
分区容忍性(Partition Tolerance): 分区容忍性就是允许系统通过网络协同工作,分区容忍性要解决由于网络分区导致数据的不完整及无法访问等问题。(想要高可用和数据强一致性节点,那么节点间的通讯就必须无故障(网络必须很好),但是节点又多数据又要强一致,那么网络的压力就会很大,网络压力(网络故障)大,就可能导致数据的不完整及无法访问等问题)
2、分布式事务与本地事务
-
本地事务: (一个事务的全部操作都在一个机器(一个系统)上,在本地内存中保证事务的进行)
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。mysql的事务就是一种本地事务。 -
分布式事务:(一个事务的多个操作落在不同的机器(系统)上,那么这个事务就需要通过网络协调完成)
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是分布式事务,如下图:
另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,当一次事务需要操作多个数据源,此时也属于分布式事务,当系统作了数据库拆分后会出现此种情况
上面两种分布式事务表现形式第一种用的最多。
3、存在分布式事务问题的例子:
电商系统中,当有用户下单后,除了在订单表(在系统A中)插入一条记录外,对应商品表(在系统B中)的这个商品数量必须减1吧,怎么保证?!在搜索广告系统中,当用户点击某广告后,除了在点击事件表(在系统A中)中增加一条记录外,还得去商家账户表(在系统B中)中找到这个商家并扣除广告费吧,怎么保证? 像分布式系统中,这些操作就是分布式事务,我们在操作时需要开启事务,使数据的修改具有一致性。
4、解决方案
1.分布式事务—————— 两阶段提交协议
两阶段提交协议(Two-phase Commit,2PC是基于XA分布式协议实现的)经常被用来实现分布式事务。一般分为协调器TC和若干事务执行者两种角色,这里的事务执行者就是具体的数据库,协调器可以和事务执行器在一台机器上。
什么是XA:
XA是一个分布式事务协议,由Tuxedo提出。XA中大致分为两部分: 事务管理器(协调器TC)和本地资源管理器(数据库)。其中 本地资源管理器往往由数据库实现,比如Oracle、DB2这些商业数据库都实现了XA接口,而事务管理器作为全局的调度者,负责各个本地资源的提交和回滚。
上述两阶段提交的能实在是太差,根本不适合高并发的系统。为什么?
1)两阶段提交涉及多次节点间的网络通信,通信时间太长!
2)事务时间相对于变长了,锁定的资源的时间也变长了,造成资源等待时间也增加好多!
总之:2PC 是一种尽量保证强一致性的分布式事务,因此它是同步阻塞的,而同步阻塞就导致长久的资源锁定问题,总体而言效率低,并且存在单点故障问题,在极端条件下存在数据不一致的风险。
正是由于分布式事务存在很严重的性能问题,大部分高并发服务都在避免使用,往往通过**其他途径(消息队列解决同步阻塞)**来解决数据一致性问题。
5、rockermq+最终一致性实现分布式事务
如果仔细观察生活的话,生活的很多场景已经给了我们提示。
比如在北京很有名的姚记炒肝点了炒肝并付了钱后,他们并不会直接把你点的炒肝给你,而是给你一张小票,然后让你拿着小票到出货区排队去取。为什么他们要将付钱和取货两个动作分开呢?原因很多,其中一个很重要的原因是为了使他们接待能力增强(并发量更高)。
还是回到我们的问题,只要这张小票在,你最终是能拿到炒肝的。同理转账服务也是如此,当支付宝账户扣除1万后,我们只要生成一个凭证(消息)即可,这个凭证(消息)上写着“让余额宝账户增加1万”,只要这个凭证(消息)能可靠保存,我们最终是可以拿着这个凭证(消息)让余额宝账户增加1万的,即我们能依靠这个凭证(消息)完成最终一致性。
上面的小票和凭证就是我们rockermq中的消息。
5.1、rocketmq事务原理
消息事务就是基于消息中间件的两阶段提交,本质上是对消息中间件的一种特殊利用,它是将本地事务和发消息放在了一个分布式事务里,保证要么本地操作成功成功并且对外发消息成功,要么两者都失败,开源的RocketMQ就支持这一特性,具体原理如下:
half消息(半事务消息):
暂不能投递的消息,发送方已经成功地将消息发送到了Broker,但是Broker未收到最终确认指令,此时该消息被标记成“暂不能投递”状态,即不能被消费者看到。处于该种状态下的消息即半事务消息。
第一步先给 Broker 发送事务消息即半消息,半消息不是说一半消息,而是这个消息对消费者来说不可见,然后发送成功后发送方再执行本地事务。
再根据本地事务的结果向 Broker 发送 Commit 或者 RollBack 命令。
并且 RocketMQ 的发送方会提供一个反查事务状态接口,如果一段时间内半消息没有收到任何操作请求,那么 Broker 会通过反查接口得知发送方事务是否执行成功,然后执行 Commit 或者 RollBack 命令。
如果是 Commit 那么订阅方就能收到这条消息,然后再做对应的操作,做完了之后再消费这条消息即可。
如果是 RollBack 那么订阅方收不到这条消息,等于事务就没执行过。
可以看到通过 RocketMQ 还是比较容易实现的,RocketMQ 提供了事务消息的功能,我们只需要定义好事务反查接口即可。
5.2、代码实现
本地事务和消息回查
package com.lihua.rocketmq.transaction;
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.producer.TransactionListener;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.common.message.MessageExt;
/**
* 执行本地事务和消息回查
*
* @author 15594
*/
public class ICBCTransactionListener implements TransactionListener {
//执行本地事务 ,,注意 参数Object arg 是生产者什么事务监听器是传进来的。
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
System.out.println("预提交消息成功:" + msg);
try{
//模拟执行回查
int i = 1/0;
//假设A要转账i元给B
System.out.println("数据库查询余额的操作:"+"查询到余额为100");
//余额
int over = 100;
//转账金额
int transfer = Integer.parseInt(msg.getKeys());
if (over-transfer>=0){
System.out.println("余额充足,转账成功!");
//提交事务
return LocalTransactionState.COMMIT_MESSAGE;
} else{
System.out.println("余额不足,转账失败!");
//回滚事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}catch (Exception e){
System.out.println(e.getMessage());
return LocalTransactionState.UNKNOW;
}
}
/**
* 本地事务回查:<br>
*
* 关于消息回查,有三个常见的属性设置。它们都在broker加载的配置文件中设置,例如:
*
* transactionTimeout=20,指定TM在20秒内应将最终确认状态发送给TC,否则引发消息回查。默
* 认为60秒
* transactionCheckMax=5,指定最多回查5次,超过后将丢弃消息并记录错误日志。默认15次。
* transactionCheckInterval=10,指定设置的多次消息回查的时间间隔为10秒。默认为60秒。<br>
*
* 引发消息回查的原因最常见的有两个:
*
* 1)回调操作返回UNKNWON
*
* 2)TC没有接收到TM的最终全局事务确认指令
* */
public LocalTransactionState checkLocalTransaction(MessageExt msg) {
System.out.println("执行回查:"+msg.getBody());
//注意:这里的回查并不是要回调本地事务。是查询事务运行过,运行的结果是什么。
//比如:由于RocketMQ迟迟没有收到消息的确认消息,因此主动询问这条prepare消息,是否正常?
//可以查询数据库看这条数据是否已经处理。
//查询到数据库已近成功扣款,所以返回LocalTransactionState.COMMIT_MESSAGE
boolean DB = true;
if (DB){
//提交事务
return LocalTransactionState.COMMIT_MESSAGE;
}else {
//回滚事务
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
}
事务消息生产者:
package com.lihua.rocketmq.transaction;
import org.apache.rocketmq.client.producer.DefaultMQProducer;
import org.apache.rocketmq.client.producer.SendResult;
import org.apache.rocketmq.client.producer.TransactionMQProducer;
import org.apache.rocketmq.common.message.Message;
import org.apache.rocketmq.remoting.common.RemotingHelper;
/**
*
*
* @author 15594
*/
public class TransactionProducer {
public static void main(String[] args) throws Exception {
// 创建事务生产者
TransactionMQProducer producer = new TransactionMQProducer("tpg");
//注册到注册中心
producer.setNamesrvAddr("39.96.52.225:9876");
// 为生产者添加事务监听器
producer.setTransactionListener(new ICBCTransactionListener());
//启动producer实例
producer.start();
for (int i = 0; i < 10; i++) {
Message msg = new Message("transactionTopic" , "TagA" , ("分布式事务" + "转账99元"+i).getBytes(RemotingHelper.DEFAULT_CHARSET));
//为了方便测试通过key来设置转账金额
msg.setKeys("99");
// 发送事务消息
// 第二个参数用于指定在执行本地事务时要使用的业务参数
SendResult sendResult = producer.sendMessageInTransaction(msg,null);
System.out.println("发送结果为:" + sendResult.getSendStatus());
}
// 如果不再发送消息,关闭Producer实例。
//注意:不能关掉producer否则回查会失败,因为回查带一分钟后台触发,如果已近关闭producer,就无法触发回查了
//producer.shutdown();
}
}
消费者:(普通消费者即可,因为在消息事务中消费者是不用参与进来的,如果能消费到消息说明事务成功了,如果没有消费到消息说明事务失败了)
package com.lihua.rocketmq.consumer;
import org.apache.rocketmq.client.consumer.DefaultMQPullConsumer;
import org.apache.rocketmq.client.consumer.DefaultMQPushConsumer;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
import org.apache.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
import org.apache.rocketmq.client.consumer.listener.MessageListenerConcurrently;
import org.apache.rocketmq.client.exception.MQClientException;
import org.apache.rocketmq.common.consumer.ConsumeFromWhere;
import org.apache.rocketmq.common.message.MessageExt;
import org.apache.rocketmq.common.protocol.heartbeat.MessageModel;
import java.util.List;
/**
*
* 信息消费者
* 默认集群消费
* @author 15594
*/
public class PushConsumer {
public static void main(String[] args) {
//push消费者,由Broker推送消息
try {
pushConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}
private static void pullConsumer() {
//定义一个pull消费者
DefaultMQPullConsumer consumer = new DefaultMQPullConsumer("cg");
//指定nameserver
consumer.setNamesrvAddr("39.96.52.225:9876");
//指定消费的topic和tag
}
private static void pushConsumer() throws Exception {
//定义一个push消费者
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("cg");
//指定nameserver
consumer.setNamesrvAddr("39.96.52.225:9876");
// 指定从第一条消息开始消费,指定消费offset
consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_FIRST_OFFSET);
// 指定消费topic与tag *号表示全部tag
consumer.subscribe("transactionTopic", "*");
// 指定采用“广播模式”进行消费,默认为“集群模式”
consumer.setMessageModel(MessageModel.BROADCASTING);
// 注册消息监听器 ,
consumer.registerMessageListener(new MessageListenerConcurrently() {
// 一旦broker中有了其订阅的消息就会触发该方法的执行,
// 其返回值为当前consumer消费的状态
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
// 逐条消费消息
for (MessageExt msg : msgs) {
System.out.println(new String(msg.getBody()));
}
// 返回消费状态:消费成功
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
// 开启消费者消费
consumer.start();
System.out.println("Consumer Started");
}
}
5.3、最终一致性
通过代码我们可以看到,生产者与消费者的数据并不是强一致性的(生产者扣款成功,消费者立刻增加余额)。因为消费者消费消息是有一定延迟的,如果消费者一直执行不成功(消费不到增加余额的消息),那么一致性会被破坏。当然,rocketmq中消息最少被消费一次的原则。所以这个消息肯定会被消费。只是可能会延迟。
5.4、分布式事务出现的问题
- 消除幂等
rocketmq中能保证消息最少被消费一次,但是不能保证消息的重复性, 所以要注意消除幂等。 - 死信队列
事务消息也是一样,当半消息被commit了之后确实就是普通消息了,如果订阅者一直不消费或者消费不了则会一直重试,到最后进入死信队列。其实这也算最大努力。
所以最大努力通知其实只是表明了一种柔性事务的思想:我已经尽力我最大的努力想达成事务的最终一致了。
适用于对时间不敏感的业务,例如短信通知。