分布式事务——幂等设计(rocketmq案例)
幂等指的就是执行多次和执行一次的效果相同,主要是为了防止数据重复消费。MQ中为了保证消息的可靠性,生产者发送消息失败(例如网络超时)会触发 "重试机制",它不是生产者重试而是MQ自动触发的重试机制, 而这种情况下消费者就会收到两条消息,比如明明只需要扣一次款, 可是消费者却执行了2次。为了解决幂等问题,每一个消息应该有一个全局的唯一的标识,当处理过这条消息后,就把这个标识保存到数据库或者redis中,在处理消息前前判断这个标识记录为空就好了。像activemq中msgId就是唯一的,我们可以直接拿这个id来判断,但是rocketmq重试机制不一样,它重发会产生一个新的id,但是它提供了setKeys()这个api,我们可以给key设置一个唯一的流水编号来加以判断。(重试机制是不存在并发问题的,它是间隔一段时间自动促发的)。
1. 导入依赖( 生产者和消费者的依赖都一样)
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.5.7.RELEASE</version>
- <relativePath/>
- </parent>
- <!-- springcloud
- <dependencyManagement>
- <dependencies>
- <dependency>
- <groupId>org.springframework.cloud</groupId>
- <artifactId>spring-cloud-dependencies</artifactId>
- <version>Camden.SR6</version>
- <type>pom</type>
- <scope>import</scope>
- </dependency>
- </dependencies>
- </dependencyManagement>
- -->
- <properties>
- <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
- <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
- <java.version>1.8</java.version>
- </properties>
- <dependencies>
- <!-- webmvc -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <!-- 集成lombok 框架(get/set) -->
- <dependency>
- <groupId>org.projectlombok</groupId>
- <artifactId>lombok</artifactId>
- </dependency>
- <!-- RocketMq -->
- <dependency>
- <groupId>com.alibaba.rocketmq</groupId>
- <artifactId>rocketmq-client</artifactId>
- <version>3.2.6</version>
- </dependency>
- <!-- 热加载 -->
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-devtools</artifactId>
- <optional>true</optional>
- </dependency>
- <!-- jackson -->
- <dependency>
- <groupId>com.alibaba</groupId>
- <artifactId>fastjson</artifactId>
- <version>1.2.30</version>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
pom.xml
2. 生产者配置参数和配置文件
- #该应用是否启用生产者
- #rocketmq.producer.isOnOff=on
- #发送同一类消息的设置为同一个group,保证唯一,默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
- rocketmq.producer.groupName=mqtest
- #mq的nameserver地址
- rocketmq.producer.namesrvAddr=192.168.5.7:9876
- #消息最大长度 默认1024*4(4M)
- rocketmq.producer.maxMessageSize=4096
- #发送消息超时时间,默认3000
- rocketmq.producer.sendMsgTimeout=3000
- #发送消息失败重试次数,默认2
- rocketmq.producer.retryTimesWhenSendFailed=3
rocketmq.properties
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.PropertySource;
- import com.alibaba.rocketmq.client.exception.MQClientException;
- import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
- /**
- * 生产者配置
- */
- @PropertySource("classpath:rocketmq.properties")
- @Configuration
- public class MQProducerConfiguration {
- public static final Logger LOGGER = LoggerFactory.getLogger(MQProducerConfiguration.class);
- /**
- * 发送同一类消息的设置为同一个group,保证唯一,默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
- */
- @Value("${rocketmq.producer.groupName}")
- private String groupName;
- /** 服务器地址 */
- @Value("${rocketmq.producer.namesrvAddr}")
- private String namesrvAddr;
- /**
- * 消息最大大小,默认4M
- */
- @Value("${rocketmq.producer.maxMessageSize}")
- private Integer maxMessageSize ;
- /**
- * 消息发送超时时间,默认3秒
- */
- @Value("${rocketmq.producer.sendMsgTimeout}")
- private Integer sendMsgTimeout;
- /**
- * 消息发送失败重试次数,默认2次
- */
- @Value("${rocketmq.producer.retryTimesWhenSendFailed}")
- private Integer retryTimesWhenSendFailed;
- @Bean
- public DefaultMQProducer getRocketMQProducer() {
- DefaultMQProducer producer;
- producer = new DefaultMQProducer(this.groupName);
- producer.setNamesrvAddr(this.namesrvAddr);
- //如果需要同一个jvm中不同的producer往不同的mq集群发送消息,需要设置不同的instanceName
- //producer.setInstanceName(instanceName);
- producer.setMaxMessageSize(this.maxMessageSize);
- producer.setSendMsgTimeout(this.sendMsgTimeout);
- //如果发送消息失败,设置重试次数,默认为2次
- producer.setRetryTimesWhenSendFailed(this.retryTimesWhenSendFailed);
- try {
- producer.start();
- LOGGER.info(String.format("rocketmq producer start "));
- } catch (MQClientException e) {
- LOGGER.error(String.format("producer is error {}", e.getMessage(),e));
- }
- return producer;
- }
- }
MQProducerConfiguration
3. 生产者发送消息
- import java.util.UUID;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.web.bind.annotation.RequestMapping;
- import org.springframework.web.bind.annotation.RestController;
- import com.alibaba.rocketmq.client.producer.DefaultMQProducer;
- import com.alibaba.rocketmq.client.producer.SendResult;
- import com.alibaba.rocketmq.common.message.Message;
- import lombok.extern.slf4j.Slf4j;
- @RestController
- @Slf4j
- public class TestController {
- /**使用RocketMq的生产者*/
- @Autowired
- private DefaultMQProducer defaultMQProducer;
- @RequestMapping("/send")
- public void send(){
- String msg = "幂等";
- log.info("开始发送消息:"+msg);
- try {
- // arg0主题名称 arg1分组 arg2内容
- Message sendMsg = new Message("DemoTopic","wulei",(msg).getBytes());
- // 注意: activemq的msgId是唯一的,但是rocketmq的不是,所以幂等不能用id来判断,我们可以通过setKeys来解决,一般都是业务id,这里用随机数代替。
- sendMsg.setKeys(UUID.randomUUID().toString());
- SendResult sendResult = defaultMQProducer.send(sendMsg);
- //默认3秒超时
- log.info("消息发送响应信息:"+sendResult.toString());
- } catch (Exception e) {
- e.printStackTrace();
- }
- }
- }
TestController
4. 消费者配置参数和配置文件
- ##该应用是否启用消费者
- #rocketmq.consumer.isOnOff=on
- #发送同一类消息的设置为同一个group,保证唯一,默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
- rocketmq.consumer.groupName=mqtest
- #mq的nameserver地址
- rocketmq.consumer.namesrvAddr=192.168.5.7:9876
- #该消费者订阅的主题和tags("*"号表示订阅该主题下所有的tags),格式:topic~tag1||tag2||tag3;topic2~*;
- rocketmq.consumer.topics=DemoTopic~*;
- rocketmq.consumer.consumeThreadMin=20
- rocketmq.consumer.consumeThreadMax=64
- #设置一次消费消息的条数,默认为1条
- rocketmq.consumer.consumeMessageBatchMaxSize=1
rocketmq.properties
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.beans.factory.annotation.Value;
- import org.springframework.context.annotation.Bean;
- import org.springframework.context.annotation.Configuration;
- import org.springframework.context.annotation.PropertySource;
- import com.alibaba.rocketmq.client.consumer.DefaultMQPushConsumer;
- import com.alibaba.rocketmq.client.exception.MQClientException;
- import com.alibaba.rocketmq.common.consumer.ConsumeFromWhere;
- import com.wulei.listener.MQConsumeMsgListenerProcessor;
- /**
- * 消费者配置
- */
- @PropertySource("classpath:rocketmq.properties")
- @Configuration
- public class MQConsumerConfiguration {
- public static final Logger LOGGER = LoggerFactory.getLogger(MQConsumerConfiguration.class);
- // 地址
- @Value("${rocketmq.consumer.namesrvAddr}")
- private String namesrvAddr;
- // 发送同一类消息的设置为同一个group,保证唯一,默认不需要设置,rocketmq会使用ip@pid(pid代表jvm名字)作为唯一标示
- @Value("${rocketmq.consumer.groupName}")
- private String groupName;
- // 该消费者订阅的主题和tags("*"号表示订阅该主题下所有的tags)
- @Value("${rocketmq.consumer.topics}")
- private String topics;
- @Value("${rocketmq.consumer.consumeThreadMin}")
- private int consumeThreadMin;
- @Value("${rocketmq.consumer.consumeThreadMax}")
- private int consumeThreadMax;
- @Value("${rocketmq.consumer.consumeMessageBatchMaxSize}")
- private int consumeMessageBatchMaxSize;
- @Autowired
- private MQConsumeMsgListenerProcessor mqMessageListenerProcessor;
- @Bean
- public DefaultMQPushConsumer getRocketMQConsumer(){
- DefaultMQPushConsumer consumer = new DefaultMQPushConsumer(groupName);
- consumer.setNamesrvAddr(namesrvAddr);
- consumer.setConsumeThreadMin(consumeThreadMin);
- consumer.setConsumeThreadMax(consumeThreadMax);
- consumer.registerMessageListener(mqMessageListenerProcessor);
- /**
- * 设置Consumer第一次启动是从队列头部开始消费还是队列尾部开始消费
- * 如果非第一次启动,那么按照上次消费的位置继续消费
- */
- consumer.setConsumeFromWhere(ConsumeFromWhere.CONSUME_FROM_LAST_OFFSET);
- /**
- * 设置消费模型,集群还是广播,默认为集群
- */
- //consumer.setMessageModel(MessageModel.CLUSTERING);
- /**
- * 设置一次消费消息的条数,默认为1条
- */
- consumer.setConsumeMessageBatchMaxSize(consumeMessageBatchMaxSize);
- try {
- /**
- * 设置该消费者订阅的主题和tag,如果是订阅该主题下的所有tag,则tag使用*;如果需要指定订阅该主题下的某些tag,则使用||分割,例如tag1||tag2||tag3
- */
- String[] topicTagsArr = topics.split(";");
- for (String topicTags : topicTagsArr) {
- String[] topicTag = topicTags.split("~");
- consumer.subscribe(topicTag[0],topicTag[1]);
- }
- consumer.start();
- LOGGER.info("consumer is start !!! groupName:{},topics:{},namesrvAddr:{}",groupName,topics,namesrvAddr);
- }catch (MQClientException e){
- LOGGER.error("consumer is start !!! groupName:{},topics:{},namesrvAddr:{}",groupName,topics,namesrvAddr,e);
- }
- return consumer;
- }
- }
MQConsumerConfiguration
5. 消费者监听消息
- import java.util.HashMap;
- import java.util.List;
- import org.slf4j.Logger;
- import org.slf4j.LoggerFactory;
- import org.springframework.stereotype.Component;
- import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyContext;
- import com.alibaba.rocketmq.client.consumer.listener.ConsumeConcurrentlyStatus;
- import com.alibaba.rocketmq.client.consumer.listener.MessageListenerConcurrently;
- import com.alibaba.rocketmq.common.message.MessageExt;
- @Component
- public class MQConsumeMsgListenerProcessor implements MessageListenerConcurrently{
- private static final Logger logger = LoggerFactory.getLogger(MQConsumeMsgListenerProcessor.class);
- // 假装这是一个redis
- private HashMap<String, String> myredis = new HashMap<String, String>();
- /**
- * 默认msgs里只有一条消息,可以通过设置consumeMessageBatchMaxSize参数来批量接收消息<br/>
- * 不要抛异常,如果没有return CONSUME_SUCCESS ,consumer会重新消费该消息,直到return CONSUME_SUCCESS
- */
- @Override
- public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
- // if(CollectionUtils.isEmpty(msgs)){
- // logger.info("接受到的消息为空,不处理,直接返回成功");
- // return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- // }
- //
- //for(MessageExt messageExt : msgs) { }
- MessageExt messageExt = msgs.get(0);
- String keys = messageExt.getKeys();// 自定义的唯一key
- String msgId = null; // 消息id(不是唯一的)
- String msgContext = null; // 消息内容
- int reconsume = 0; // 重试次数
- if(messageExt.getTopic().equals("DemoTopic") && messageExt.getTags().equals("wulei")){
- if(myredis.get(keys)==null) {
- //logger.info("接受到的消息为:"+messageExt.toString());
- msgId = messageExt.getMsgId();
- msgContext = new String(messageExt.getBody());
- reconsume = messageExt.getReconsumeTimes();
- try {
- int i = 1/0;
- System.out.println("消费成功: id:"+msgId+" msg"+msgContext+" 次数"+reconsume);
- myredis.put(messageExt.getKeys(), msgContext);
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- } catch (Exception e) {
- // 重试3次就不在重试了,直接返回消费成功状态,并触发人工补偿机制。
- if(reconsume==2) {
- myredis.put(messageExt.getKeys(), msgContext);
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }else {
- // 一般消费者这边尽量不要抛异常,它失败就会触发重试机制。如果非要抛异常可以在try{}catch{}里面return ConsumeConcurrentlyStatus.RECONSUME_LATER(表示失败让他重试)
- return ConsumeConcurrentlyStatus.RECONSUME_LATER;
- }
- }
- }else {
- // 已经消费过就不要再重试了,直接返回成功。
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- }else {
- // 不存在不要再重试了,直接返回成功。
- return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
- }
- }
- }
MQConsumeMsgListenerProcessor
分布式事务——幂等设计(rocketmq案例)的更多相关文章
- 分布式事务之如何基于RocketMQ的事务消息特性实现分布式系统的最终一致性?
导读 在之前的文章中我们介绍了如何基于RocketMQ搭建生产级消息集群,以及2PC.3PC和TCC等与分布式事务相关的基本概念(没有读过的读者详见
- 了解一下Mysql分布式事务及优缺点、使用案例(php+mysql)
在开发中,为了降低单点压力,通常会根据业务情况进行分表分库,将表分布在不同的库中(库可能分布在不同的机器上),但是一个业务场景可能会同时处理两个表的操作.在这种场景下,事务的提交会变得相对复杂,因为多 ...
- springCloud分布式事务实战(一)案例需求及实现步骤
本文不对分布式事务原理进行探索,而是通过一个案例来说明如何使用分布式事务 案例需求:创建2个基于springCloud的微服务,分别访问不同的数据库:然后创建一个整合服务,调用微服务实现数据的保存到2 ...
- 分布式之分布式事务、分布式锁、接口幂等性、分布式session
一.分布式session session 是啥?浏览器有个 cookie,在一段时间内这个 cookie 都存在,然后每次发请求过来都带上一个特殊的 jsessionid cookie,就根据这个东西 ...
- texedo 分布式事务
1.问题现象 但是实际情况,完全出乎笔者的想法.检查一般对象数据表锁定,只需要检查v$locked_object和v$transaction视图,就可以定位到具体人.但是检查之后的结果如下: SQL& ...
- 消息服务框架(MSF)应用实例之分布式事务三阶段提交协议的实现
一,分布式事务简介 在当前互联网,大数据和人工智能的热潮中,传统企业也受到这一潮流的冲击,纷纷响应国家“互联网+”的战略号召,企业开始将越来越多的应用从公司内网迁移到云端和移动端,或者将之前孤立的IT ...
- 分布式事务之深入理解什么是2PC、3PC及TCC协议?
导读 在上一篇文章<[分布式事务]基于RocketMQ搭建生产级消息集群?>中给大家介绍了基于RocketMQ如何搭建生产级消息集群.因为本系列文章最终的目的是介绍基于RocketMQ的事 ...
- 即时消息服务框架(iMSF)应用实例之分布式事务三阶段提交协议的实现
一,分布式事务简介 在当前互联网,大数据和人工智能的热潮中,传统企业也受到这一潮流的冲击,纷纷响应国家“互联网+”的战略号召,企业开始将越来越多的应用从公司内网迁移到云端和移动端,或者将之前孤立的IT ...
- 微服务开发的最大痛点-分布式事务SEATA入门简介
前言 在微服务开发中,存在诸多的开发痛点,例如分布式事务.全链路跟踪.限流降级和服务平滑上下线等.而在这其中,分布式事务是最让开发者头痛的.那分布式事务是什么呢? 分布式事务就是指事务的参与者.支持事 ...
随机推荐
- 拦截器中,request中getReader()和getInputStream()只能调用一次,构建可重复读取inputStream的request.
由于 request中getReader()和getInputStream()只能调用一次 在项目中,可能会出现需要针对接口参数进行校验等问题. 因此,针对这问题,给出一下解决方案 实现方法:先将Re ...
- Spring Cloud Config教程(五)客户端使用
要在应用程序中使用这些功能,只需将其构建为依赖于spring-cloud-config-client的Spring引导应用程序(例如,查看配置客户端或示例应用程序的测试用例).添加依赖关系的最方便的方 ...
- 20道JS原理题助你面试一臂之力!(转)
20道JS原理题助你面试一臂之力! 前言 本文针对目前常见的面试题,仅提供了相应的核心原理及思路,部分边界细节未处理.后续会持续更新,希望对你有所帮助. 1. 实现一个call函数 // 思路:将要改 ...
- Java equals 和 hashCode 的这几个问题可以说明白吗?
前言 上一篇文章 如何妙用 Spring 数据绑定? ,灵魂追问 环节留下了一个有关 equals 和 hashcode 问题 .基础面试经常会碰到与之相关的问题,这不是一个复杂的问题,但很多朋友都苦 ...
- spring cloud:feign-hystrix
producer 1. File-->new spring starter project 2.add dependency <dependency> <group ...
- Gridview中显示的值根据数据库中带出的值作更改
前台页面对Gridview增加事件 OnRowDataBound="GridView1_RowDataBound"protected void GridView1_RowDataB ...
- modern php笔记---php (性状)
modern php笔记---php (性状) 一.总结 一句话总结: trait是和继承一个层次的东西 一个类use MyTrait;后,trait中的方法覆盖父类方法,当前类中的方法覆盖trait ...
- LeetCode_1114.按顺序打印(多线程)
LeetCode_1114 LeetCode-1114.按顺序打印 我们提供了一个类: public class Foo { public void one() { print("one&q ...
- spring boot 和 mybatis集成
1.pom.xml 添加mybatis和mysql依赖 <!-- 添加 MyBatis --> <dependency> <groupId>org.mybatis. ...
- tomcat的work目录的作用
最近发现,很多网友喜欢把tomcat的work目录里的东西叫做缓存,其实那不是很恰当,work目录只是tomcat的工作目录,也就是tomcat把jsp转换为class文件的工作目录,这也正是为什么它 ...