一、RocketMQ基础
1、MQ简介
(1)MQ简介
MQ(Message Queue)是一种用于传递消息的跨进程的通信机制。通俗点说,MQ是一个先进先出的数据结构,可以理解为一个管道。消息生产者将消息放到管道中,消息消费者从管道中获取消息来使用。MQ的作用就是缓存消息和传递消息。
为什么需要使用MQ呢?我们可以直接让消息生产者将消息发送给消息消费者。它们之间有两种通信方式RESTful、RPC,但是会让消息生产者和消息消费者二者紧密耦合,与高内聚/松耦合的原则相违背。
在消息生产者和消息消费者之间加入MQ后,消息生产者不用关心消息消费者,只要将消息放入MQ就可以了。消息消费者也不用关心消息生产者,只要从MQ中取消息就可以了,这就实现了消息生产者和消息消费者的解耦。
(2)MQ的应用场景
1、异步解耦
最常见的场景是用户注册后,需要发送注册邮件和短信通知,以告知用户注册成功。
传统的做法如下,这种方式中只有注册、邮件、短信三个任务全部完成后,才能返回注册结果到客户端。但是对于用户来说,注册功能实际只需要注册系统存储用户的账户信息后,该用户便可以登录,而后续的注册短信和邮件都不是需要实时关注的步骤。
基于MQ的做法如下,当数据写入注册系统后,注册系统就把其他操作放入消息队列MQ中,然后返回用户结果。这样用户可以马上登录,消息队列MQ异步地进行其它操作。
异步解耦是消息队列MQ的主要特点,主要目的是减少请求响应时间和解耦。主要的使用场景就是将比较耗时而且不需要即时(同步)返回结果的操作作为消息放入消息队列。同时,由于使用了消息队列MQ,只要保证消息格式不变,消息的发送方和接收方并不需要彼此联系,也不需要受对方的影响,即解耦合。
2、流量削峰
流量削峰是消息队列MQ的常用场景,一般在秒杀或团购(高并发)活动中使用广泛。
在秒杀或团购活动中,由于用户请求量较大,导致流量暴增,秒杀的应用在处理如此大量的访问流量后,下游的通知系统无法承载海量的调用量,甚至会导致系统崩溃等问题而发生漏通知的情况。为解决这些问题,可在应用和下游通知系统之间加入消息队列MQ。
秒杀处理流程如下所述:
(1)用户发起海量秒杀请求到秒杀业务处理系统。
(2)秒杀处理系统按照秒杀处理逻辑将满足秒杀条件的请求发送至消息队列MQ。
(3)下游的通知系统订阅消息队列MQ的秒杀消息,再将秒杀成功消息发送到用户。
(4)用户收到秒杀成功的通知。
(3)常见的MQ产品
目前业界有很多MQ产品,比较出名的有下面这些:
1、ZeroMQ
号称最快的消息队列系统,尤其针对大吞吐量的需求场景。扩展性好,开发比较灵活,采用C语言实现,实际上只是一个socket库的重新封装,如果做为消息队列使用,需要开发大量的代码。
ZeroMQ仅提供非持久性的队列,也就是说如果down机,数据将会丢失。
2、RabbitMQ
使用erlang语言开发,性能较好,适合于企业级的开发。但是不利于做二次开发和维护。
3、ActiveMQ
历史悠久的Apache开源项目。已经在很多产品中得到应用,实现了JMS1.1规范,可以和springjms轻松融合,实现了多种协议,支持持久化到数据库,对队列数较多的情况支持不好。
4、RocketMQ
RocketMQ是阿里巴巴开源的分布式消息中间件,基于高可用分布式集群技术,提供低延时的、高可靠性的消息发布与订阅服务。现在是Apache的一个顶级项目,在阿里内部使用非常广泛,已经经过了"双11"这种万亿级的消息流转。
5、Kafka
Kafka是Apache下的一个子项目,是一个高性能跨语言分布式Publish/Subscribe消息队列系统,相对于ActiveMQ是一个非常轻量级的消息系统,除了性能非常好之外,还是一个工作良好的分布式系统。
2、RocketMQ核心架构和概念
(1)RocketMQ核心架构
1、RocketMQ主要由四个部分组成:NameServer、Broker、Producer、Consumer。
Name Server是消息队列的协调者,相当于邮局,用于管理Broker。Broker向Name Server注册路由信息,Producer和Consumer向Name Server获取路由信息。
Broker是RocketMQ的核心,相当于邮递员,负责消息的接收、存储和投递。
Producer是消息生产者。它从NameServer获取Broker信息并与Broker建立连接,向Broker发送消息。
Consumer是消息消费者。它从NameServer获取Broker信息并与Broker建立连接,从Broker获取消息。
2、消息队列的组成
Topic用来发送和接收消息。为了区分不同类型的消息,在发送和接收消息前都需要先创建相应的Topic。
为了提高性能和吞吐量,一个Topic可以设置多个Message Queue,生产者可以并行往各个Message Queue里发送消息,消费者也可以并行的从多个Message Queue读取消息。
(2)理解RocketMQ运行过程
1、首先启动NameServer,然后启动Broker并注册到NameServer中。
2、创建消息生产者,并且从Name Server中自动获取Broker。
3、消息生产者相当于寄件人,创建消息对象Message,并投递到Broker管理的Topic。
4、创建消息消费者,并且从NameServer中自动获取Broker。
5、消息消费者相当于收件人,它会订阅Broker管理的特定Topic。
6、当Topic中有消息到达时,Broker就会通知消费消费都来消费Topic中的消息对象。
3、RocketMQ环境搭建
在Docker环境中安装RocketMQ服务,这里使用RocketMQ4.8.0。
(1)下载RocketMQ镜像
1、镜像foxiswho/rocketmq:4.8.0中包含了NameServer和Broker。
docker pull foxiswho/rocketmq:4.8.0
2、控制台镜像
docker pull styletang/rocketmq-console-ng
(2)启动NameServer
NameServer是一个独立的服务器,其默认端口是9876。
docker run -d \
--name rmqnamesrv \
-e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \
-p 9876:9876 \
foxiswho/rocketmq:4.8.0 \
sh mqnamesrv
注意:这里用了启动namesrv服务的命令sh mqnamesrv。
(3)启动Broker
Broker在启动时会把自身信息注册到NameServer,所以需要配置NameServer的地址。
Broker需要被外网访问,所以需要为Broker配置宿主机的IP地址作为brokerIP1的值,这样Broker会把宿主机的IP注册到NameServer中。
1、在宿主机中创建文件:
mkdir -p /data/rmq/conf
cd /data/rmq/conf
touch broker.conf
chmod 777 * -R
2、编辑 /data/rmq/conf/broker.conf
vim /data/rmq/conf/broker.conf
# 编辑后写入
brokerIP1=192.168.4.202
brokerName=broker_climb_1
注意:brokerIP1的值是宿主机CentOS的IP地址,brokerName是Broker的唯一标识。
3、在启动命令中使用-c参数指定的配置文件,挂载/data/rmq/conf/broker.conf到容器的/home/rocketmq/conf/broker.conf。
注意:broker默认侦听端口10911,vip端口是10909,主从复制端口是10912。
docker run -d \
--name rmqbroker \
-e "NAMESRV_ADDR=192.168.4.202:9876" \
-e "JAVA_OPT_EXT=-Xms512M -Xmx512M -Xmn128m" \
-v /data/rmq/conf/broker.conf:/home/rocketmq/conf/broker.conf \
-p 10911:10911 \
-p 10909:10909 \
-p 10912:10912 \
foxiswho/rocketmq:4.8.0 \
sh mqbroker -c /home/rocketmq/conf/broker.conf
在Broker容器的日志中可以看到Broker注册成功的信息。
The broker[broker_climb_1, 192.168.4.202:10911] boot success. serializeType=JSON and name server is 192.168.4.202:9876
(4)启动控制台
控制台需要从NameServer中读取整个MQ的信息,所以需要NameServer的IP地址。
使用下面命令启动控制台,外部映射端口是8180。
docker run --name rmqconsole -dt \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=192.168.4.202:9876 -Dcom.rocketmq.sendMessageWithVIPChannel=false" \
-p 8180:8080 \
styletang/rocketmq-console-ng
在浏览器中使用下面的URL访问控制台的界面。
http://192.168.4.202:8180/
4、原生Java API实现消息发送
我们使用Java代码来演示消息的发送和接收。
(1)引入RocketMQ依赖
1、创建一个Maven项目。
2、在父项目rmq_demo中加入RocketMQ客户端的依赖,它与Spring没有关系。
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>com.climbcloud.mq</groupId>
<artifactId>rmq_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>rmq-demo-sender</module>
<module>rmq-demo-receiver</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.7.1</version>
</dependency>
</dependencies>
</project>
(2)消息发送的步骤
1. 创建消息生产者,指定生产者所属的组名
2. 指定Nameserver地址
3. 启动生产者
4. 创建消息对象,指定主题、标签和消息体
5. 发送消息
6. 关闭生产者
(3)发送消息程序
public class RocketMQSendTest {
public static void main(String[] args) throws Exception {
//1. 创建消息生产者, 指定生产者所属的组名
DefaultMQProducer producer = new DefaultMQProducer("myproducer-group");
//2. 指定Nameserver地址
producer.setNamesrvAddr("192.168.0.106:9876");
//3. 启动生产者
producer.start();
//4. 创建消息对象,指定主题、标签和消息体
Message msg = new Message("myTopic", "myTag", ("RocketMQ Message").getBytes(StandardCharsets.UTF_8));
//5. 同步发送消息,后面1000是超时时间
SendResult sendResult = producer.send(msg,10000);
System.out.println(sendResult);
//6. 关闭生产者
producer.shutdown();
}
}
(4)测试
在IDEA控制台中可以看到sendResult的输出信息。
SendResult [sendStatus=SEND_OK, msgId=C0A8006A1D002437C6DC09C94DB50000, offsetMsgId=C0A8006700002A9F0000000000000000, messageQueue=MessageQueue [topic=myTopic, brokerName=broker_climb_1, queueId=0], queueOffset=0]
在RocketMQ控制台中可以看到刚创建的消息队列,点击“主题”可以查看新增的Topic。
还可以点击“消息”可以查看发出的消息。
如果前面方式不能查询(这是BUG),可以直接用MESSAGEID来查询。
可以看到发送消息的内容:
5、原生Java API实现消息消费
(1)消息接收的步骤
1. 创建消息消费者,指定消费者所属的组名
2. 指定Nameserver地址
3. 指定消费者订阅的主题和标签
4. 设置回调函数,编写处理消息的方法
5. 启动消息消费者
(2)接收消息程序
public class RocketMQReceiveTest {
public static void main(String[] args) throws MQClientException {
//1. 创建消息消费者, 指定消费者所属的组名
DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("myconsumergroup");
//2. 指定Nameserver地址
consumer.setNamesrvAddr("192.168.0.106:9876");
//3. 指定消费者订阅的主题和标签。注意主题必须与发送方一致,而tag也要和发送方一致,这用tag用*表示接收所有tag的消息
consumer.subscribe("myTopic", "*");
//4. 设置回调函数,编写处理消息的方法
consumer.registerMessageListener(new MessageListenerConcurrently() {
@Override
public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
for(MessageExt messageExt : msgs) {
String message = new String(messageExt.getBody(), StandardCharsets.UTF_8);
System.out.println("接收新的消息: " + message);
}
//返回消费状态
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}
});
//5. 启动消息消费者
consumer.start();
System.out.println("Consumer Started.");
}
}
(3)测试
1、首先运行消费者程序,它将侦听指定Topic和tag类型的消息。
2、然后运行发送者程序,它将发送指定的Topic和tag类型的消息到Broker中。
3、在消费者程序中可以看到接收的消息,并且程序没有退出,还会继续接收消息。
Consumer Started.
接收新的消息: RocketMQ Message
6、开发建议
建议每个应用创建一个Topic,利用tag来标记应用中的不同业务。如果有消息业务有唯一标识,则填写到keys字段中,方便以后的定位或过滤查找。
二、RocketMQTemplate编程
RocketMQ提供三种方式来发送普通消息:可靠同步发送、可靠异步发送和单向发送。
1、创建项目
(1)RocketMQ与SpringBoot集成
RocketMQ与Spring集成主要提供了三个特性:
1、RocketMQTemplate统一发送消息,包括同步消息、异步消息和事务消息。
2、@RocketMQTransactionListener用来处理事务消息的监听和回查。
3、@RocketMQMessageListener用来消费消息。
(2)创建项目
1、创建父子项目。
2、在父项目rmq_demo中加入RocketMQ客户端的依赖,它与Spring没有关系。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
</parent>
<groupId>com.climbcloud.mq</groupId>
<artifactId>rmq_demo</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<modules>
<module>rmq-demo-sender</module>
<module>rmq-demo-receiver</module>
</modules>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<!-- rocketmq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
</project>
2、发送消息
(1)可靠同步发送
同步发送是指发送端在发送消息时,阻塞线程进行等待,直到服务器返回发送的结果。发送端如果需要保证消息的可靠性,防止消息发送失败,可以采用同步阻塞式的发送,然后同步检查Brocker返回的状态来判断消息是否持久化成功。
这种方式应用场景非常广泛,例如重要通知邮件、报名短信通知、营销短信系统等。
(2)可靠异步发送
异步发送是指发送端在发送消息时,传入回调接口实现类,调用该发送接口后不会阻塞,发送方法会立即返回,这时回调任务会在另一个线程中执行,消息发送结果会回传给相应的回调函数。具体的业务实现可以根据发送结果来判断是否需要重试来保证消息的可靠性。
异步发送一般用于通信链路耗时较长,对响应时间较为敏感的业务场景,例如用户视频上传后通知启动转码服务,转码完成后通知推送转码结果等。
(3)单向发送
单向发送是指发送端发送完成之后,调用该发送接口后立刻返回,并不返回发送的结果,业务方无法根据发送的状态来判断消息是否发送成功。
单向发送相对前两种发送方式来说是一种不可靠的消息发送方式,因此要保证消息发送的可靠性,不推荐采用这种方式来发送消息。
单向发送适用于某些耗时非常短,但对可靠性要求并不高的场景,例如日志收集。
三种发送方式的对比:
发送方式 | 发送TPS | 发送结果反馈 | 可靠性 |
同步发送 | 快 | 有 | 不丢失 |
异步发送 | 快 | 有 | 不丢失 |
单向发送 | 最快 | 无 | 可能丢失 |
(4)RocketMQ配置
1、在application.yml中配置如下。
# 必须配置
rocketmq:
# 指定nameServer
nameServer: 192.168.0.106:9876
producer:
# 指定发送者组名
group: myproducer-group
# 超时时间
sendMessageTimeout: 300000
(5)发送消息实现
@SpringBootTest
public class RocketMQTemplateSenderTest {
@Autowired
private RocketMQTemplate rocketMQTemplate;
//同步消息。syncSend方法执行完并返回SendResult后,才能执行下面的代码。
@Test
public void testSyncSend() {
//参数一: topic。如果想添加tag,可以使用"topic:tag"的写法
//参数二: 消息内容
//参数三: 超时时间
SendResult sendResult = rocketMQTemplate.syncSend("test-topic-1", "这是一条同步消息", 10000);
System.out.println(sendResult);
}
//异步消息。asyncSend方法是异步运行,它执行后立即返回,不会等待返回结果
@Test
public void testAsyncSend() throws InterruptedException {
//参数一: topic。如果想添加tag 可以使用"topic:tag"的写法
//参数二: 消息内容
//参数三: 回调函数,处理返回结果
rocketMQTemplate.asyncSend("test-topic-1", "这是一条异步消息", new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
System.out.println(sendResult);
}
@Override
public void onException(Throwable throwable) {
System.out.println(throwable);
}
});
//让线程不要终止,等待回调函数被调用,处理异步返回结果
Thread.sleep(30000000);
}
//单向消息。sendOneWay方法没有返回结果。
@Test
public void testOneWay() {
rocketMQTemplate.sendOneWay("test-topic-1", "这是一条单向消息");
}
}
3、接收消息
(1)RocketMQ配置
1、在application.yml中配置如下。
rocketmq:
# 指定nameServer
nameServer: 192.168.0.106:9876
注意:消费者组名在程序中声明。
(2)RocketMQListener介绍
消息接收服务类要实现RocketMQListener接口,在方法onMessage中处理消息,其中泛型是消息体的数据类型,即发送方发送的数据类型。
消息接收服务类上需要@RocketMQMessageListener,其中指定了消费者组和Topic。注意:Topic必须与生产者方保持一致,否则无法消费。
@Slf4j
@Component
@RocketMQMessageListener(consumerGroup = "shop-user", topic = "order-topic")
public class SmsService implements RocketMQListener<Order> {
// 实现消费逻辑
@Override
public void onMessage(Order order) {
log.info("收到一个订单信息{},接下来发送短信", JSON.toJSONString(order));
}
}
(3)编写消息接收服务
@Component
@RocketMQMessageListener(consumerGroup = "myconsumer-group", topic = "test-topic-1")
public class ConsumerService implements RocketMQListener<String> {
// 实现消费逻辑
@Override
public void onMessage(String messgae) {
System.out.println("收到一个信息: " + messgae);
}
}
三、RocketMQ解耦
1、业务需求
我们模拟一种使用RocketMQTemplate实现下单消息的发送和接收的场景。用户下单成功之后,向下单用户发送短信。
下单操作是在订单微服务中实现,而短信发送是在用户微服务中实现,就涉及到了两个微服务之间的调用问题。但是下单之后通常要快速返回结果,而短信发送是不需要立即响应,这种情形就特别适合MQ应用场景。
我们在订单微服务中在下单成功之后,将订单信息投递到MQ中。用户微服务监听到MQ中有新的消息,就会消费MQ中的订单信息,并且发送短信。
2、订单微服务发送消息
(1)创建OrderDto
在shop-order-api中创建OrderDto类。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description="Order对象")
public class OrderDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "订单id")
private Long id;
@Schema(description = "用户id")
private Long userId;
@Schema(description = "用户名")
private String userName;
@Schema(description = "商品id")
private Long productId;
@Schema(description = "商品名称")
private String productName;
@Schema(description = "商品价格")
private BigDecimal productPrice;
@Schema(description = "购买数量")
private Integer number;
}
(2)MapStruct转换
定义dto和entity之间的转换规则。
@Mapper
public interface OrderConvert {
OrderConvert INSTANCE = Mappers.getMapper(OrderConvert.class);
Order dto2entity(OrderDto orderDto);
@Mappings({
@Mapping(source="id", target="id"),
@Mapping(source="name", target="name"),
@Mapping(source="price", target="price"),
@Mapping(source="stock", target="stock")
})
OrderDto entity2dto(Order order);
List<OrderDto> entityList2dtoList(List<Order> orders);
List<Order> dtoList2entityList(List<OrderDto> orderDtos);
}
(3)RocketMQ配置
1、在shop-order-service中添加rocketmq的依赖。
<!-- rocketmq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
2、在bootstrap.yml中添加RocketMQ配置。
rocketmq:
#rocketMQ服务的地址
name-server: 192.168.0.106:9876
producer:
# 生产者组
group: shop-order-service
(4)发送订单消息
这里使用RocketMQTemplate的convertAndSend(D destination, Object payload)方法,先使用MessageConverter序列化对象payload并构建消息体,然后将消息体发送到MQ。
@Slf4j
@RestController
@RequestMapping("/orders")
public class OrderController {
@Autowired
private OrderService orderService;。
@Autowired
private RemoteProductService productService;
@Autowired
private RocketMQTemplate rocketMQTemplate;
//准备买1件商品
@PostMapping("")
public R order(Long productId) throws JsonProcessingException {
log.info(">>客户下单,使用{}号调用商品微服务查询商品信息", productId);
//通过fegin调用商品微服务
R<ProductDto> result = productService.getById(productId);
ProductDto productDto = result.getData();
log.info(">>查询{}号商品信息,查询结果:{}", productId, new ObjectMapper().writeValueAsString(productDto));
// 下单(创建订单)
Order order = new Order();
order.setUserId(1L);
order.setUserName("测试用户");
order.setProductId(productDto.getId());
order.setProductName(productDto.getName());
order.setProductPrice(productDto.getPrice());
order.setNumber(1);// 默认1件商品
orderService.save(order);
log.info(">>创建订单成功,订单信息为:{}" + new ObjectMapper().writeValueAsString(order));
// 下单成功后发送消息到mq。第一个参数是topic或topic:tag,第二个参数是消息体
rocketMQTemplate.convertAndSend("order-topic:tag1", OrderConvert.INSTANCE.entity2dto(order));
return R.success();
}
}
(5)测试消息发送
1、通过Psotman访问“http://localhost:57022/orders?productId=1418594619650609154”,可以看到返回的JSON数据,并且数据库中加入了订单信息。
2、在MQ控制台中查看order-topic中是否已经加入下单消息。
在消息详情中可以看到消息体。
3、创建用户微服务
(1)引入依赖
这里在pom.xml中引入Web和Mybatis依赖,重点是引入shop-common-rest和shop-common-mybatis,程序运行时会自动加载其中的自动配置类。
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>shop-user</artifactId>
<groupId>com.climbcloud.shop</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<artifactId>shop-user-service</artifactId>
<dependencies>
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-user-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- 公用子项目 -->
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-common-rest</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-common-mybatis</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!-- Web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- REST文档 -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
</dependency>
<!-- mybatis-plus不要与mybatis同时被依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
(2)配置项目
在bootstrap.yml中配置数据库连接和Mybatis。
server:
port: 57023
spring:
application:
name: shop-user-service
main:
allow-bean-definition-overriding: true # 需要设置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/shop-user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=false
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
mvc:
format:
# http数据的日期格式
date: yyyy-MM-dd HH:mm:ss
# 出现错误时直接抛出异常,交由SpringMVC处理
throw-exception-if-no-handler-found: true
web:
resources:
# SpringMVC不要为资源建立 /** 的映射配置,否则请求都被处理就没有404
add-mappings: false
jackson:
# 只控制java.util.Date的序列化format
date-format: yyyy-MM-dd HH:mm:ss
locale: zh # 当地时区
time-zone: GMT+8
default-property-inclusion: NON_NULL # Google建议属性为NULL则不序列化
mybatis-plus:
# 别名配置,多个package用逗号或者分号分隔
typeAliasesPackage: com.climbcloud.**.entity
# mapperLocations是mapper文件位置
mapper-locations: classpath:com/climbcloud/**/mapper/*.xml
configuration:
#配置JdbcTypeForNull
jdbc-type-for-null: 'null'
# 显示SQL语句
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 健康监控:服务下线后自动删除注册实例
management:
endpoints:
web:
exposure:
include: "*"
(3)应用配置类
@Configuration
@MapperScan(basePackages = {"com.climbcloud.**.mapper"})
public class UserConfiguration implements WebMvcConfigurer {
}
(4)生成代码
public class CodeGenerator {
public static void main(String[] args) {
// 工程所在目录
String projectPath = System.getProperty("user.dir");
String projectName = "/shop-user/shop-user-service";
String author = "filteraid";
// 目前支持MySQL
String url = "jdbc:mysql://localhost:3306/shop-user?useUnicode=true&characterEncoding=UTF8&serverTimezone=GMT%2b8&useSSL=false&allowPublicKeyRetrieval=true";
String username = "root";
String password = "123456";
// 按驼峰命名的数据表名
String[] tableNames = new String[]{ "shop_user" };
// 去掉前缀后按驼峰命名来生成类名
String [] tablePrefix = new String[] { "shop" };
// 是否有父实体
boolean hasSuperEntity = Boolean.TRUE;
String[] superEntityColumns = {"id"};
// 包配置
String packageName = "com.climbcloud";
String moduleName = "user";
MpGenerator mpGenerator = new MpGenerator();
mpGenerator.generator(projectPath, projectName, author, url, username, password, tableNames, tablePrefix, hasSuperEntity,
superEntityColumns, packageName, moduleName);
}
}
(5)业务开发
服务实现类继承了BaseServiceImpl,父类中可以定义所有服务类的公共方法。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
}
(6)UserDto
在shop-user-api中创建UserDto。
@Data
@NoArgsConstructor
@AllArgsConstructor
@Schema(description="User对象")
public class UserDto implements Serializable {
private static final long serialVersionUID = 1L;
@Schema(description = "用户id")
private Long id;
@Schema(description = "用户名")
private String userName;
@Schema(description = "密码")
private String password;
@Schema(description = "手机号")
private String telephone;
@Schema(description = "像片")
private String photo;
@Schema(description = "生日")
private LocalDateTime birthday;
}
(7)MapStruct转换
@Mapper
public interface UserConvert {
UserConvert INSTANCE = Mappers.getMapper(UserConvert.class);
User dto2entity(UserDto orderDto);
@Mappings({
@Mapping(source="id", target="id"),
@Mapping(source="userName", target="userName"),
@Mapping(source="password", target="password"),
@Mapping(source="telephone", target="telephone"),
@Mapping(source="photo", target="photo"),
@Mapping(source="birthday", target="birthday", dateFormat="yyyy-MM-dd HH:mm:ss")
})
UserDto entity2dto(User order);
List<UserDto> entityList2dtoList(List<User> orders);
List<User> dtoList2entityList(List<UserDto> orderDtos);
}
(8)编写查询服务
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@ResponseStatus(HttpStatus.OK)
@GetMapping("/{id}")
public R<UserDto> getById(@PathVariable("id") Long id) {
User user = userService.getById(id);
UserDto userDto = UserConvert.INSTANCE.entity2dto(user);
return R.success().data(userDto);
}
}
(9)测试
1、在数据表shop-user中加入一条记录。
2、启动程序,在Postman中访问“http://localhost:57023/users/1”。
4、用户微服务订阅消息
(1)RocketMQ配置
1、在shop-user-service中添加rocketmq的依赖。
<!-- rocketmq -->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.climbcloud.shop</groupId>
<artifactId>shop-order-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
2、在application.yml中添加RocketMQ配置。
rocketmq:
#rocketMQ服务的地址
name-server: 192.168.0.106:9876
(2)用户微服务订阅消息
下面是一个订阅消息的服务,在接收消息后发送短信的服务。
@Slf4j
@Service
@RocketMQMessageListener(consumerGroup = "shop-user-group", topic = "order-topic")
public class SmsService implements RocketMQListener<OrderDto> {
// 实现消费逻辑
@Override
public void onMessage(OrderDto orderDto) {
log.info("收到一个订单信息{},接下来发送短信", orderDto);
}
}
(3)测试消息订阅
启动订单微服务、商品微服务和用户微服务,然后在订单微服务中执行下单操作,在用户微服务中观看控制台,就会输出订单消费的日志信息。
在RocketMQ控制台界面中再次查看消费,发现消息已经被本消费者消费了。