前言:
以下以网上课程购买流程举一个例子:
如何实现两个分布式服务(订单服务、学习服务)共同完成一件事即订单支付成功自动添加学生选课的需求,
这里的关键是如何保证两个分布式服务的事务的一致性。
订单支付结果通知方法{ 1)更新支付表中支付状态为成功。 2)远程调用选课接口添加记录(Feign调用) }
尝试解决上边的需求,在订单服务中远程调用选课接口,伪代码如下:
1.更新支付表状态为本地数据库操作。 2.远程调用选课接口为网络远程调用请求 3.为保存事务上边两步操作由spring控制事务,当遇到Exception异常则回滚本地数据库操作。 问题如下: 1、如果更新支付表失败则抛出异常,不再执行远程调用,此设想没有问题。 2、如果更新支付表成功,网络远程调用超时会拉长本地数据库事务时间,影响数据库性能。
(远程调用非常耗时的哦) 3、如果更新支付表成功,远程调用添加选课成功(选课数据库commit成功),
最后更新支付表commit失败,此时出现操作不一致。 上面的问题就涉及到了分布式事务的控制
一、什么是分布式事务
(一)分布式系统
部署在不同结点上的系统通过网络交互来完成协同工作的系统
比如:充值加积分的业务,用户在充值系统向自己的账户充钱,在积分系统中自己积分相应的增加。
充值系统和积分系统是两个不同的系统,一次充值加积分的业务就需要这两个系统协同工作来完成。
(二)、什么是事务
事务是指由一组操作组成的一个工作单元,这个工作单元具有原子性(atomicity)、
一致性(consistency)、隔离性(isolation)和持久性(durability)。
原子性:执行单元中的操作要么全部执行成功,要么全部失败。如果有一部分成功
一部分失败那么成功的操作要全部回滚到执行前的状态。
一致性:执行一次事务会使用数据从一个正确的状态转换到另一个正确的状态,执行前后数据都是完整的。
隔离性:在该事务执行的过程中,任何数据的改变只存在于该事务之中,对外界没有影响,
事务与事务之间是完全的隔离的。只有事务提交后其它事务才可以查询到最新的数据。
持久性:事务完成后对数据的改变会永久性的存储起来,即使发生断电宕机数据依然在。
(三)什么是本地事务
本地事务就是用关系数据库来控制事务,关系数据库通常都具有ACID特性,
传统的单体应用通常会将数据全部存储在一个数据库中,会借助关系数据库来完成事务控制。
(四)、什么是分布式事务
在分布式系统中一次操作由多个系统协同完成,这种一次事务操作涉及多个系统
通过网络协同完成的过程称为分布式事务。这里强调的是多个系统通过网络协同完成一个事务的过程,
并不强调多个系统访问了不同的数据库,即使多个系统访问的是同一个数据库也是

另外一种分布式事务的表现是,一个应用程序使用了多个数据源连接了不同的数据库,
当一次事务需要操作多个数据源,此时也属

(五)、分布式事务的应用场景

(六)、分布式事务解决方案
从CAP理论引出了很多的解决方案:
1、两阶段提交协议(2PC):
2PC的优点:实现强一致性,部分关系数据库支持(Oracle、MySQL等)。
缺点:整个事务的执行需要由协调者在多个节点之间去协调,增加了事务的执行时间,性能低下。
解决方案有:springboot+Atomikos or Bitronix
2.事务补偿(TCC):try、Confirm、Cancel
1、Try 检查及预留业务资源完成提交事务前的检查,并预留好资源。 2、Confirm 确定执行业务操作 对try阶段预留的资源正式执行。 3、Cancel 取消执行业务操作 对try阶段预留的资源释放。
1、Try 下单业务由订单服务和库存服务协同完成,在try阶段订单服务和库存服务完成检查和预留资源。 订单服务检查当前是否满足提交订单的条件(比如:当前存在未完成订单的不允许提交新订单)。 库存服务检查当前是否有充足的库存,并锁定资源。 2、Confirm 订单服务和库存服务成功完成Try后开始正式执行资源操作。 订单服务向订单写一条订单信息。 库存服务减去库存。 3、Cancel 如果订单服务和库存服务有一方出现失败则全部取消操作。 订单服务需要删除新增的订单信息。 库存服务将减去的库存再还原。 优点:最终保证数据的一致性,在业务层实现事务控制,灵活性好。 缺点:开发成本高,每个事务操作每个参与者都需要实现try/confirm/cancel三个接口。 注意:TCC的try/confirm/cancel接口都要实现幂等性,在为在try、confirm、cancel失败后要不断重试。
什么是幂等性?
幂等性是指同一个操作无论请求多少次,其结果都相同。
幂等操作实现方式有:
1、操作之前在业务方法进行判断如果执行过了就不再执行。
2、缓存所有请求和处理的结果,已经处理的请求则直接返回结果。
3、在数据库表中加一个状态字段(未处理,已处理),数据操作时判断未处理时再处理。
3.消息队列实现最终一致性(本案例介绍)
优点 :
由MQ按异步的方式协调完成事务,性能较高。
不用实现try/confirm/cancel接口,开发成本比TCC低。
缺点:
此方式基于关系数据库本地事务来实现,会出现频繁读写数据库记录,
浪费数据库资源,另外对于高并发操作不是最佳方案
二、案例(消息队列实现最终一致性)
该案例模拟用户订单服务里面下单,然后可以学习该课程,具体流程如下:
流程:
1.订单下之后,将准备发送的消息存到任务表里
2.定时任务扫描消息表(任务表),
3.消费者接收到消息之后,先去历史表查询是否该消息已经处理,没有处理,则添加该选课
,选过了直接返回(利用线程池发送,try,有异常打印日志即可)
4.接收到添加课程成功的消息,则将对应的任务清除。
(一)、创建表
/* 任务表 */; DROP TABLE IF EXISTS `kc_task`; CREATE TABLE `kc_task` ( `id` varchar(32) NOT NULL COMMENT '主键', `yid` varchar(32) NOT NULL COMMENT '业务ID', `create_time` datetime NULL COMMENT '创建时间', `update_time` datetime NULL COMMENT '更新时间', `delete_time` datetime NULL COMMENT '删除时间', `task_type` varchar(32) NULL COMMENT '任务类型', `mq_exchange` varchar(64) NOT NULL COMMENT '交换机名称', `mq_routingkey` varchar(64) NOT NULL COMMENT '路由key', `request_body` varchar(512) NOT NULL COMMENT '任务请求的内容', `version` int(10) NULL COMMENT '乐观锁版本号', `status` varchar(32) NULL COMMENT '任务状态', `errormsg` varchar(512) NULL COMMENT '任务错误消息', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 任务历史表 */; DROP TABLE IF EXISTS `kc_task_his`; CREATE TABLE `kc_task_his` ( `id` varchar(32) NOT NULL COMMENT '主键', `yid` varchar(32) NOT NULL COMMENT '业务ID', `create_time` datetime NULL COMMENT '创建时间', `update_time` datetime NULL COMMENT '更新时间', `delete_time` datetime NULL COMMENT '删除时间', `task_type` varchar(32) NULL COMMENT '任务类型', `mq_exchange` varchar(64) NOT NULL COMMENT '交换机名称', `mq_routingkey` varchar(64) NOT NULL COMMENT '路由key', `request_body` varchar(512) NOT NULL COMMENT '任务请求的内容', `version` int(10) NULL COMMENT '乐观锁版本号', `status` varchar(32) NULL COMMENT '任务状态', `errormsg` varchar(512) NULL COMMENT '任务错误消息', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 学习课程历史表:可以防止多次消费的作用 */; DROP TABLE IF EXISTS `kc_learn_course_his`; CREATE TABLE `kc_learn_course_his` ( `id` varchar(32) NOT NULL COMMENT '主键', `yid` varchar(32) NOT NULL COMMENT '业务ID', `create_time` datetime NULL COMMENT '创建时间', `update_time` datetime NULL COMMENT '更新时间', `delete_time` datetime NULL COMMENT '删除时间', `task_type` varchar(32) NULL COMMENT '任务类型', `mq_exchange` varchar(64) NOT NULL COMMENT '交换机名称', `mq_routingkey` varchar(64) NOT NULL COMMENT '路由key', `request_body` varchar(512) NOT NULL COMMENT '任务请求的内容', `version` int(10) NULL COMMENT '乐观锁版本号', `status` varchar(32) NULL COMMENT '任务状态', `errormsg` varchar(512) NULL COMMENT '任务错误消息', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /* 添加课程任务的表 */; DROP TABLE IF EXISTS `kc_learn_course`; CREATE TABLE `kc_learn_course` ( `id` varchar(32) NOT NULL COMMENT '主键', `course_id` varchar(32) NOT NULL COMMENT '业务ID:课程ID', `user_id` varchar(32) NOT NULL COMMENT '用户ID', `charge` varchar(32) NULL COMMENT '收费规则', `price` float(8,2) NULL COMMENT '课程价格', `valid` varchar(32) NULL COMMENT '有效性', `start_time` datetime NULL COMMENT '学习开始时间', `end_time` datetime NULL COMMENT '学习结束时间', `status` varchar(32) NULL COMMENT '选课状态', `errormsg` varchar(512) NULL COMMENT '选课错误消息', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(二)、定时器的使用
三种完成方法
1.java自带的API java.util.Timer类 java.util.TimerTask类
2.Quartz框架 开源 功能强大 使用起来稍显复杂
3.Spring 3.0以后自带了task 调度工具,比Quartz更加的简单方便
cron表达式


@Scheduled所支持的参数:
1. cron:cron表达式,指定任务在特定时间执行;
2. fixedDelay:表示上一次任务执行完成后多久再次执行,参数类型为long,单位ms;
3. fixedDelayString:与fixedDelay含义一样,只是参数类型变为String;
4. fixedRate:表示按一定的频率执行任务,参数类型为long,单位ms;
5. fixedRateString: 与fixedRate的含义一样,只是将参数类型变为String;
6. initialDelay:表示延迟多久再第一次执行任务,参数类型为long,单位ms;
7. initialDelayString:与initialDelay的含义一样,只是将参数类型变为String;
8. zone:时区,默认为当前时区,一般没有用到。
springTask单线程的使用方法
1.在启动类上加上注解@EnableScheduling
2.在配置类上加注解@Component,对应的方法上加注解 @Scheduled(fixedDelay = 2000)
//2s执行一次 @Scheduled(fixedDelay = 2000) public void sendChooseCourse(){ System.out.println("sendChooseCourse time: "+System.currentTimeMillis()); System.out.println("定时任务:线程名称: "+Thread.currentThread().getName()); }
缺点:实际开发开发一般可能会涉及到多个定时任务,不希望所有的任务都运行在一个线程中,想要改成多线程,
SpringTask提供一个多线程的TaskScheduler,Spring已经有默认实现
案例
1.配置定时任务线程池
package com.lanpo.fenbushiproduct.config; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.annotation.AsyncConfigurer; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.annotation.SchedulingConfigurer; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.scheduling.config.ScheduledTaskRegistrar; import java.util.concurrent.Executor; /* 1.@EnableScheduling加了该注解启动类上不用加 Spring 中,创建定时任务除了使用@Scheduled 注解外,还可以使用 SchedulingConfigurer。 2.@Schedule 注解有一个缺点,其定时的时间不能动态的改变,而基于 SchedulingConfigurer 接口的方式可以做到。SchedulingConfigurer 接口可以实现在@Configuration 类上,同时不要忘了, 还需要@EnableScheduling 注解的支持。 3.目的是: 通过实现AsyncConfigurer自定义线程池,包含异常处理 实现AsyncConfigurer接口对异常线程池更加细粒度的控制 *a) 创建线程自己的线程池 b) 对void方法抛出的异常处理的类AsyncUncaughtExceptionHandler */ @Configuration @EnableScheduling public class AsyncTaskThreadPool implements SchedulingConfigurer , AsyncConfigurer { private int poolsize=10; @Override public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } @Bean public ThreadPoolTaskScheduler getAsyncTaskThread(){ ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); taskScheduler.initialize(); taskScheduler.setPoolSize(poolsize); //线程名称前缀 taskScheduler.setThreadNamePrefix("mythread-po-task"); return taskScheduler; } /* SchedulingConfigurer 使用的也是单线程的方式, getAsyncTaskThread()方法得到的是springTask提供一个多线程的TaskScheduler */ @Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setTaskScheduler(getAsyncTaskThread()); } @Override public Executor getAsyncExecutor() { Executor executor= getAsyncTaskThread(); return executor; } }
2.定时任务异步
package com.lanpo.fenbushiproduct.config; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class TaskLeranSchedual { //2s执行一次 @Scheduled(fixedDelay = 2000) public void sendChooseCourse(){ System.out.println("sendChooseCourse time: "+System.currentTimeMillis()); System.out.println("定时任务异步:线程名称: "+Thread.currentThread().getName()); } //2s执行一次 @Scheduled(fixedDelay = 2000) public void sendChooseCourse2(){ System.out.println("sendChooseCourse2 time: "+System.currentTimeMillis()); System.out.println("定时任务异步:线程名称: "+Thread.currentThread().getName()); } }
结果:
sendChooseCourse2 time: 1588562646855 定时任务异步:线程名称: mythread-po-task4 sendChooseCourse time: 1588562646855 定时任务异步:线程名称: mythread-po-task3
补充:ThreadPoolTaskExecutor 也是不错的定时任务线程池
(三)、订单下单到定时器扫描
1.下单接口

2.课程订单下单然后定时器扫描(之后以courseId作为路由key)
package com.lanpo.fenbushiproduct.service; import com.alibaba.fastjson.JSONArray; import com.lanpo.fenbushiproduct.Dao.KcTaskMapper; import com.lanpo.fenbushiproduct.config.KcTaskRabbitMq; import com.lanpo.fenbushiproduct.pojo.KcLearnCourse; import com.lanpo.fenbushiproduct.pojo.KcTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; import java.util.UUID; @Service public class KcTaskService { private static final Logger Log = LoggerFactory.getLogger(KcTaskService.class); @Autowired private KcTaskMapper kcTaskMapper; //客户端下单业务 @Transactional(rollbackFor = Exception.class) public void addKcTask(KcLearnCourse kcLearnCourse) { try {//默认只有一个用户 KcTask kcTasks = kcTaskMapper.findByYid(kcLearnCourse.getCourseId()); if (kcTasks != null) { //更新课程信息的 } else { KcTask kcTask = new KcTask(); kcTask.setId(UUID.randomUUID().toString()); //业务yid和key都是courseId kcTask.setYid(kcLearnCourse.getCourseId()); kcTask.setCreateTime(new Date()); kcTask.setMqExchange(KcTaskRabbitMq.TASK_TRAN_CHANGE); kcTask.setMqRoutingkey(kcLearnCourse.getCourseId()); kcTask.setRequestBody(JSONArray.toJSON(kcLearnCourse).toString()); kcTask.setStatus("准备发送"); kcTask.setTaskType("课程"); kcTaskMapper.addKcTask(kcTask); Log.info("添加任务课程成功"); } } catch (Exception e) { //响应给前端 Log.info("添加任务课程失败"); //使try里面业务回滚 throw new RuntimeException(); } } //定时任务扫描全部 public List<KcTask> findKcTaskAll() { return kcTaskMapper.findAll(); } }
3.课程下单实体
public class KcLearnCourse implements Serializable { private static final long serialVersionUID = -916357110051689423L; private String id; private String courseId; private String userId; private String charge;//收费规则 private String price; private String valid;//有效性 private String startTime; private String endTime; private String status; private String errormsg; //省略get和set的方法
3.配置订单服务的MQ
1.fenbushiProduct服务(注意:路由和交换机最好和别的不一样,特别是路由配置)
package com.lanpo.fenbushiproduct.config; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class KcTaskRabbitMq { //队列和交换机 public static final String TASK_TRAN_QUEUE="TASK_TRAN_QUEUE"; public static final String TASK_TRAN_CHANGE="TASK_TRAN_CHANGE"; public static final String ROUTING_KEY = "yid.#";//业务ID @Bean(TASK_TRAN_CHANGE)//给每一个bean命名可以在本来配置多个 public Exchange EXCHAGE_TOPIC_TRAN(){ //durable持久化,这是交换机名称持久化 return ExchangeBuilder.topicExchange(TASK_TRAN_CHANGE).durable(true).build(); } //声明队列 @Bean(TASK_TRAN_QUEUE)//给每一个bean命名可以在本来配置多个 public Queue TRAN_TOPIC_QUEUE(){ return new Queue(TASK_TRAN_QUEUE); } //绑定交换机和队列 @Bean public Binding BINDING_TASK_CHANGE_AND_QUEUE(@Qualifier(TASK_TRAN_QUEUE) Queue queue ,@Qualifier(TASK_TRAN_CHANGE) Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY).noargs(); } }
4.
package com.lanpo.fenbushiproduct.config; import com.lanpo.fenbushiproduct.pojo.KcTask; import com.lanpo.fenbushiproduct.service.KcTaskService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import java.util.List; import java.util.UUID; @Component public class TaskLeranSchedual { private static final Logger Log = LoggerFactory.getLogger(TaskLeranSchedual.class); @Autowired private KcTaskService kcTaskService; //默认使用了confirm模式 @Autowired private RabbitTemplate rabbitTemplate; public static final String KC_TASK_ROUTING_KEY = "yid."; //2s执行一次 @Scheduled(fixedDelay = 60000) public void sendChooseCourse() { //扫描任务表,此处暂时扫全表,可以业务需要灵活处理 List<KcTask> kcTaskAll = kcTaskService.findKcTaskAll(); if (kcTaskAll != null) { for (KcTask kcTask : kcTaskAll) { try {//真正的key格式=【yid.a8278a4b-3cc4-467c-9f47-7140a787c7ea】 CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); rabbitTemplate.convertAndSend(kcTask.getMqExchange(), KC_TASK_ROUTING_KEY + kcTask.getYid() , kcTask.getRequestBody(), correlationData); Log.info("【课程任务定时器】准备发送的业务yid:" + kcTask.getYid()); } catch (Exception e) { Log.info("课程任务发生失败,业务ID是:" + kcTask.getYid()); } } } } }
5.yml配置
server: port: 8082 spring: application: name: product-service rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest virtual-host: / publisher-confirms: true publisher-returns: true listener: simple: acknowledge-mode: manual datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/transaction?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC password: 152443 username: root eureka: client: service-url: defaultZone: http://localhost:8081/eureka/ mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lanpo.fenbushiproduct.pojo
6.pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
(四)、消费者(fenbushiConsumer)
1.监听队列
package com.lanpo.fenbushiconsumer.config; import com.alibaba.fastjson.JSON; import com.lanpo.fenbushiconsumer.pojo.KcLearnCourse; import com.lanpo.fenbushiconsumer.pojo.KcLearnCourseHis; import com.lanpo.fenbushiconsumer.service.AddCouurseAndHisService; import com.lanpo.fenbushiconsumer.service.KcLearnCourseHisService; import com.lanpo.fenbushiconsumer.service.ReturnKcTaskMessageService; import com.rabbitmq.client.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; import java.math.BigDecimal; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; import java.util.UUID; @Component public class ReceviceKcTask { private static final Logger Log = LoggerFactory.getLogger(ReceviceKcTask.class); @Autowired KcLearnCourseHisService kcLearnCourseHisService; @Autowired AddCouurseAndHisService addCouurseAndHisService; @Autowired ReturnKcTaskMessageService returnKcTaskMessageService; //监听某一个队列 @RabbitListener(queues = "TASK_TRAN_QUEUE") public void getTaskMessage(String kctask,Message message, Channel channel) throws IOException { String yid =""; try { String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey(); yid = receivedRoutingKey.substring(4); KcLearnCourseHis kcLearnCourseHis = kcLearnCourseHisService.findById(yid); SimpleDateFormat sformat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); if(kcLearnCourseHis==null){//等于null添加到学习表,这里可以防止重复提交 //封装KcLearnCourse KcLearnCourse kcLearnCourse = new KcLearnCourse(); Map map = JSON.parseObject(kctask, Map.class); kcLearnCourse.setId(UUID.randomUUID().toString()); kcLearnCourse.setCharge((String) map.get("charge")); kcLearnCourse.setStartTime( sformat.parse((String) map.get("startTime"))); kcLearnCourse.setEndTime(sformat.parse((String) map.get("endTime"))); kcLearnCourse.setPrice(new BigDecimal((String) map.get("price"))); kcLearnCourse.setCourseId((String) map.get("courseId")); kcLearnCourse.setUserId((String) map.get("userId")); kcLearnCourse.setStatus((String) map.get("status")); kcLearnCourse.setValid((String) map.get("valid")); //封装KcLearnCourseHis KcLearnCourseHis kcLearnCourseHis1 = new KcLearnCourseHis(); kcLearnCourseHis1.setId(UUID.randomUUID().toString()); kcLearnCourseHis1.setYid(yid); kcLearnCourseHis1.setMqRoutingkey(yid); kcLearnCourseHis1.setCreateTime(new Date()); //课程表和历史表被本地事务管理 addCouurseAndHisService.addCouurseAndHis(kcLearnCourse,kcLearnCourseHis1); Log.info("将订单加到学习表成功 yid(courseId) = " +yid +" 准备给订单服务发送消息。。。。。。"); }else { Log.info("该订单已经处理过 yid(courseId) = " +yid +" 准备给订单服务发送消息。。。。。。"); } //告诉订单服务学习表添加成功 returnKcTaskMessageService.sendReturnKcTaskMessage2(yid,message); //确认收到消息,确认当前消费者的一个消息收到 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { if (message.getMessageProperties().getRedelivered()) { Log.info("【课程任务消费者】的该消息被拒绝过,不再放回到队列:{}", message); //被拒绝一次之后,不再放到队列中 Log.info("该消息yid在此处应该保存到日志中待开发人员手动处理,本案例不再扩展添加到日志的业务"); channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } else { Log.info("【课程任务消费者】的该消息第一次被拒绝,再放回到队列:{}", message); channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } e.printStackTrace(); } } }
2.课程学习历史表
public class KcLearnCourseHis implements Serializable { private static final long serialVersionUID = -916357110051689486L; private String id; private String yid; private Date createTime; private Date updateTime; private Date deleteTime; private String taskType; private String mqExchange; private String mqRoutingkey; private String requestBody; private Integer version; private String status; private String errormsg; //省略get和set
3.课程学习实体
public class KcLearnCourse implements Serializable { private String id; private String courseId; private String userId; private String charge; private BigDecimal price; private String valid; private Date startTime; private Date endTime; private String status; private String errormsg; //省略get和set
4.关键:保证学习表和历史表一致性
package com.lanpo.fenbushiconsumer.service; import com.lanpo.fenbushiconsumer.pojo.KcLearnCourse; import com.lanpo.fenbushiconsumer.pojo.KcLearnCourseHis; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class AddCouurseAndHisService { @Autowired KcLearnCourseHisService kcLearnCourseHisService; @Autowired KcLearnCourseService kcLearnCourseService; //消费端课程学习和历史表开启本地事务 @Transactional(rollbackFor = Exception.class) public void addCouurseAndHis(KcLearnCourse kcLearnCourse, KcLearnCourseHis kcLearnCourseHis){ kcLearnCourseService.addKClearnCourse(kcLearnCourse); kcLearnCourseHisService.addKcLearnCourseHis(kcLearnCourseHis); } }
(五)、给订单服务回复消息
1.发送消息也是confirm模式,配置和订单服务配置一致,返回的消息MQ配置如下
package com.lanpo.fenbushiconsumer.config; import org.springframework.amqp.core.*; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ConsumerKcTaskRabbitMq { //队列和交换机 public static final String CONSUMER_TASK_TRAN_QUEUE="CONSUMER_TASK_TRAN_QUEUE"; public static final String CONSUMER_TASK_TRAN_CHANGE="CONSUMER_TASK_TRAN_CHANGE"; public static final String ROUTING_KEY = "backYid.#";//业务ID @Bean(CONSUMER_TASK_TRAN_CHANGE)//给每一个bean命名可以在本来配置多个 public Exchange EXCHAGE_TOPIC_TRAN(){ //durable持久化,这是交换机名称持久化 return ExchangeBuilder.topicExchange(CONSUMER_TASK_TRAN_CHANGE).durable(true).build(); } //声明队列 @Bean(CONSUMER_TASK_TRAN_QUEUE)//给每一个bean命名可以在本来配置多个 public Queue TRAN_TOPIC_QUEUE(){ return new Queue(CONSUMER_TASK_TRAN_QUEUE); } //绑定交换机和队列 @Bean public Binding BINDING_CONSUMER_TASK_CHANGE_AND_QUEUE(@Qualifier(CONSUMER_TASK_TRAN_QUEUE) Queue queue ,@Qualifier(CONSUMER_TASK_TRAN_CHANGE) Exchange exchange){ return BindingBuilder.bind(queue).to(exchange).with(ROUTING_KEY).noargs(); } }
2.给订单服务回消息
package com.lanpo.fenbushiconsumer.service; import com.lanpo.fenbushiconsumer.config.ConsumerKcTaskRabbitMq; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.connection.CorrelationData; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.UUID; import java.util.concurrent.ExecutorService; @Service public class ReturnKcTaskMessageService { private static final Logger Log = LoggerFactory.getLogger(MyThreadTask.class); public static final String ROUTING_KEY = "backYid.";//业务ID @Autowired RabbitTemplate rabbitTemplate;public void sendReturnKcTaskMessage2(String yid, Message message){ CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString()); try { rabbitTemplate.convertAndSend(ConsumerKcTaskRabbitMq.CONSUMER_TASK_TRAN_CHANGE, ROUTING_KEY+yid, "ok", correlationData); Log.info("从课程任务消费端发送到订单服务成功: yid是:" + yid); }catch (Exception e) { Log.info("从课程任务消费端发送到订单服务失败: {}:" ,message); } } }
3.yml配置(pom.xml和订单服务一样)
server: port: 8083 spring: application: name: consumer1-service rabbitmq: host: 127.0.0.1 port: 5672 username: guest password: guest virtual-host: / #手动回复ack listener: simple: acknowledge-mode: manual publisher-confirms: true publisher-returns: true datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/transaction?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC password: 152443 username: root eureka: client: service-url: defaultZone: http://localhost:8081/eureka/ mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.lanpo.fenbushiconsumer.pojo
(六)、订单服务监听发回的消息
package com.lanpo.fenbushiproduct.config; import com.rabbitmq.client.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class ReceviceKcLearnCourseId { private static final Logger Log = LoggerFactory.getLogger(ReceviceKcLearnCourseId.class); //监听某一个队列 @RabbitListener(queues = "CONSUMER_TASK_TRAN_QUEUE") public void getTaskMessage(String kctask,Message message, Channel channel) throws IOException { try{ String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey(); System.out.println("从学习服务收到的yid================================== "+receivedRoutingKey); //确认收到消息,确认当前消费者的一个消息收到 channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { if (message.getMessageProperties().getRedelivered()) { Log.info("【从学习服务收到消息】的该消息被拒绝过,不再放回到队列:{}", message); //被拒绝一次之后,不再放到队列中 channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } else { Log.info("【从学习服务收到消息】的该消息第一次被拒绝,再放回到队列:{}", message); channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } e.printStackTrace(); } } }
(七)、测试
1.任务表有两条订单课程,定时器每分钟扫一次

2.为了看出效果,先启动订单服务

3.消费端学习服务收到的消息,并给订单服务发回消息()

4.发回的消息队列

5.从学习服务发回的消息courseId

(八)、我们应该把返回的courseId从任务学习表删除。
package com.lanpo.fenbushiproduct.config; import com.lanpo.fenbushiproduct.service.KcTaskService; import com.rabbitmq.client.Channel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.io.IOException; @Component public class ReceviceKcLearnCourseId { @Autowired KcTaskService kcTaskService; private static final Logger Log = LoggerFactory.getLogger(ReceviceKcLearnCourseId.class); //监听某一个队列 @RabbitListener(queues = "CONSUMER_TASK_TRAN_QUEUE") public void getTaskMessage(String kctask,Message message, Channel channel) throws IOException { try{ //backYid.6a8236c0-961-4823-9242-afdas3c8c261 String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey(); String yid = receivedRoutingKey.substring(8); kcTaskService.deleteByYid(yid); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); } catch (Exception e) { if (message.getMessageProperties().getRedelivered()) { Log.info("【从学习服务收到消息】删除失败被拒绝过,不再放回到队列:{}", message); //被拒绝一次之后,不再放到队列中 channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); } else { Log.info("【从学习服务收到消息】第一次删除失败,再放回到队列:{}", message); channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true); } e.printStackTrace(); } } }
总结: 从业务上来看还是有很多不完善的。一使用@RabbitListener注解指定消费方法,
默认情况是单线程监听队列,可以观察当队列有多个任务时消费端每次只消费一个消息,
单线程处理消息容易引起消息处理缓慢,消息堆积,不能最大利用硬件资源
可以配置mq的容器工厂参数,增加并发处理数量即可实现多线程处理监听队列,
实现多线程处理消息。
浙公网安备 33010602011771号