09 消息驱动的微服务Spring Cloud Stream

基于MQ的通信流程

课程微服务-----------》MQ-----------》用户微服务

在LessonService类中考虑使用MQ来实现用户微服务的扣减金额服务,当然也可以使用feign来实现,实际项目中一定要考虑具体的场景。这里仅仅是为了讲解MQ。

MQ适用场景

  1. 异步处理(耗时不阻塞主流程的服务)

  2. 流量削峰填谷

  3. 解耦服务间的依赖关系

合理的使用MQ,提高服务的吞吐量,提高用户的体验

操作MQ的方式演进

原生API-》Spring Messaging(参考Spring Boot说明书)->Spring Integration(配置多) -> Spring Cloud Stream

Spring Cloud Stream是什么

一个用于构建消费驱动的微服务的框架,致力于简化MQ通信的服务
在这里插入图片描述
在这里插入图片描述

Spring Cloud Stream支持的MQ产品

  1. RabbitMQ (Spring Cloud Stream支持)
  2. Kafka(Spring Cloud Stream支持)
  3. Apache RocketMQ(Spring Cloud Alibaba支持)

Spring Cloud Stream编程模型:概念

  1. Destination Binder(目标绑定器):与消息中间件通信的组件
  2. Destination Bindings(目标绑定):Binding是连接应用程序跟消息中间件的桥梁,用于消息的消费和生产,由binder创建,有input binding和output binding
  3. 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);
	}
}

接收端测试

  1. 启动用户微服务

  2. 查看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=?
发布了21 篇原创文章 · 获赞 1 · 访问量 338

猜你喜欢

转载自blog.csdn.net/m0_37607945/article/details/104476636