基于MQ的通信流程
课程微服务-----------》MQ-----------》用户微服务
在LessonService类中考虑使用MQ来实现用户微服务的扣减金额服务,当然也可以使用feign来实现,实际项目中一定要考虑具体的场景。这里仅仅是为了讲解MQ。
MQ适用场景
-
异步处理(耗时不阻塞主流程的服务)
-
流量削峰填谷
-
解耦服务间的依赖关系
合理的使用MQ,提高服务的吞吐量,提高用户的体验
操作MQ的方式演进
原生API-》Spring Messaging(参考Spring Boot说明书)->Spring Integration(配置多) -> Spring Cloud Stream
Spring Cloud Stream是什么
一个用于构建消费驱动的微服务的框架,致力于简化MQ通信的服务
Spring Cloud Stream支持的MQ产品
- RabbitMQ (Spring Cloud Stream支持)
- Kafka(Spring Cloud Stream支持)
- Apache RocketMQ(Spring Cloud Alibaba支持)
Spring Cloud Stream编程模型:概念
- Destination Binder(目标绑定器):与消息中间件通信的组件
- Destination Bindings(目标绑定):Binding是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产,由binder创建,有input binding和output binding
- Message(消息)
搭建RabbitMQ
直接使用docker即可(注意 虚拟主机的网络配置着必须在NAT模式下才可以,否则无法下载软件)
mkdir -p /docker-data/rabbitmq
docker run -d --name mq -p 5672:5672 -p 15672:15672 -v /docker-data/rabbitmq:/var/lib/rabbitmq -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=admin rabbitmq:3.7.14-management
Stream实现消息收发
接收端添加依赖
使用rabbitmq添加
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
接收端添加注解
在启动类上面添加注解EnableBinding
package com.cloud.msuser;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Sink;
@SpringBootApplication
@EnableBinding(Sink.class)
public class MsUserApplication {
public static void main(String[] args) {
SpringApplication.run(MsUserApplication.class, args);
}
}
接收端添加配置
spring.cloud.stream.bindings.input.destination=stream-test
spring.rabbitmq.host=192.168.99.100
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
接收端创建一个类
package com.cloud.msuser.rabbit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;
@Component
public class TestStreamListener {
private static final Logger logger = LoggerFactory.getLogger(TestStreamListener.class);
@StreamListener(Sink.INPUT)
public void test(String message) {
logger.info("消费消息 ,message = {}", message);
}
}
接收端测试
-
启动用户微服务
-
查看ribbitmq管理界面
发送端添加依赖(课程微服务)
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
启动类添加注解
# 添加@EnableBinding注解
package com.cloud.msclass;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;
@EnableFeignClients //(defaultConfiguration = GlobalFeignClientConfiguration.class)
@SpringBootApplication
@EnableBinding(Source.class)
public class MsClassApplication {
public static void main(String[] args) {
SpringApplication.run(MsClassApplication.class, args);
}
/**
* spring web提供的轻量级http client
* @return
*/
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
添加配置
spring.cloud.stream.bindings.output.destination=stream-test
spring.rabbitmq.host=192.168.99.100
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
添加测试Controller
package com.cloud.msclass.controller;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestController {
@Autowired
private Source source;
@GetMapping("/test-stream")
public boolean testStream() {
String date = DateFormatUtils.ISO_DATETIME_FORMAT.format(new Date());
return this.source.output().send(MessageBuilder.withPayload("我是消息体" + date).build());
}
}
测试
启动课程微服务 访问地址 http://localhost:8010/test-stream
查看用户微服务控制台:可以查看到信息
Stream自定义接口实现消息收发
用户微服务
在用户微服务中添加如下接口
package com.cloud.msuser.rabbit;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
public interface MySink {
String MY_INPUT = "myInput";
@Input(MY_INPUT)
SubscribableChannel input();
}
启动类添加信息
@EnableBinding({ Sink.class, MySink.class })
添加配置
spring.cloud.stream.bindings.myInput.destination=stream-mytest
在测试类中添加方法
package com.cloud.msuser.rabbit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;
@Component
public class TestStreamListener {
private static final Logger logger = LoggerFactory.getLogger(TestStreamListener.class);
@StreamListener(Sink.INPUT)
public void test(String message) {
logger.info("消费消息 ,message = {}", message);
}
@StreamListener(MySink.MY_INPUT)
public void test2(String message) {
logger.info("自定义消费消息 ,message = {}", message);
}
}
课程微服务
在课程微服务中添加如下接口
package com.cloud.msclass.rabbit;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
public interface MySource {
String MY_OUTPUT = "myOutput";
@Output(MY_OUTPUT)
MessageChannel output();
}
启动类添加注解
@EnableBinding({ Source.class, MySource.class })
添加配置:
spring.cloud.stream.bindings.myOutput.destination=stream-mytest
在TestController中添加测试方法
package com.cloud.msclass.controller;
import java.util.Date;
import java.util.List;
import org.apache.commons.lang.time.DateFormatUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import com.cloud.msclass.rabbit.MySource;
@RestController
public class TestController {
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("/test-discovery")
public List<ServiceInstance> testDiscovery() {
// 到Consul上查询指定微服务的所有势力
return discoveryClient.getInstances("ms-user");
}
@Autowired
private Source source;
/*
* http://localhost:8010/test-stream
*/
@GetMapping("/test-stream")
public boolean testStream() {
String date = DateFormatUtils.ISO_DATETIME_FORMAT.format(new Date());
return this.source.output().send(MessageBuilder.withPayload("我是消息体" + date).build());
}
@Autowired
private MySource mysource;
/*
* http://localhost:8010/test-my-stream
*/
@GetMapping("/test-my-stream")
public boolean testMyStream() {
String date = DateFormatUtils.ISO_DATETIME_FORMAT.format(new Date());
return this.mysource.output().send(MessageBuilder.withPayload("我是自定义消息体" + date).build());
}
}
测试
访问地址http://localhost:8010/test-my-stream
查看用户微服务控制台可以看到输出消息
查看SInk(接收消息)和Source(发送消息)接口:
package org.springframework.cloud.stream.messaging;
import org.springframework.cloud.stream.annotation.Input;
import org.springframework.messaging.SubscribableChannel;
/**
* Bindable interface with one input channel.
*
* @author Dave Syer
* @author Marius Bogoevici
* @see org.springframework.cloud.stream.annotation.EnableBinding
*/
public interface Sink {
/**
* Input channel name.
*/
String INPUT = "input";
/**
* @return input channel.
*/
@Input(Sink.INPUT)
SubscribableChannel input();
}
package org.springframework.cloud.stream.messaging;
import org.springframework.cloud.stream.annotation.Output;
import org.springframework.messaging.MessageChannel;
/**
* Bindable interface with one output channel.
*
* @author Dave Syer
* @author Marius Bogoevici
* @see org.springframework.cloud.stream.annotation.EnableBinding
*/
public interface Source {
/**
* Name of the output channel.
*/
String OUTPUT = "output";
/**
* @return output channel
*/
@Output(Source.OUTPUT)
MessageChannel output();
}
除此之外还有一个Processor接口 继承了上面两个接口 既能发送消息 也能接收消息
package org.springframework.cloud.stream.messaging;
/**
* Bindable interface with one input and one output channel.
*
* @author Dave Syer
* @author Marius Bogoevici
* @see org.springframework.cloud.stream.annotation.EnableBinding
*/
public interface Processor extends Source, Sink {
}
错误处理
修改用户微服务代码:
package com.cloud.msuser.rabbit;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.ErrorMessage;
import org.springframework.stereotype.Component;
@Component
public class TestStreamListener {
private static final Logger logger = LoggerFactory.getLogger(TestStreamListener.class);
@StreamListener(Sink.INPUT)
public void test(String message) {
logger.info("消费消息 ,message = {}", message);
// 简单模拟一个异常
throw new RuntimeException("发生异常");
}
@StreamListener(MySink.MY_INPUT)
public void test2(String message) {
logger.info("自定义消费消息 ,message = {}", message);
}
// 全局处理异常
@StreamListener("errorChannel")
public void error(Message<?> message) {
ErrorMessage errorMessage = (ErrorMessage) message;
logger.warn("Handling ERROR : {}", errorMessage);
}
}
访问地址:http://localhost:8010/test-stream
查看控制台可查看到有异常消息
2020-02-24 12:06:40.787 INFO 12600 --- [4uUqo-pBOgJjA-1] c.c.msuser.rabbit.TestStreamListener : 消费消息 ,message = 我是消息体2020-02-24T12:06:40
2020-02-24 12:06:41.799 INFO 12600 --- [4uUqo-pBOgJjA-1] c.c.msuser.rabbit.TestStreamListener : 消费消息 ,message = 我是消息体2020-02-24T12:06:40
2020-02-24 12:06:43.800 INFO 12600 --- [4uUqo-pBOgJjA-1] c.c.msuser.rabbit.TestStreamListener : 消费消息 ,message = 我是消息体2020-02-24T12:06:40
2020-02-24 12:06:43.801 WARN 12600 --- [4uUqo-pBOgJjA-1] c.c.msuser.rabbit.TestStreamListener : Handling ERROR : ErrorMessage [payload=org.springframework.messaging.MessagingException: Exception thrown while invoking TestStreamListener#test[1 args]; nested exception is java.lang.RuntimeException: 发生异常, failedMessage=GenericMessage [payload=byte[34], headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedExchange=stream-test, amqp_deliveryTag=1, deliveryAttempt=3, amqp_consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA, amqp_redelivered=false, amqp_receivedRoutingKey=stream-test, amqp_timestamp=Mon Feb 24 12:06:40 CST 2020, amqp_messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, id=2b3cda7f-3fa4-5e74-ff44-3cf960cc12ac, amqp_consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, sourceData=(Body:'我是消息体2020-02-24T12:06:40' MessageProperties [headers={}, timestamp=Mon Feb 24 12:06:40 CST 2020, messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, contentType=application/json, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=stream-test, receivedRoutingKey=stream-test, deliveryTag=1, consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA]), contentType=application/json, timestamp=1582517200774}], headers={amqp_raw_message=(Body:'我是消息体2020-02-24T12:06:40' MessageProperties [headers={}, timestamp=Mon Feb 24 12:06:40 CST 2020, messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, contentType=application/json, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=stream-test, receivedRoutingKey=stream-test, deliveryTag=1, consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA]), id=72893c61-0c85-dbfb-2a8d-d5f41d8225e9, sourceData=(Body:'我是消息体2020-02-24T12:06:40' MessageProperties [headers={}, timestamp=Mon Feb 24 12:06:40 CST 2020, messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, contentType=application/json, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=stream-test, receivedRoutingKey=stream-test, deliveryTag=1, consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA]), timestamp=1582517203801}] for original GenericMessage [payload=byte[34], headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedExchange=stream-test, amqp_deliveryTag=1, deliveryAttempt=3, amqp_consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA, amqp_redelivered=false, amqp_receivedRoutingKey=stream-test, amqp_timestamp=Mon Feb 24 12:06:40 CST 2020, amqp_messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, id=2b3cda7f-3fa4-5e74-ff44-3cf960cc12ac, amqp_consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, sourceData=(Body:'我是消息体2020-02-24T12:06:40' MessageProperties [headers={}, timestamp=Mon Feb 24 12:06:40 CST 2020, messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, contentType=application/json, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=stream-test, receivedRoutingKey=stream-test, deliveryTag=1, consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA]), contentType=application/json, timestamp=1582517200774}]
2020-02-24 12:06:43.804 ERROR 12600 --- [4uUqo-pBOgJjA-1] o.s.integration.handler.LoggingHandler : org.springframework.messaging.MessagingException: Exception thrown while invoking TestStreamListener#test[1 args]; nested exception is java.lang.RuntimeException: 发生异常, failedMessage=GenericMessage [payload=byte[34], headers={amqp_receivedDeliveryMode=PERSISTENT, amqp_receivedExchange=stream-test, amqp_deliveryTag=1, deliveryAttempt=3, amqp_consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA, amqp_redelivered=false, amqp_receivedRoutingKey=stream-test, amqp_timestamp=Mon Feb 24 12:06:40 CST 2020, amqp_messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, id=2b3cda7f-3fa4-5e74-ff44-3cf960cc12ac, amqp_consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, sourceData=(Body:'我是消息体2020-02-24T12:06:40' MessageProperties [headers={}, timestamp=Mon Feb 24 12:06:40 CST 2020, messageId=861abc1f-930a-cd0a-03bb-c9f73db1c3ef, contentType=application/json, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=stream-test, receivedRoutingKey=stream-test, deliveryTag=1, consumerTag=amq.ctag-b1TpBoh1IeMvVbp5YMFP_A, consumerQueue=stream-test.anonymous.dsgs0yN2Q4uUqo-pBOgJjA]), contentType=application/json, timestamp=1582517200774}]
监控
http://localhost:8010/actuator/channels
消息的分发
现在如果将用户微服务部署两份的话,通过课程微服务发送消息,这个消息会被消费两次,也就出现了重复消费的情况。
在配置中添加信息:
spring.cloud.stream.bindings.input.group=g1
相同group下只有一个消费者去消费消息,负载均衡算法是轮询
如果有一个集群配置group为g1,另一个集群配置为g2,那么在g1和g2中都会挑选出一个服务进行消费,通过group可以灵活进行消息分发
消息内容的分发
某个topic下的内容随着版本的变迁不兼容了,我们希望对于不同版本的消息有不同的处理逻辑,需要对原有代码进行改造。首先修改消费者TestStreamListener(用户微服务)
@StreamListener(value = Sink.INPUT, condition = "headers['version']=='v1'")
public void testV1(String message) {
logger.info("消费消息V1 ,message = {}", message);
}
@StreamListener(value = Sink.INPUT, condition = "headers['version']=='v2'")
public void testV2(String message) {
logger.info("消费消息V2 ,message = {}", message);
}
改造生产者TestController(课程微服务)
/*
* http://localhost:8010/test-stream
*/
@GetMapping("/test-stream")
public boolean testStream() {
String date = DateFormatUtils.ISO_DATETIME_FORMAT.format(new Date());
return this.source.output().send(MessageBuilder.withPayload("我是消息体" + date)
.setHeader("version", "v2").build());
}
此时用户微服务只有testV2方法才会接收到消息了
实现业务
由于业务只使用一个topic,删除掉自定义的注解MySink/MySource,以及注解、配置等信息、删除测试类
# 取一个贴切的名称
spring.cloud.stream.bindings.output.destination=lesson-buy
创建DTO(课程微服务和用户微服务均需要)
package com.cloud.msclass.domain.dto;
import java.math.BigDecimal;
public class UserMoneyDTO {
private Integer userId;
private BigDecimal money;
private String event;
private String description;
public UserMoneyDTO() {
super();
}
public UserMoneyDTO(Integer userId, BigDecimal money, String event, String description) {
super();
this.userId = userId;
this.money = money;
this.event = event;
this.description = description;
}
// 省略setter getter
}
用户微服务
添加实体类
package com.cloud.msuser.domain.entity;
import java.math.BigDecimal;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;
@Table(name = "user_account_event_log")
@Entity
public class UserAccountEventLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY) // mysql的自增策略
private Integer id;
@Column
private Integer userId;
@Column
private BigDecimal money;
@Column
private String event;
@Column
private String description;
@Column
private Date createTime;
public UserAccountEventLog() {
super();
}
public UserAccountEventLog(Integer userId, BigDecimal money, String event, String description, Date createTime) {
super();
this.userId = userId;
this.money = money;
this.event = event;
this.description = description;
this.createTime = createTime;
}
public Integer getId() {
return id;
}
// 省略setter getter
}
添加数据层
package com.cloud.msuser.repository;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import com.cloud.msuser.domain.entity.UserAccountEventLog;
@Repository
public interface UserAccountEventLogRepository extends CrudRepository<UserAccountEventLog, Integer> {
}
在UserService中添加lessonBuy方法和事务注解
package com.cloud.msuser.service;
import java.math.BigDecimal;
import java.util.Date;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.cloud.msuser.domain.dto.UserMoneyDTO;
import com.cloud.msuser.domain.entity.User;
import com.cloud.msuser.domain.entity.UserAccountEventLog;
import com.cloud.msuser.repository.UserAccountEventLogRepository;
import com.cloud.msuser.repository.UserRepository;
@Transactional(rollbackFor = Exception.class)
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private UserAccountEventLogRepository userAccountEventLogRepository;
public User findById(Integer id) {
// 如果有就返回 否则抛出异常
return this.userRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("用户不存在"));
}
public void lessonBuy(UserMoneyDTO moneyDTO) {
Integer userId = moneyDTO.getUserId();
BigDecimal money = moneyDTO.getMoney(); // 使用的金额
// 2. 扣减余额
Optional<User> optionalUser = this.userRepository.findById(userId);
User user = optionalUser.orElseThrow(() -> new IllegalArgumentException("当前用户不逊在"));
user.setMoney(user.getMoney().subtract(money));
this.userRepository.save(user);
// 3. 记录日志 user_account_evnt_log
UserAccountEventLog log = new UserAccountEventLog(userId, money, moneyDTO.getEvent(), moneyDTO.getDescription(),
new Date());
userAccountEventLogRepository.save(log);
}
}
创建MQ消息接收类
package com.cloud.msuser.rabbit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;
import com.cloud.msuser.domain.dto.UserMoneyDTO;
import com.cloud.msuser.service.UserService;
@Component
public class LessonBuyStreamListener {
@Autowired
private UserService userService;
@StreamListener(Sink.INPUT)
public void lessonBuy(UserMoneyDTO moneyDTO) {
userService.lessonBuy(moneyDTO);
}
}
课程微服务
完善LessonService方法和添加事务注解
package com.cloud.msclass.service;
import java.math.BigDecimal;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.messaging.Source;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import com.cloud.msclass.domain.dto.UserDTO;
import com.cloud.msclass.domain.dto.UserMoneyDTO;
import com.cloud.msclass.domain.entity.Lesson;
import com.cloud.msclass.domain.entity.LessonUser;
import com.cloud.msclass.feign.MsUserFeignClient;
import com.cloud.msclass.repository.LessonRepository;
import com.cloud.msclass.repository.LessonUserRepository;
@Service
// 添加事务注解
@Transactional(rollbackFor = Exception.class)
public class LessonService {
@Autowired
private LessonRepository lessonRepository;
@Autowired
private LessonUserRepository lessonUserRepository;
@Autowired
private RestTemplate restTemplate;
// 引入MQ
@Autowired
private Source source;
@Autowired
private MsUserFeignClient msUserFeignClient;
public Lesson buyById(Integer id) {
// 1. 根据id查询lesson
Lesson lesson = this.lessonRepository.findById(id).orElseThrow(() -> new IllegalArgumentException("该课程不存在"));
// 2. 根据lesson.id查询user_lesson,那么直接返回lesson
LessonUser lessonUser = this.lessonUserRepository.findByLessonId(id);
if (lessonUser != null) {
return lesson;
}
// TODO 登录实现后需重构
// 3. 如果user_lesson==null && 用户的余额 > lesson.price 则购买成功
Integer userId = 1;
UserDTO userDTO = this.msUserFeignClient.findUserById(userId);
BigDecimal money = userDTO.getMoney().subtract(lesson.getPrice());
if (money.doubleValue() < 0) {
throw new IllegalArgumentException("余额不足");
}
// 购买逻辑 ... 1. 发送消息给用户微服务 让它扣减金额
String description = String.format("%s购买了id为%s的课程", userId, id);
this.source.output().send(
MessageBuilder.withPayload(new UserMoneyDTO(userId, lesson.getPrice(), "购买课程", description)).build());
// 2.向lesson_user表插入数据
LessonUser lu = new LessonUser();
lu.setLessonId(id);
lu.setUserId(userId);
this.lessonUserRepository.save(lu);
return lesson;
}
}
http://localhost:8010/lesssons/buy/1
测试结果
// 课程微服务日志
Hibernate: select lessonuser0_.lesson_id as lesson_i1_1_0_, lessonuser0_.user_id as user_id2_1_0_ from lesson_user lessonuser0_ where lessonuser0_.lesson_id=?
Hibernate: insert into lesson_user (user_id, lesson_id) values (?, ?)
// 用户微服务日志
Hibernate: select user0_.id as id1_0_0_, user0_.money as money2_0_0_, user0_.password as password3_0_0_, user0_.reg_time as reg_time4_0_0_, user0_.role as role5_0_0_, user0_.username as username6_0_0_ from user user0_ where user0_.id=?
Hibernate: select user0_.id as id1_0_0_, user0_.money as money2_0_0_, user0_.password as password3_0_0_, user0_.reg_time as reg_time4_0_0_, user0_.role as role5_0_0_, user0_.username as username6_0_0_ from user user0_ where user0_.id=?
Hibernate: insert into user_account_event_log (create_time, description, event, money, user_id) values (?, ?, ?, ?, ?)
Hibernate: update user set money=?, password=?, reg_time=?, role=?, username=? where id=?