项目监听服务搭建
最后一个服务是监听MQ进行处理的项目(消息)监听服务。这个服务其实是可以和其它服务进行合并的,但是为了清晰我们还是分开做了一个模块:
<?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.0http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>springcloud101</artifactId>
<groupId>me.josephzhu</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>springcloud101-projectservice-listener</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.7</version>
</dependency>
<dependency>
<groupId>me.josephzhu</groupId>
<artifactId>springcloud101-userservice-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>me.josephzhu</groupId>
<artifactId>springcloud101-projectservice-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>me.josephzhu</groupId>
<artifactId>springcloud101-investservice-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
引入了Stream相关依赖,去掉了数据访问相关依赖,因为这里我们只会调用外部服务,服务本身不会进行数据访问。
配置信息如下:
server:
port: 8764
spring:
application:
name: projectservice-listener
cloud:
stream:
bindings:
input:
destination: zhuye
zipkin:
base-url: http://localhost:9411
sleuth:
feign:
enabled: true
sampler:
probability: 1.0
feign:
hystrix:
enabled: true
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8865/eureka/
registry-fetch-interval-seconds: 5
management:
endpoints:
web:
exposure:
include: "*"
endpoint:
health:
show-details: always
唯一值得注意的是,这里我们定义了Spring Cloud Input绑定到也是之前定义的Output的那个交换机zhuye上面,实现了MQ发送接受数据连通。
下面我们定义了三个外部服务客户端(代码和其它地方使用的一模一样),投资服务:
package me.josephzhu.springcloud101.projectservice.listener;
import me.josephzhu.springcloud101.investservice.api.InvestService;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(value = "investservice")
public interface RemoteInvestService extends InvestService{
}
用户服务:
package me.josephzhu.springcloud101.projectservice.listener;
import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.userservice.api.User;
import me.josephzhu.springcloud101.userservice.api.UserService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@FeignClient(value = "userservice", fallback = RemoteUserService.Fallback.class)
public interface RemoteUserService extends UserService {
@Component
@Slf4j
class Fallbackimplements RemoteUserService{
@Override
public User getUser(long id) throws Exception {
log.warn("getUser fallback");
return null;
}
@Override
public BigDecimalconsumeMoney(long id, BigDecimal amount) throws Exception {
log.warn("consumeMoney fallback");
return null;
}
@Override
public BigDecimallendpayMoney(long investorId, long borrowerId, BigDecimal amount) throws Exception {
log.warn("lendpayMoney fallback");
return null;
}
}
}
项目服务:
package me.josephzhu.springcloud101.projectservice.listener;
import me.josephzhu.springcloud101.projectservice.api.Project;
import me.josephzhu.springcloud101.projectservice.api.ProjectService;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
@FeignClient(value = "projectservice", fallback = RemoteProjectService.Fallback.class)
public interface RemoteProjectService extends ProjectService{
@Component
class Fallbackimplements RemoteProjectService{
@Override
public ProjectgetProject(long id) throws Exception {
return null;
}
@Override
public BigDecimalgotInvested(long id, BigDecimal amount) throws Exception {
return null;
}
@Override
public BigDecimallendpay(long id) throws Exception {
return null;
}
}
}
监听程序实现如下:
package me.josephzhu.springcloud101.projectservice.listener;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import me.josephzhu.springcloud101.projectservice.api.Project;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.stream.annotation.EnableBinding;
import org.springframework.cloud.stream.annotation.StreamListener;
import org.springframework.cloud.stream.messaging.Sink;
import org.springframework.stereotype.Component;
@Component
@EnableBinding(Sink.class)
@Slf4j
public class ProjectServiceListener {
@Autowired
RemoteUserService remoteUserService;
@Autowired
RemoteProjectService remoteProjectService;
@Autowired
RemoteInvestService remoteInvestService;
static ObjectMapper objectMapper = new ObjectMapper();
@StreamListener(Sink.INPUT)
public void handleProject(Projectproject) {
try {
log.info("收到消息: " + project);
if (project.getStatus() == 2) {
remoteInvestService.getOrders(project.getId())
.forEach(invest-> {
try {
remoteUserService.lendpayMoney(invest.getInvestorId(), invest.getBorrowerId(),invest.getAmount());
} catch (Exceptionex) {
try {
log.error("处理放款的时候遇到异常:" + objectMapper.writeValueAsString(invest),ex);
} catch (JsonProcessingExceptione) {
}
}
});
remoteProjectService.lendpay(project.getId());
}
} catch (Exceptionex) {
log.error("处理消息出现异常",ex);
}
}
}
我们通过@StreamListener方便实现消息监听,在收听到Project消息(其实最标准的应该为MQ消息定义一个XXNotification的DTO,比如ProjectStatusChangedNotification,这里我们偷懒直接使用了Project这个DTO)后:
1. 判断项目状态是不是2募集完成,如果是的话
2. 首先,调用投资服务getOrders接口获取项目所有投资信息
3. 然后,逐一调用用户服务lendpayMoney接口为每一笔投资进行余额转移(把投资人冻结的钱解冻,转给借款人可用余额)
4. 最后,调用项目服务lendpay接口更新项目状态为放款完成
这里可以看到,虽然lendpay接口耗时很久(里面休眠5秒)但是由于处理是异步的,不会影响投资订单这个操作,这是通过MQ进行异步处理的应用点之一。
演示和测试
激动人心的时刻来了,我们来通过演示看一下我们这套Spring Cloud微服务体系的功能。
先启动Eureka,然后依次启动所有的基础服务,最后依次启动所有的业务服务。
全部启动后,访问一下http://localhost:8865/来查看Eureka注册中心:
这里可以看到所有服务已经注册在线:
1. 8866的Zuul
2. 8867的Tubine
3. 8761的用户服务
4. 8762的项目服务
5. 8763的投资服务
访问http://localhost:8761/getUser?id=1可以测试用户服务:
访问http://localhost:8762/getProject?id=2可以测试项目服务:
我们来初始化一下数据库,默认有一个项目信息:
还有两个投资人和一个借款人:
现在来通过网关访问http://localhost:8866/invest/createInvest投资服务(使用网关进行路由,我们配置的是匹配invest/**这个path路由到投资服务,直接访问服务的时候无需提供invest前缀)使用投资人1做一次投资:
在没有提供token的时候会出现错误,加上token后访问成功:
可以看到投资后投资人冻结账户为100,项目剩余金额为900,多了一条投资记录:
我们使用投资人1测试5次投资,使用投资人2测试5次投资,测试后可以看到项目状态变为了3放款完成:
数据库中有10条投资记录:
两个投资人的冻结余额都为0,可用余额分别少了500,借款人可用余额多了1000,说明放款成功了。
同时可以在ProjectListner的日志中看到收到消息的日志:
我们可以访问http://localhost:15672打开RabbitMQ都是管理台看一下我们那条消息的情况:
可以看到在队列中的确有一条消息先收到然后不久后(大概是6秒后)得到了ack处理完毕。队列绑定到了zhuye这个交换机上:
至此,我们已经演示了Zuul、Eureka和Stream,现在我们来看一下断路器功能。
我们首先访问http://localhost:8867/hystrix:
然后输入http://localhost:8867/turbine.stream(Turbine聚合监控数据流)进入监控面板:
多访问几次投资服务接口可以看到每一个服务方法的断路器情况以及三套服务断路器线程池的情况,我们接下去关闭用户服务,再多访问几次投资服务接口,可以看到getUser断路器打开(getUser方法有个红点):
同时在投资服务日志中可以看到断路器走了Fallback的用户服务:
最后,我们访问Zipkin来看一下服务链路监控的威力,访问http://localhost:9411/zipkin/然后点击按照最近排序可以看到有一条很长的链路:
点进去看看:
整个链路覆盖:
1、网关:
2、断路器以及同步服务调用
3、消息发送和接受的异步处理
整个过程一清二楚,只是这里没有Redis和数据库访问的信息,我们可以通过定义扩展实现,这里不展开阐述。还可以点击Zipkin的依赖链接分析服务之间的依赖关系:
点击每一个服务可以查看明细:
还记得我们引用了p6spy吗,我们来看一下投资服务的日志:
方括号中的几个数据分别是appname,traceId,spanId,exportable(是否发送到zipkin)。
随便复制一个traceId,粘贴到zipkin即可查看这个SQL的完整链路:
演示到此结束。
总结
这是一篇超长的文章,在本文中我们以一个实际的业务例子介绍演示了如下内容:
1. Eureka服务注册发现
2. Feign服务远程调用
3. Hystrix服务断路器
4. Turbine断路器监控聚合
5. Stream做异步处理
6. Sleuth和Zipkin服务调用链路监控
7. Zuul服务网关和自定义过滤器
8. JPA数据访问和Redisson分布式锁
虽然我们给出的是一个完整的业务例子,但是我们可以看到投资的时候三大服务是需要做事务处理的,这里因为是演示Spring Cloud,完全忽略了分布式事务处理,以后有机会会单独写文章来讨论这个事情。
总结一下我对Spring Cloud的看法:
1. 发展超快,感觉Spring Cloud总是会先用开源的东西先纳入体系然后慢慢推出自己的实现,Feign、Gateway就是这样的例子
2. 因为发展快,版本迭代快,所以网上的资料往往五花八门,各种配置不一定适用最新版本,还是看官方文档最好
3. 但是官方文档有的时候也不全面,这个时候只能自己阅读相关源码
4. 现在还不够成熟(可用,但用的不是最舒服,需要用好的话需要做很多定制),功能不是最丰富,属于凑活能用的阶段,照这个速度,1年后我们再看到时候可能就很爽了
5. 期待Spring Cloud在配置服务、网关服务、全链路监控、一体化的配置后台方面继续加强
6. 不管怎么说,如果只需要2小时就可以搭建一套微服务体系,具有服务发现+同步调用+异步调用+调用监控+熔断+网关的功能,还是很震撼的,小型创业项目用这套架构可以当天就起步项目
7. 社区还提供了一个Admin项目功能比较丰富,你可以尝试搭建https://github.com/codecentric/spring-boot-admin,这里就不演示搭建过程介绍具体功能了,请直接参见源码,启动后相关截图如下:
希望本文对你有用,完整代码见https://github.com/JosephZhu1983/SpringCloud101