
特点
- springboot2、springcloud
- 高可用注册中心nacos、nacos负载均衡算法
- 多实例服务:gateway、backend
- 熔断机制sentinel
- 服务链路追踪zipkin
- 服务监测actuator
- docker-stack一键启动mysql主备
- redis集群
- 高可用nginx
- docker-compose一键启动elk
- rabbitmq实现消息百分百投递成功、幂等性消费消息
- 统一异常处理、统一返回值、统一日志管理
- 基于jwt的登陆登出机制
- 基于注解的接口权限控制
- 实现自己的spring-boot-starter
- 设计模式的使用
- ......
下载&&运行
先启动nacos,再启动ci-backend、ci-gateway
一、项目骨架搭建
创建一个简单的maven工程作为我们的父项目,删除掉src等多余目录,只保留pom.xml,项目目录为:

<!-- 项目基础配置 -->
<groupId>com.cmsr</groupId>
<artifactId>ci-parent</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.6.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!-- 注册中心nacos -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<!--spring cloud版本管理-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.SR1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--spring cloud alibaba版本管理-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.1.0.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
然后创建一个父module:ci-backend,同样也是一个简单的maven项目,同样删除掉src等多余目录,只保留pom.xml
右键项目(如上图的ci-sample),New -> Module,填好ArtifactId(ci-backend)、Module name (ci-backend),点击Finish。
接着右键刚创建好的module,以同样的方法创建两个子module:ci-backend-api、ci-backend-service,此时的项目架构图如下:

创建子module的时候,ArtifactId和Module name都保持一致,分别都是ci-backend-api、ci-backend-service。ci-backend-api模块是后期各个微服务之间调用时使用的,当我们引入Feign的时候才会用到该模块。
现在我们将ci-backend-service模块改成springboot项目,首先添加一个启动类BackendLauncher.java
@SpringBootApplication
public class BackendLauncher {
public static void main(String[] args) {
SpringApplication.run(BackendLauncher.class, args);
}
}
接着添加配置文件application.yml
server:
port: 9000
spring:
application:
name: cmsr-backend
接着在pom.xml添加springmvc依赖,编写一个测试接口,测试我们的项目是否搭建成功
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
@RestController
@RequestMapping("/test")
public class TestController {
@GetMapping
public String test() {
return "hello springboot";
}
}
启动我们的项目,在页面上访问localhost:9000/test,如果页面返回hello springboot,则表示我们的项目启动成功了。
此时我们的项目结构是这样的:

启动项目的时候,如果控制台报了异常:java.lang.IllegalArgumentException: no server available at com.alibaba.nacos.client.naming.net.NamingProxy.reqAPI,这是因为我们本地还没有启动注册中心,下面我们就将ci-backend-service项目注册到nacos上。
启动nacos后,可以登陆nacos的管理界面看看:localhost:8848/nacos
nacos的管理界面的登陆名密码都是nacos
接着我们需要在ci-backend-service里整合nacos。整合nacos很简单,只需在application.yml里配置一下即可
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848
最后再次启动ci-backend-service,就会在nacos管理界面上看到我们注册的cmsr-backend服务了

二、搭建网关
网关是基于SpringCloud Gateway来搭建的。所有的请求都要先经过网关,然后由网关路由到各个微服务上。
首先在父模块下创建新的module:ci-gateway,然后在pom.xml里添加SpringCloud Gateway依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
接着添加项目的启动类和配置文件application.yml
@SpringBootApplication
public class GatewayLauncher {
public static void main(String[] args) {
SpringApplication.run(GatewayLauncher.class, args);
}
}
server:
port: 8000
spring:
application:
name: cmsr-gateway
cloud:
nacos:
discovery:
server-addr: localhost:8848
gateway:
discovery:
locator:
enabled: true # 让gateway通过服务发现组件找到其他的微服务
启动项目后,ci-gateway也会将自己注册到nacos上

此时项目的结构是这样的:

现在有了网关,我们就不用直接访问cmsr-backend了,而是通过访问网关,让网关路由到cmsr-backend上,而实现这一步只需在application.yml上配置一下即可
spring:
cloud:
gateway:
routes:
- id: cmsr-backend
uri: lb://cmsr-backend
predicates:
- Path=/backend/**
filters:
- StripPrefix=1
这段配置的意思是当访问以/backend打头的url都负载均衡路由到cmsr-backend服务上
配置好后重启一下ci-gateway,在浏览器上访问localhost:8000/backend/test时,页面也会返回hello springboot
三、ci-backend多实例的负载均衡
假设我们的ci-backend部署了多个实例,那么网关要怎么路由到哪个实例上呢?是轮训、随机还是什么?肯定不是的!假如一个实例部署在北京,一个实例部署在上海,上海的用户请求ci-backend服务不可能路由到北京的那个实例上吧,那得走多远啊!此时应该有一个负载均衡算法来帮网关决定应该路由到哪个实例上。我们可以借助nacos来实现。
nacos领域模型有集群的概念,我们把部署在上海的ci-gateway和ci-backend实例放到nacos的一个集群名为shanghai的集群里,把部署在北京的ci-gateway和ci-backend实例放到nacos的一个集群名为beijing的集群里。
当一个上海用户使用上海的ci-gateway发起请求,那么这个请求就优先被转发到nacos集群名为shanghai集群的ci-backend上,当该集群里没有ci-backend服务的时候,才会跨集群调用nacos里集群名为beijing集群的ci-backend。
当集群名为shanghai集群有多个ci-backend,那么可以为每个ci-backend设置一个权重,权重高的部署在好的服务器上,权重低的部署在相对较好的服务器上。
好了,知道了负载均衡的思路后,让我们来coding这个负载均衡算法吧!
在ci-gateway项目下创建一个包:loadBalanced,在该目录下实现负载均衡算法
/**
* 基于nacos同一集群下的权重的负载均衡算法
*/
public class NacosSameClusterWeightRule extends AbstractLoadBalancerRule {
@Autowired
private NacosDiscoveryProperties nacosDiscoveryProperties;
private static final Logger log = LoggerFactory.getLogger(NacosSameClusterWeightRule.class);
@Override
public void initWithNiwsConfig(IClientConfig iClientConfig) {
}
@Override
public Server choose(Object key) {
try {
// 获取配置文件中nacos的集群名称
String clusterName = nacosDiscoveryProperties.getClusterName();
BaseLoadBalancer baseLoadBalancer = (BaseLoadBalancer) this.getLoadBalancer();
// 获取想要请求的微服务名称
String name = baseLoadBalancer.getName();
NamingService namingService = nacosDiscoveryProperties.namingServiceInstance();
// 获取想要请求服务的所有实例
List<Instance> instances = namingService.selectInstances(name, true);
if (CollectionUtils.isEmpty(instances)) {
return null;
}
// 获取指定集群下的所有实例
List<Instance> sameClusterInstall = instances.stream().filter(instance -> {
return Objects.equals(instance.getClusterName(), clusterName);
}).collect(Collectors.toList());
List<Instance> instancesToBeChosen;
if (CollectionUtils.isEmpty(sameClusterInstall)) {
instancesToBeChosen = instances;
log.warn("发生跨集群调用, name = {}, clusterName = {}", clusterName, instances);
} else {
instancesToBeChosen = sameClusterInstall;
}
Instance instance = ExtendBalancer.getHostByRandomWeight2(instancesToBeChosen);
log.info("选择的实例是:port = {}, instance = {}", instance.getPort(), instance);
return new NacosServer(instance);
} catch (NacosException e) {
log.error("异常:", e);
return null;
}
}
}
class ExtendBalancer extends Balancer {
public static Instance getHostByRandomWeight2(List<Instance> hosts) {
return getHostByRandomWeight(hosts);
}
}
接着配置该负载均衡规则
@Configuration
public class RibbonConfig {
@Bean
public IRule ribbonRule() {
return new NacosSameClusterWeightRule();
}
}
@Configuration
@RibbonClients(defaultConfiguration = RibbonConfig.class)
public class BackendRibbonConfig {
}
在ci-gateway项目的application.yml配置自己是属于哪个集群
spring:
cloud:
nacos:
discovery:
cluster-name: shanghai
在ci-backend-service项目的application.yml配置自己是属于哪个集群和权重
spring:
cloud:
nacos:
discovery:
cluster-name: shanghai
weight: 10
启动ci-gateway和两个ci-backend-service来验证我们的负载均衡算法是否生效
idea启动多个实例方法,如下如,

然后然后勾选右上角的Allow parallel run后,修改ci-backend-service的端口号和nacos的权重,这样就可以启动两个不同的实例了
启动完成之后,我们去nacos的管理界面看下服务列表,可以发现有两个ci-backend-service实例

点击详情进去,就会发现有一个集群名为shanghai的集群,里面有个两个ci-backend-service实例,权重分别是在配置文件里配置的

接着我们在页面上请求多次localhost:8000/backend/test,然后对比下idea控制台上输出的日志,是否权重高的实例被访问的次数多于权重低的实例。
大家也可以启动多个ci-backend-service实例,但是集群名称不是shanghai,看看idea控制台会打印什么日志。
四、 搭建nacos集群
上面说的shanghai集群不是nacos集群,而是nacos的领域模型,这点要注意。
ci-gateway和ci-backend-service服务都需要注册到nacos上,当单机的nacos挂了那么所有的服务都不可用了,所以我们需要一个nacos集群。参考官方文档
首先我们需要一个mysql数据库,这里大家就自行安装一下吧
然后在nacos下载包找到 /nacos/conf/nacos-mysql.sql,将其导入到数据库中
同时在 /nacos/conf/application.properties 添加如下内容:
# 表明用MySQL作为后端存储
spring.datasource.platform=mysql
# 有几个数据库实例
db.num=1
# mysql实例的地址
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true
db.user=root
db.password=123456
然后将下载的nacos包复制三份,分别将/nacos/conf/application.properties 里面server.port=8848分别改成
server.port=8848
server.port=8849
server.port=8850
分别启动这三个nacos,即可组成集群
sh /nacos/bin/start.sh
修改ci-gateway和ci-backend-service配置文件,支持nacos集群
spring:
cloud:
nacos:
discovery:
server-addr: localhost:8848,localhost:8849,localhost:8850
我们可以手动关闭一个nacos,然后去访问接口,如果接口还能正常返回,表示我们的应用服务已经支持nacos集群了。
五、实现高可用网关ci-gateway
上面我们已经实现了服务ci-backend和nacos的高可用方案,而网关是所有流量的入口,所以我们也需要启动多个网关实例,然后通过nginx反向代理实现网关的高可用。
首先通过idea启动三个ci-gateway实例,端口分别是:8000、8001、8002
然后启动nginx,点我下载nginx
window操作系统直接双击nginx.exe即可;如果是linux或者mac系统的话可以参看我之前的文章:nginx-超详细配置及其优化
在nginx.conf文件的http段里添加一个server和upstream来实现反向代理
http {
server {
listen 7000;
server_name localhost;
location / {
proxy_pass http://gateway;
}
}
upstream gateway {
#网关实例地址
server 127.0.0.1:8000 weight=2;
server 127.0.0.1:8001 weight=2;
server 127.0.0.1:8002 weight=2;
}
}
这段配置的意思是:当我们访问localhost:7000/backend/test的时候,nginx会替换成http://gateway/backend/test,而gateway又被换成127.0.0.1:8000或127.0.0.1:8001再或者127.0.0.1:8002,最终结果请求都会路由到ci-backend-service上。如下图所示:

到这里我们通过nginx实现了网关ci-gateway的高可用。
六、高可用nginx
上面实现了网关的高可用,但是我们的nginx是单机的,也不能满足上生产环境的条件,所以我们需要一个高可用的nginx方案,我们借助keepalive封装好的VRRP来实现。
到此,项目从nginx到gateway到nacos到各个微服务都是高可用了!下面我们就一步一步把微服务ci-backend-service完善起来吧。
七、全局统一返回值
全局统一的配置需要写到一块,这样其他模块无需再重复配置,所以我们需要创建一个ci-common模块。然后在该模块coding我们的全局统一代码吧
首先添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
接着编写统一返回数据的结构体CommonResponse
public final class CommonResponse<T> {
private int code;
private String msg;
private T data;
public static CommonResponse success() {
return new CommonResponse(200, "ok");
}
public static <T> CommonResponse<T> success(T result) {
return new CommonResponse<>(200, "ok", result);
}
private CommonResponse(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
private CommonResponse(int errCode, String msg) {
this.code = errCode;
this.msg = msg;
this.data = null;
}
// getter and setter
}
接着需要将业务的返回值拦截,封装成CommonResponse返回给前端
@RestControllerAdvice
public class CommonResponseAdvice implements ResponseBodyAdvice<Object> {
// 所有的返回值都要封装成CommonResponse
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
// 将返回值封装成CommonResponse的具体逻辑
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest,
ServerHttpResponse serverHttpResponse) {
CommonResponse response = CommonResponse.success();
if (null == o) {
return response;
} else if (o instanceof byte[]){
return o;
} else if (o instanceof CommonResponse) {
response = (CommonResponse) o;
} else if (o instanceof String) {
response.setData(o);
return JSON.toJSONString(response);
} else {
response.setData(o);
}
return response;
}
}
然后在ci-backend-service添加ci-common的依赖,因为ci-common已经有spring-boot-starter-web的依赖了,如果ci-backend-service也有其依赖,可以把其依赖删掉,保持代码简洁
<dependency>
<groupId>com.cmsr</groupId>
<artifactId>ci-common</artifactId>
<version>1.0.0</version>
</dependency>
再然后在ci-backend-service的启动类上修改下配置,让其能扫描到ci-common下的组件
@SpringBootApplication(scanBasePackages = "com.cmsr")
最后重启下ci-backend验证下结果:

如果我们需要一些特定的接口不需要封装成CommonResponse,我们可以写一个注解,如果该接口上添加上了该注解,我们就忽略掉,具体实现如下:
/**
* 忽略对返回值的封装
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreCommonResponseAdvice {
}
最后修改下CommonResponseAdvice的supports方法
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
if (methodParameter.getDeclaringClass().isAnnotationPresent(IgnoreCommonResponseAdvice.class)) {
return false;
}
return !Objects.requireNonNull(methodParameter.getMethod()).isAnnotationPresent(IgnoreCommonResponseAdvice.class);
}
然后在我们不需要返回成CommonResponse格式的接口上加上该注解即可。
八、统一全局异常
在ci-common模块添加GlobalExceptionAdvice.java
@RestControllerAdvice
public class GlobalExceptionAdvice {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionAdvice.class);
/**
* request body为空时的异常处理
*/
@ExceptionHandler(value = HttpMessageNotReadableException.class)
public ResponseEntity<?> handleBusinessException(HttpMessageNotReadableException ex) {
return new ResponseEntity<>(
new CommonResponse<>(9999, "请求参数不能为空或格式化错误", null), HttpStatus.OK);
}
/**
* 统一异常处理
*/
@ExceptionHandler(value = Exception.class)
public ResponseEntity<?> handleException(Exception ex) {
log.error("统一异常处理切面捕获异常:", ex);
return new ResponseEntity<>(
new CommonResponse<>(500, "服务异常,请稍后再试", null), HttpStatus.OK);
}
}
后期各个微服务的自定义业务异常也可以定义在ci-common里,然后完善下GlobalExceptionAdvice即可