RabbitMQ
一、引言
模块之间的耦合度多高,导致一个模块宕机后,全部功能都不能用了,并且同步通讯的成本过高,用户体验差。
RabbitMQ引言 |
---|
二、RabbitMQ介绍
市面上比较火爆的几款MQ:
ActiveMQ,RocketMQ,Kafka,RabbitMQ。
语言的支持:ActiveMQ,RocketMQ只支持Java语言,Kafka可以支持多们语言,RabbitMQ支持多种语言。
效率方面:ActiveMQ,RocketMQ,Kafka效率都是毫秒级别,RabbitMQ是微秒级别的。
消息丢失,消息重复问题: RabbitMQ针对消息的持久化,和重复问题都有比较成熟的解决方案。
学习成本:RabbitMQ非常简单。
RabbitMQ是由Rabbit公司去研发和维护的,最终是在Pivotal。
RabbitMQ严格的遵循AMQP协议,高级消息队列协议,帮助我们在进程之间传递异步消息。
三、RabbitMQ安装
windows上的安装: http://note.youdao.com/noteshare?id=179f27cd4eb0732db4665c5e14e53811&sub=76F523218CED41EB8A6B3630A9440A81 docker安装: version: "3.1" services: rabbitmq: image: daocloud.io/library/rabbitmq:management restart: always container_name: rabbitmq ports: - 5672:5672 - 15672:15672 volumes: - ./data:/var/lib/rabbitmq
四、RabbitMQ架构【重点
】
4.1 官方的简单架构图
Publisher - 生产者:发布消息到RabbitMQ中的Exchange
Consumer - 消费者:监听RabbitMQ中的Queue中的消息
Exchange - 交换机:和生产者建立连接并接收生产者的消息
Queue - 队列:Exchange会将消息分发到指定的Queue,Queue和消费者进行交互
Routes - 路由:交换机以什么样的策略将消息发布到Queue
简单架构图 |
---|
1、RabbitMQ消息投递有几种模式(type有哪几种)。
2、如何保证消息不丢失(三个方面说明)。
3、如何保证消息不重复消费。
4、在项目中具体应用。
4.2 RabbitMQ的完整架构图
完整架构图
完整架构图 |
---|
4.3 查看图形化界面并创建一个Virtual Host
创建一个全新的用户和全新的Virtual Host,并且将test用户设置上可以操作/test的权限
监控界面 |
---|
五、RabbitMQ的使用【重点
】
5.1 RabbitMQ的通讯方式
通讯方式 |
---|
5.2 Java连接RabbitMQ
5.2.1 创建maven项目
…………
5.2.2 导入依赖
<dependencies> <dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.6.0</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> </dependency> </dependencies>
5.2.3 创建工具类连接RabbitMQ
public class MQConnection { public static Connection getConnection(){ // 创建Connection工厂 ConnectionFactory factory = new ConnectionFactory(); factory.setHost("localhost"); factory.setPort(5672); factory.setUsername("guest"); factory.setPassword("guest"); factory.setVirtualHost("/"); // 创建Connection Connection conn = null; try { conn = factory.newConnection(); } catch (Exception e) { e.printStackTrace(); } // 返回 return conn; } }
效果图 |
---|
5.3 Hello-World
一个生产者,一个默认的交换机,一个队列,一个消费者
结构图 |
---|
创建生产者,创建一个channel,发布消息到exchange,指定路由规则。
@Test public void publish() throws Exception { //1. 获取Connection Connection connection = MQConnection.getConnection(); //2. 创建Channel Channel channel = connection.createChannel(); //3. 发布消息到exchange,同时指定路由的规则 String msg = "Hello-World!"; // 参数1:指定exchange,使用""。 // 参数2:指定路由的规则,使用具体的队列名称。 // 参数3:指定传递的消息所携带的properties,使用null。 // 参数4:指定发布的具体消息,byte[]类型 channel.basicPublish("","HelloWorld",null,msg.getBytes()); // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功!"); //4. 释放资源 channel.close(); connection.close(); }
创建消费者,创建一个channel,创建一个队列,并且去消费当前队列
@Test public void consume() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 channel.queueDeclare("HelloWorld",true,false,false,null); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("HelloWorld",true,consume); System.out.println("消费者开始监听队列!"); // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); }
5.4 Work
一个生产者,一个默认的交换机,一个队列,两个消费者
结构图 |
---|
只需要在消费者端,添加Qos能力以及更改为手动ack即可让消费者,根据自己的能力去消费指定的消息,而不是默认情况下由RabbitMQ平均分配了,生产者不变,正常发布消息到默认的exchange,并指定routing
消费者指定Qoa和手动ack
//1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); DefaultConsumer consumer = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费者1号接收到消息:" + new String(body,"UTF-8")); //2. 手动ack channel.basicAck(envelope.getDeliveryTag(),false); } }; //3. 指定手动ack channel.basicConsume("Work",false,consumer);
完整代码:
public class MQTest { @Test public void test(){ final Connection connection = MQConnection.getConnection(); System.out.println(connection); try { System.in.read(); } catch (IOException e) { e.printStackTrace(); } } /** * Hello-world * 发布一个简单的消息直接到队列中 * @throws Exception */ @Test public void publish() throws Exception { //1. 获取Connection Connection connection = MQConnection.getConnection(); //2. 创建Channel Channel channel = connection.createChannel(); for (int i = 0; i < 10; i++) { //3. 发布消息到exchange,同时指定路由的规则 String msg = "Hello-World!===" + i; // 参数1:指定exchange,使用""。 // 参数2:指定路由的规则,使用具体的队列名称。 // 参数3:指定传递的消息所携带的properties,使用null。 // 参数4:指定发布的具体消息,byte[]类型 // 注意,在发布消息之前应该创建相应的队列 channel.basicPublish("","HelloWorld",null,msg.getBytes()); } // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功!"); //4. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("HelloWorld",true,false,false,null); //1 指定当前消费者,一次消费多少个消息 channel.basicQos(1); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("接收到消息:" + new String(body,"UTF-8")); //2. 手动ack channel.basicAck(envelope.getDeliveryTag(),false); } }; //3. 指定手动ack channel.basicConsume("HelloWorld",false,consume); //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 // channel.basicConsume("HelloWorld",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } }
5.5 Publish/Subscribe
一个生产者,一个交换机,两个队列,两个消费者
结构图 |
---|
声明一个Fanout类型的exchange,并且将exchange和queue绑定在一起,绑定的方式就是直接绑定。
让生产者创建一个exchange并且指定类型,和一个或多个队列绑定到一起。
//3. 创建exchange - 绑定某一个队列 //参数1: exchange的名称 //参数2: 指定exchange的类型 FANOUT - pubsub , DIRECT - Routing , TOPIC - Topics channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT); channel.queueBind("pubsub-queue1","pubsub-exchange",""); channel.queueBind("pubsub-queue2","pubsub-exchange","");
消费者还是正常的监听某一个队列即可。
完整代码:
public class MQFanoutTest { /** * Hello-world * 发布一个简单的消息直接到队列中 * @throws Exception */ @Test public void publish() throws Exception { //1. 获取Connection Connection connection = MQConnection.getConnection(); //2. 创建Channel Channel channel = connection.createChannel(); for (int i = 0; i < 10; i++) { //3. 发布消息到exchange,同时指定路由的规则 String msg = "Hello-World!===" + i; // 参数1:指定exchange,使用""。 // 参数2:指定路由的规则,使用具体的队列名称。 // 参数3:指定传递的消息所携带的properties,使用null。 // 参数4:指定发布的具体消息,byte[]类型 // 注意,在发布消息之前应该创建相应的队列 channel.basicPublish("pubsub-exchange","",null,msg.getBytes()); } // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功!"); //4. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume1() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("pubsub-queue1",true,false,false,null); //3. 创建exchange - 绑定某一个队列 //参数1: exchange的名称 //参数2: 指定exchange的类型 FANOUT - pubsub , DIRECT - Routing , TOPIC - Topics channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT); channel.queueBind("pubsub-queue1","pubsub-exchange",""); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("pubsub-queue1",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume2() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("pubsub-queue2",true,false,false,null); //3. 创建exchange - 绑定某一个队列 //参数1: exchange的名称 //参数2: 指定exchange的类型 FANOUT - pubsub , DIRECT - Routing , TOPIC - Topics channel.exchangeDeclare("pubsub-exchange", BuiltinExchangeType.FANOUT); channel.queueBind("pubsub-queue2","pubsub-exchange",""); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("pubsub-queue2",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } }
5.6 Routing
一个生产者,一个交换机,两个队列,两个消费者
结构图 |
---|
生产者在创建DIRECT类型的exchange后,根据RoutingKey去绑定相应的队列,并且在发送消息时,指定消息的具体RoutingKey即可。
//3. 创建exchange, routing-queue-error,routing-queue-info, channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT); channel.queueBind("routing-queue-error","routing-exchange","ERROR"); channel.queueBind("routing-queue-info","routing-exchange","INFO"); //4. 发布消息到exchange,同时指定路由的规则 channel.basicPublish("routing-exchange","ERROR",null,"ERROR".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO1".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO2".getBytes()); channel.basicPublish("routing-exchange","INFO",null,"INFO3".getBytes());
消费者没有变化
完整代码:
public class MQDirectTest { /** * Hello-world * 发布一个简单的消息直接到队列中 * @throws Exception */ @Test public void publish() throws Exception { //1. 获取Connection Connection connection = MQConnection.getConnection(); //2. 创建Channel Channel channel = connection.createChannel(); for (int i = 0; i < 10; i++) { //3. 发布消息到exchange,同时指定路由的规则 String msg = "Hello-World!===" + i; // 参数1:指定exchange,使用""。 // 参数2:指定路由的规则,使用具体的队列名称。 // 参数3:指定传递的消息所携带的properties,使用null。 // 参数4:指定发布的具体消息,byte[]类型 // 注意,在发布消息之前应该创建相应的队列 channel.basicPublish("routing-exchange","zl",null,msg.getBytes()); } // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功!"); //4. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume1() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("routing-queue-junjie",true,false,false,null); //3. 创建exchange, routing-queue-error,routing-queue-info, channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT); channel.queueBind("routing-queue-junjie","routing-exchange","jj"); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("routing-queue-junjie",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume2() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("routing-queue-zulong",true,false,false,null); //3. 创建exchange, routing-queue-error,routing-queue-info, channel.exchangeDeclare("routing-exchange", BuiltinExchangeType.DIRECT); channel.queueBind("routing-queue-zulong","routing-exchange","zl"); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("routing-queue-zulong",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } }
5.7 Topic
一个生产者,一个交换机,两个队列,两个消费者
结构图 |
---|
生产者创建Topic的exchange并且绑定到队列中,这次绑定可以通过*和#关键字,对指定RoutingKey内容,编写时注意格式 xxx.xxx.xxx去编写, * -> 一个xxx,而# -> 代表多个xxx.xxx,在发送消息时,指定具体的RoutingKey到底是什么。
//2. 创建exchange并指定绑定方式 channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC); channel.queueBind("topic-queue-1","topic-exchange","*.red.*"); channel.queueBind("topic-queue-2","topic-exchange","fast.#"); channel.queueBind("topic-queue-2","topic-exchange","*.*.rabbit"); //3. 发布消息到exchange,同时指定路由的规则 channel.basicPublish("topic-exchange","fast.red.monkey",null,"红快猴子".getBytes()); channel.basicPublish("topic-exchange","slow.black.dog",null,"黑漫狗".getBytes()); channel.basicPublish("topic-exchange","fast.white.cat",null,"快白猫".getBytes());
消费者只是监听队列,没变化。
完整代码:
public class MQTopicTest { @Test public void publish() throws Exception { //1. 获取Connection Connection connection = MQConnection.getConnection(); //2. 创建Channel Channel channel = connection.createChannel(); for (int i = 0; i < 10; i++) { //3. 发布消息到exchange,同时指定路由的规则 String msg = "Hello-World!===" + i; // 参数1:指定exchange,使用""。 // 参数2:指定路由的规则,使用具体的队列名称。 // 参数3:指定传递的消息所携带的properties,使用null。 // 参数4:指定发布的具体消息,byte[]类型 // 注意,在发布消息之前应该创建相应的队列 channel.basicPublish("topic-exchange","zl",null,msg.getBytes()); } // Ps:exchange是不会帮你将消息持久化到本地的,Queue才会帮你持久化消息。 System.out.println("生产者发布消息成功!"); //4. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume1() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("topic-queue-jj",true,false,false,null); //2. 创建exchange并指定绑定方式 channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC); channel.queueBind("topic-queue-jj","topic-exchange","#.qt.#"); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("topic-queue-jj",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } /** * 消费者消费消息 * @throws Exception */ @Test public void consume2() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("topic-queue-zl",true,false,false,null); //2. 创建exchange并指定绑定方式 channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC); channel.queueBind("topic-queue-zl","topic-exchange","#.yf.#"); channel.queueBind("topic-queue-zl","topic-exchange","zl"); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("topic-queue-zl",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } @Test public void consume3() throws Exception { //1. 获取连接对象 Connection connection = MQConnection.getConnection(); //2. 创建channel final Channel channel = connection.createChannel(); //3. 声明队列-HelloWorld //参数1:queue - 指定队列的名称 //参数2:durable - 当前队列是否需要持久化(true) //参数3:exclusive - 是否排外(conn.close() - 当前队列会被自动删除,当前队列只能被一个消费者消费) //参数4:autoDelete - 如果这个队列没有消费者在消费,队列自动删除 //参数5:arguments - 指定当前队列的其他信息 // 创建队列 channel.queueDeclare("topic-queue-yj",true,false,false,null); //2. 创建exchange并指定绑定方式 channel.exchangeDeclare("topic-exchange", BuiltinExchangeType.TOPIC); channel.queueBind("topic-queue-yj","topic-exchange","#.yf.#"); //4. 开启监听Queue DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { System.out.println("接收到消息:" + new String(body,"UTF-8")); } }; //参数1:queue - 指定消费哪个队列 //参数2:autoAck - 指定是否自动ACK (true,接收到消息后,会立即告诉RabbitMQ) //参数3:consumer - 指定消费回调 channel.basicConsume("topic-queue-yj",true,consume); System.out.println("消费者开始监听队列!"); // 将消费者停住,让其一直监听消息 // System.in.read(); System.in.read(); //5. 释放资源 channel.close(); connection.close(); } }
六、RabbitMQ整合SpringBoot【重点
】
6.1 SpringBoot整合RabbitMQ
6.1.1 创建SpringBoot工程
6.1.2 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
6.1.3 编写配置文件
spring: rabbitmq: host: 192.168.199.109 port: 5672 username: test password: test virtual-host: /test
6.1.4 声明exchange、queue
@Configuration public class RabbitMQConfig { //1. 创建exchange - topic @Bean public TopicExchange getTopicExchange(){ return new TopicExchange("boot-topic-exchange",true,false); } //2. 创建queue @Bean public Queue getQueue(){ return new Queue("boot-queue",true,false,false,null); } //3. 绑定在一起 @Bean public Binding getBinding(TopicExchange topicExchange,Queue queue){ return BindingBuilder.bind(queue).to(topicExchange).with("*.red.*"); } }
6.1.5 发布消息到RabbitMQ
@Autowired private RabbitTemplate rabbitTemplate; @Test void contextLoads() { rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!"); }
6.1.6 创建消费者监听消息
@Component public class Consumer { @RabbitListener(queues = "boot-queue") public void getMessage(Object message){ System.out.println("接收到消息:" + message); } }
6.2 Ack
消费者在消费消息时,为了保证消息的可靠性,可以设置ack模式,当没有设置时,为默认自动ack。
当消费成功后,会对mq进行一个响应,mq收到成功消费的响应后,才会移除消息。
自动ack,默认设置,当消费过程中没有异常并顺利执行完毕,会自动响应mq服务器,当消费过程中出现异常,则不会响应,会被mq视为ack失败。
手动ack,当消息成功后,需要手动ack响应mq服务器,否则在连接未断开前,一直是unacked状态,不会重新分配消费者,直到连接断开,才会回到ready状态,并重新分配消费者。
消息拒绝,当消息被拒绝时,如果设置重新入列,会继续将该消息返回到mq重新分配消费者,如果设置false,则直接丢弃消息。
ack也支持事务,与生产者投递消息时事务代码几乎一样。但是必须设置为手动ack,才能支持事务,自动ack情形下,事务是无效的。
6.2.1 添加配置文件
spring: rabbitmq: listener: simple: acknowledge-mode: manual
6.2.2 手动ack
@RabbitListener(queues = "myqueue1") // 监听mq public void consumer1(String msg, Channel channel, Message message) throws IOException { System.out.println("consumer1===" + msg); // 当设置了手动ack时,没有进行ack,mq会将该消息设置为unacked状态,此时如果连接断开,会将消息重新设置为ready状态,继续分配消费者 // 消息的拒绝,会将消息直接设置为ready状态,继续分配消费者 // 参数:1,消息对象的标识, 2、requeue,是否重新回到队列 // channel.basicReject(message.getMessageProperties().getDeliveryTag(), false); // 手动ack channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); }
七、RabbitMQ的其他操作
7.1 消息的可靠性
RabbitMQ的事务:事务可以保证消息100%传递,可以通过事务的回滚去记录日志,后面定时再次发送当前消息。事务的操作,效率太低,加了事务操作后,比平时的操作效率至少要慢100倍。
RabbitMQ除了事务,还提供了Confirm的确认机制,这个效率比事务高很多。
7.1.0 事务模型
在投递消息时开启事务。
投递完消息后,无误的情况下提交事务。
有误的情况下回滚事务。
性能非常低下。一般情况下MQ就是用来解耦进行削峰填谷,使用事务不推荐。
注意:在连接关闭前,事务都没有commit时,等同于rollback。
try ( // 获得连接 Connection connection = MQConnections.getConnection(); // 创建通道 Channel channel = connection.createChannel(); ){ // 开启事务 channel.txSelect(); // 直接发送简单消息到队列 // 参数:1、交换机 2、队列名称 3、消息携带的properties 4、消息主体 channel.basicPublish("myex1", "", null, msg.getBytes()); System.out.println("消息发送成功"); // 提交事务 channel.txCommit(); // 回滚事务 // channel.txRollback(); }catch (Exception e){ e.printStackTrace(); }
7.1.1 普通Confirm方式
开启了确认机制后,也会降低性能,大概是原来的10倍。不论生产者是否接收确认消息,消息都已经成功发送了。
//3.1 开启confirm channel.confirmSelect(); //3.2 发送消息 String msg = "Hello-World!"; channel.basicPublish("","HelloWorld",null,msg.getBytes()); //3.3 判断消息发送是否成功 if(channel.waitForConfirms()){ System.out.println("消息发送成功"); }else{ System.out.println("发送消息失败"); }
7.1.2 批量Confirm方式。
//3.1 开启confirm channel.confirmSelect(); //3.2 批量发送消息 for (int i = 0; i < 1000; i++) { String msg = "Hello-World!" + i; channel.basicPublish("","HelloWorld",null,msg.getBytes()); } //3.3 确定批量操作是否成功 channel.waitForConfirmsOrDie(); // 当你发送的全部消息,有一个失败的时候,就直接全部失败 抛出异常IOException
7.1.3 异步Confirm方式。
//3.1 开启confirm channel.confirmSelect(); //3.2 批量发送消息 for (int i = 0; i < 1000; i++) { String msg = "Hello-World!" + i; channel.basicPublish("","HelloWorld",null,msg.getBytes()); } //3.3 开启异步回调 channel.addConfirmListener(new ConfirmListener() { @Override public void handleAck(long deliveryTag, boolean multiple) throws IOException { System.out.println("消息发送成功,标识:" + deliveryTag + ",是否是批量" + multiple); } @Override public void handleNack(long deliveryTag, boolean multiple) throws IOException { System.out.println("消息发送失败,标识:" + deliveryTag + ",是否是批量" + multiple); } });
消息传递可靠性 |
---|
7.1.4 Return机制
Confirm只能保证消息到达exchange,无法保证消息可以被exchange分发到指定queue。
而且exchange是不能持久化消息的,queue是可以持久化消息。
采用Return机制来监听消息是否从exchange送到了指定的queue中
消息传递可靠性 |
---|
开启Return机制,并在发送消息时,指定mandatory为true
// 开启return机制 channel.addReturnListener(new ReturnListener() { @Override public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException { // 当消息没有送达到queue时,才会执行。 System.out.println(new String(body,"UTF-8") + "没有送达到Queue中!!"); } }); // 在发送消息时,指定mandatory参数为true channel.basicPublish("","HelloWorld",true,null,msg.getBytes());
7.2 SpringBoot实现
7.2.1 编写配置文件
spring: rabbitmq: publisher-confirm-type: simple publisher-returns: true
7.2.2 开启Confirm和Return
@Component public class PublisherConfirmAndReturnConfig implements RabbitTemplate.ConfirmCallback ,RabbitTemplate.ReturnCallback { @Autowired private RabbitTemplate rabbitTemplate; @PostConstruct // init-method public void initMethod(){ rabbitTemplate.setConfirmCallback(this); rabbitTemplate.setReturnCallback(this); } @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if(ack){ System.out.println("消息已经送达到Exchange"); }else{ System.out.println("消息没有送达到Exchange"); } } @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("消息没有送达到Queue"); } }
7.3 避免消息重复消费
重复消费消息,会对非幂等行操作造成问题
重复消费消息的原因是,消费者没有给RabbitMQ一个ack
重复消费 |
---|
为了解决消息重复消费的问题,可以采用Redis,在消费者消费消息之前,现将消息的id放到Redis中,
id-0(正在执行业务)
id-1(执行业务成功)
如果ack失败,在RabbitMQ将消息交给其他的消费者时,先执行setnx,如果key已经存在,获取他的值,如果是0,当前消费者就什么都不做,如果是1,直接ack。
极端情况:第一个消费者在执行业务时,出现了死锁,在setnx的基础上,再给key设置一个生存时间。
生产者,发送消息时,指定messageId
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder() .deliveryMode(1) //指定消息书否需要持久化 1 - 需要持久化 2 - 不需要持久化 .messageId(UUID.randomUUID().toString()) .build(); String msg = "Hello-World!"; channel.basicPublish("","HelloWorld",true,properties,msg.getBytes());
消费者,在消费消息时,根据具体业务逻辑去操作redis
DefaultConsumer consume = new DefaultConsumer(channel){ @Override public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException { Jedis jedis = new Jedis("192.168.199.109",6379); String messageId = properties.getMessageId(); //1. setnx到Redis中,默认指定value-0 String result = jedis.set(messageId, "0", "NX", "EX", 10); if(result != null && result.equalsIgnoreCase("OK")) { System.out.println("接收到消息:" + new String(body, "UTF-8")); //2. 消费成功,set messageId 1 jedis.set(messageId,"1"); channel.basicAck(envelope.getDeliveryTag(),false); }else { //3. 如果1中的setnx失败,获取key对应的value,如果是0,return,如果是1 String s = jedis.get(messageId); if("1".equalsIgnoreCase(s)){ channel.basicAck(envelope.getDeliveryTag(),false); } } } };
7.4 SpringBoot如何实现
7.4.1 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
7.4.2 编写配置文件
spring: redis: host: 192.168.199.109 port: 6379
7.4.3 修改生产者
@Test void contextLoads() throws IOException { CorrelationData messageId = new CorrelationData(UUID.randomUUID().toString()); rabbitTemplate.convertAndSend("boot-topic-exchange","slow.red.dog","红色大狼狗!!",messageId); System.in.read(); }
7.4.4 修改消费者
@Autowired private StringRedisTemplate redisTemplate; @RabbitListener(queues = "boot-queue") public void getMessage(String msg, Channel channel, Message message) throws IOException { //0. 获取MessageId String messageId = message.getMessageProperties().getHeader("spring_returned_message_correlation"); //1. 设置key到Redis if(redisTemplate.opsForValue().setIfAbsent(messageId,"0",10, TimeUnit.SECONDS)) { //2. 消费消息 System.out.println("接收到消息:" + msg); //3. 设置key的value为1 redisTemplate.opsForValue().set(messageId,"1",10,TimeUnit.SECONDS); //4. 手动ack channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); }else { //5. 获取Redis中的value即可 如果是1,手动ack if("1".equalsIgnoreCase(redisTemplate.opsForValue().get(messageId))){ channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } } }
八、RabbitMQ应用
8.1 客户模块
8.1.1 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
8.1.2 编写配置文件
spring: rabbitmq: host: 192.168.199.109 port: 5672 username: test password: test virtual-host: /test
8.1.3 编写配置类
@Configuration public class RabbitMQConfig { @Bean public TopicExchange topicExchange(){ return new TopicExchange("openapi-customer-exchange",true,false); } @Bean public Queue queue(){ return new Queue("openapi-customer-queue"); } @Bean public Binding binding(Queue queue,TopicExchange topicExchange){ return BindingBuilder.bind(queue).to(topicExchange).with("openapi.customer.*"); } }
8.1.4 修改Service
//3. 发送消息 rabbitTemplate.convertAndSend("openapi-customer-exchange","openapi.customer.add",JSON.toJSON(customer)); /*//3. 调用搜索模块,添加数据到ES //1. 准备请求参数和请求头信息 String json = JSON.toJSON(customer); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.parseMediaType("application/json;charset=utf-8")); HttpEntity<String> entity = new HttpEntity<>(json,headers); //2. 使用RestTemplate调用搜索模块 restTemplate.postForObject("http://localhost:8080/search/customer/add", entity, String.class);*/
8.2 客户模块
8.2.1 导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency>
8.2.2 编写配置文件
spring: rabbitmq: host: 192.168.199.109 port: 5672 username: test password: test virtual-host: /test listener: simple: acknowledge-mode: manual
8.2.3 编写配置类
@Configuration public class RabbitMQConfig { @Bean public TopicExchange topicExchange(){ return new TopicExchange("openapi-customer-exchange",true,false); } @Bean public Queue queue(){ return new Queue("openapi-customer-queue"); } @Bean public Binding binding(Queue queue, TopicExchange topicExchange){ return BindingBuilder.bind(queue).to(topicExchange).with("openapi.customer.*"); } }
8.2.4 编写消费者
@Component public class CustomerListener { @Autowired private CustomerService customerService; @RabbitListener(queues = "openapi-customer-queue") public void consume(String json, Channel channel, Message message) throws IOException { //1. 获取RoutingKey String receivedRoutingKey = message.getMessageProperties().getReceivedRoutingKey(); //2. 使用switch switch (receivedRoutingKey){ case "openapi.customer.add": //3. add操作调用Service完成添加 customerService.saveCustomer(JSON.parseJSON(json, Customer.class)); //4. 手动ack channel.basicAck(message.getMessageProperties().getDeliveryTag(),false); } } }
[常见面试题]
:1、如何保证消息不丢失。
2、如何保证消息不重复消费。
九、实现流量削峰
pom.xml配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.qf</groupId> <artifactId>day87_sbmq2</artifactId> <version>0.0.1-SNAPSHOT</version> <name>day87_sbmq2</name> <description>Demo project for Spring Boot</description> <properties> <java.version>1.8</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.3.4.RELEASE</spring-boot.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.40</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-amqp</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>1.8</source> <target>1.8</target> <encoding>UTF-8</encoding> </configuration> </plugin> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>2.3.4.RELEASE</version> <configuration> <mainClass>com.qf.day87_sbmq2.Day87Sbmq2Application</mainClass> </configuration> <executions> <execution> <id>repackage</id> <goals> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>
首先,直接使用数据库操作:
application.properties
# 应用名称 spring.application.name=day87_sbmq2 # 应用服务 WEB 访问端口 server.port=8080 # 数据库驱动: spring.datasource.driver-class-name=com.mysql.jdbc.Driver # 数据源名称 spring.datasource.name=defaultDataSource # 数据库连接地址 spring.datasource.url=jdbc:mysql://localhost:3306/ems # 数据库用户名&密码: spring.datasource.username=root spring.datasource.password=root
代码如下:
@RestController public class OrderController { @Resource private OrderService orderService; @RequestMapping("/add") public String save(Orders order){ orderService.save(order); return "success"; } @RequestMapping("/cancel") public String cancel(Orders orders){ orderService.cancelOrder(orders); return "success"; } } @Data @NoArgsConstructor @AllArgsConstructor @TableName("orders") public class Orders implements java.io.Serializable{ @TableId(type = IdType.AUTO) private Integer id; private String no; private Double price; @TableField("create_time") private Date createTime; @TableField("update_time") private Date updateTime; private Integer status; } @Repository public interface OrderDAO extends BaseMapper<Orders> { } @Service public class OrderService { @Resource private OrderDAO orderDAO; public void save(Orders orders){ orders.setCreateTime(new Date()); orders.setUpdateTime(new Date()); orders.setNo(UUID.randomUUID().toString().replace("-", "")); orders.setStatus(0); orderDAO.insert(orders); } public void cancelOrder(Orders orders){ orders.setUpdateTime(new Date()); orders.setStatus(2); // 取消 orderDAO.updateById(orders); } } @MapperScan("com.qf.day87_sbmq2.dao") @SpringBootApplication public class Day87Sbmq2Application { public static void main(String[] args) { SpringApplication.run(Day87Sbmq2Application.class, args); } }
接着使用JMeter,1秒1000线程,测试10秒,发现会有部分数据无法添加到数据库,达到峰值。
然后,使用RabbitMQ改写,实现流量削峰。
@Configuration public class RabbitConfig { // 创建队列 @Bean public Queue simpleQueue(){ return new Queue("simpleQueue"); } // 创建Fanout类型交换机 @Bean public DirectExchange simpleExchange(){ return new DirectExchange("simpleExchange"); } // 将队列绑定到交换机 @Bean public Binding simpleBinding(){ return BindingBuilder.bind(simpleQueue()).to(simpleExchange()).with("simpleKey"); } } @Component public class MySender { @Resource private RabbitTemplate rabbitTemplate; public void sendOrder(Orders orders){ orders.setCreateTime(new Date()); orders.setUpdateTime(new Date()); orders.setNo(UUID.randomUUID().toString().replace("-", "")); orders.setStatus(0); rabbitTemplate.convertAndSend("simpleExchange", "simpleKey", orders); } } @Component public class MyConsumer { @Resource private OrderDAO orderDAO; @RabbitListener(queues = "simpleQueue") public void consume(Orders orders){ orderDAO.insert(orders); System.out.println(orders.getNo() + "添加成功"); } }
修改Controller中的代码:
@RestController public class OrderController { @Resource private MySender mySender; @RequestMapping("/add") public String save(Orders order){ mySender.sendOrder(order); return "success"; } }
再次使用JMeter,1秒1000线程,测试10秒,发现虽然消费者可能执行时间稍长,但是所有数据都安全保存到数据库中,实现削峰。
十、使用死信队列实现订单超时取消
死信队列:DLX,
dead-letter-exchange
利用DLX,当消息在一个队列中变成死信
(dead message)
之后,它能被重新publish到另一个Exchange,这个Exchange就是DLX
消息变成死信有以下几种情况:
消息被拒绝(basic.reject / basic.nack),并且requeue = false
消息TTL过期
队列达到最大长度
死信处理过程:
DLX也是一个正常的Exchange,和一般的Exchange没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。
当这个队列中有死信时,RabbitMQ就会自动的将这个消息重新发布到设置的Exchange上去,进而被路由到另一个队列。
可以监听这个队列中的消息做相应的处理。
具体实现:
上面的代码中,修改RabbitConfig类,配置延迟队列和死信队列。
@Configuration public class RabbitConfig { // 创建队列 @Bean public Queue orderDelayQueue(){ // 将订单延迟队列绑定死信队列 Map map = new HashMap(); map.put("x-dead-letter-exchange", "dead_exchange"); map.put("x-dead-letter-routing-key", "dead_route_key"); return new Queue("orderDelayQueue", true, false, false, map); } // 创建Fanout类型交换机 @Bean public DirectExchange orderDelayExchange(){ return new DirectExchange("orderDelayExchange"); } // 将队列绑定到交换机 @Bean public Binding simpleBinding(){ return BindingBuilder.bind(orderDelayQueue()).to(orderDelayExchange()).with("orderDelayKey"); } // 创建死信队列 @Bean public Queue deadQueue(){ // 将订单延迟队列绑定死信队列 return new Queue("deadQueue"); } // 创建Fanout类型交换机 @Bean public DirectExchange deadExchange(){ return new DirectExchange("dead_exchange"); } // 将队列绑定到交换机 @Bean public Binding deadBinding(){ return BindingBuilder.bind(deadQueue()).to(deadExchange()).with("dead_route_key"); } }
修改代码:
@RestController public class OrderController { @Resource private OrderService orderService; @Resource private MySender mySender; @RequestMapping("/add") public String save(Orders order){ order.setCreateTime(new Date()); order.setUpdateTime(new Date()); order.setNo(UUID.randomUUID().toString().replace("-", "")); order.setStatus(0); orderService.save(order); // 投递到订单延迟队列中,该队列没有消费者,作用是为了超时后投递到死信队列中 mySender.sendOrder(order); return "success"; } } @Component public class MySender { @Resource private RabbitTemplate rabbitTemplate; public void sendOrder(Orders orders){ rabbitTemplate.convertAndSend("orderDelayExchange", "orderDelayKey", orders, message -> { // 设置超时时间,单位毫秒 message.getMessageProperties().setExpiration("10000"); return message; } ); System.out.println(orders.getNo() + "订单已经放入队列中..."); } } @Component public class MyConsumer { @Resource private OrderDAO orderDAO; @RabbitListener(queues = "deadQueue") public void consume(Orders orders){ orders.setStatus(2); QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("no", orders.getNo()); orderDAO.update(orders, queryWrapper); System.out.println(orders.getNo() + "取消成功"); } } @Service public class OrderService { @Resource private OrderDAO orderDAO; public void save(Orders orders){ orderDAO.insert(orders); } }
通过controller中添加方法,测试添加一条订单,等待10秒后,看是否会取消订单。