广告系统开发-基础准备

相关基础

Maven

坐标

  • groupId: 工程组的标识。
  • artifactId: 工程的标识。
  • version: 工程版本号。

相关命令

  • mvn -v 查看maven版本
  • mvn compile 编译,将java源文件编译成class文件
  • mvn test 执行test目录下的测试用例
  • mvn package 打包,将java工程打成jar包
  • mvn clean 清理环境,清除target文件夹

相关特性

  • 传递依赖 项目引用了一个jar包,而该jar包又引用了其他jar包,maven会把直接引用和间接引用的jar包都下载到本地。
  • 排除依赖:只想下载直接引用的jar包 (需要给出排除的jar包)
1
2
3
4
<exclusions>
<groupId>xxxgroupId>
<artifactId>xxxartifactId>
exclusions>
  • 依赖冲突 项目中多个jar同时引用了相同的jar时,会产生依赖冲突。有以下两种避免冲突的策略:
    • 短路优先 引用路径短的优先
    • 声明优先 引用路径相同时,在pom.xml文件中谁先被声明,就使用谁。

多模块项目/聚合

  • 父模块pom文件的配置:packing类型必须是pom

    1
    2
    3
    <groupId>xxxgroupId>
    <artifactId>xxxartifactId>
    <packaging>pompackaging>
  • 聚合子模块:使用modules标签

    1
    2
    3
    4
    5
    <modules>
    <module>xxxmodule>
    <module>xxxmodule>
    <module>xxxmodule>
    modules>
  • 父模块统一管理依赖包: 使用dependencyManagement标签

  • 子模块需要在pom文件中声明父模块: 使用parent模块

项目右侧将maven的pom文件导入,重新reimport,便可以跳转类。

EurekaServer的开发

单节点EurekaServer

1
2
3
4
5
6
7
8
9
10
//两个注解表明是EurekaServer,是springboot应用启动程序
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {

public static void main(String[] args) {

SpringApplication.run(EurekaApplication.class, args);
}
}

添加配置文件application.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
spring:
application:
name: ad-eureka

server:
port: 8000

eureka:
instance:
hostname: localhost
client:
#是否同步,表示自己端就是注册中心,维护服务实例,并不需要去检索服务。
fetch-registry: false
#是否注册,表示不向注册中心注册自己
register-with-eureka: false
#指出主机地址和服务器端口
service-url:
defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

多节点EurekaServer

修改配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
spring:
application:
name: ad-eureka
#使用profiles文件去启动服务
profiles: server1
server:
port: 8000
eureka:
#实例信息,这里要修改hosts文件,相同ip地址会出现问题
instance:
hostname: server1
prefer-ip-address: false
client:
service-url:
#要指向另外两个节点
defaultZone: http://server2:8001/eureka/,http://server3:8002/eureka/

---
spring:
application:
name: ad-eureka
profiles: server2
server:
port: 8001
eureka:
instance:
hostname: server2
prefer-ip-address: false
client:
service-url:
defaultZone: http://server1:8000/eureka/,http://server3:8002/eureka/

---
spring:
application:
name: ad-eureka
profiles: server3
server:
port: 8002
eureka:
instance:
hostname: server3
prefer-ip-address: false
client:
service-url:
defaultZone: http://server1:8000/eureka/,http://server2:8001/eureka/

修改hosts文件

1
2
3
4

127.0.0.1 server1
127.0.0.1 server2
127.0.0.1 server3

Eureka相关内容

核心功能

  • Service Registry(服务注册)
  • Service Discovery(服务发现)

基本架构

Eureka 由三个角色组成:

  • Eureka Server(这一章实现的功能),提供服务注册与发现
  • Service Provider,服务提供方,将自身服务注册到 Eureka Server 上,从而让 Eureka Server 持有服务的元信息,让其他的服务消费方能够找到当前服务
  • Service Consumer,服务消费方,从 Eureka Server 上获取注册服务列表,从而能够消费服务
  • Service Provider/Consumer 相对于 Server,都叫做 Eureka Client

高可用

问题说明:单节点的 Eureka Server 虽然能够实现基础功能,但是存在单点故障的问题,不能实现高可用。因为 Eureka Server 中存储了整个系统中所有的微服务的元数据信息,单节点一旦挂了,所有的服务信息都会丢失,造成整个系统的瘫痪。

解决办法:搭建 Eureka Server 集群,让各个 Server 节点之间互相注册,从而实现微服务元数据的复制/备份,即使单个节点失效,其他的 Server 节点仍可以继续提供服务

元信息

  • Eureka Server中维护了系统中服务的,其中有两种:标准元数据和自定义元数据。
    标准元数据:主机名、IP地址、端口号、状态页和健康检查等信息,这些信息都会被发布在服务注册表中,用于服务之间的调用。
    自定义元数据:可以使用eureka.instance.metadata-map配置,这些元数据可以在远程客户端中访问,但是一般不改变客户端行为,除非客户端知道该元数据的含义。

如果我们在启动 Client(简单认为就是一个微服务)的时候,如果此时配置了错误的 Eureka Server 发现地址,或 Eureka Server 没有启动,那么,你会发现,Client 可以正常启动,但是不能完成注册。会抛出如下异常信息。

1
2
3
4
com.netflix.discovery.shared.transport.TransportException: Cannot execute request on any known server
......
at com.netflix.discovery.DiscoveryClient.register(DiscoveryClient.java:829) ~[eureka-client-1.9.2.jar:1.9.2]
......

这里的异常信息,其实就告诉了我们,Client 向 Eureka Server 发起注册的过程。我们看一看 DiscoveryClient.register 里面做了什么

源码路径:eureka-client-1.9.2-sources.jar!/com/netflix/discovery/DiscoveryClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* 通过 HTTP (eurekaTransport) 请求将 Eureka Client 注册到 Eureka Server 上
*/
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse httpResponse;
try {
// 很显然,这里的 instanceInfo 就是 Client 的元数据
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == 204;
}

instanceInfo 就是 com.netflix.appinfo.InstanceInfo 的对象实例,它被称作是 “应用实例信息”。所以,这个对象中包含的属性,最终会投递到 Eureka Server 中,也就是说,Eureka Server 维护的元信息就是 InstanceInfo 。

元信息的保存

  • 其中,涉及到元信息的保存,可能是Eureka Server内部维护着一张表来记录这些元信息,Eureka服务器没有后端存储,但注册表中的服务实例必须发送心跳信号以保持其注册是最新的,所以这可能使内存中完成。客户端可能还拥有一个eureka注册的内存缓存,这样,client不必为每个服务请求都去注册表。

Client 向 Server 发起注册(Http 请求),那么Server 必须对外提供这个接口才可以。经过追踪 Server 端的源码(eureka-core-1.9.2-sources.jar),很容易可以找到 源码路径:com/netflix/eureka/resources/ApplicationResource.java,其中 addInstance 方法即实现了服务注册的功能(Server 端)。源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
@HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
// 校验 instanceinfo 中是否包含必须的属性
if (isBlank(info.getId())) {
return Response.status(400).entity("Missing instanceId").build();
} else if (isBlank(info.getHostName())) {
return Response.status(400).entity("Missing hostname").build();
}
......

// 注册信息校验
......

// 通过 PeerAwareInstanceRegistry 的 register 方法完成 Client 的注册
registry.register(info, "true".equals(isReplication));
return Response.status(204).build();
}

我们再去看 PeerAwareInstanceRegistry 的实现类:org.springframework.cloud.netflix.eureka.server.InstanceRegistry 的 register 方法的实现:

1
2
3
4
5
6
public void register(final InstanceInfo info, final boolean isReplication) {
//传播 Client 的注册消息
handleRegistration(info, resolveInstanceLeaseDuration(info), isReplication);
//调用父类 com.netflix.eureka.registry.AbstractInstanceRegistry 的 register 方法
super.register(info, isReplication);
}

继续看 AbstractInstanceRegistry 的 register 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
try {
read.lock();
// registry 存储了注册信息, 它是一个两层的 Map 结构
// ConcurrentHashMap>> registry
Map> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap> gNewMap = new ConcurrentHashMap>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease existingLease = gMap.get(registrant.getId());
......
Lease lease = new Lease(registrant, leaseDuration);
if (existingLease != null) {
lease.setServiceUpTimestamp(existingLease.getServiceUpTimestamp());
}
gMap.put(registrant.getId(), lease);
......
} finally {
read.unlock();
}
}

InstanceInfo 中的元数据信息存储在一个 ConcurrentHashMap 对象中。Eureka Server 使用了两层 Map 结构做存储,第一层的 key 存储服务名:InstanceInfo 中的 appName 属性,第二层 key 存储实例名:InstanceInfo 中的 instanceId 属性。

微服务架构以及网关介绍

微服务架构的两种方式:

  • 点对点的方式:服务之间直接调用,每个微服务都开放Rest API,并调用其他微服务的接口。
  • API-网关方式:业务接口通过API网关暴露,是所有客户端接口的唯一入口,微服务之间的通信也通过API网关。

Zuul 的生命周期:

Zuul 提供了服务网关的功能,可以实现负载均衡反向代理动态路由请求转发等功能。Zuul 大部分功能都是通过过滤器实现的,Zuul 中定义了四种标准的过滤器类型,同时,还支持自定义过滤器。

  • Pre filters:在请求路由之前被调用,实现身份验证,在集群中选择请求的微服务。
  • Routing filters: 请求路由到微服务
  • Post filters:响应http头,收集统一信息和指标。将响应微服务发送给客户端。在 route 和 error 过滤器之后被调用
  • Error filters: 处理请求时发生错误时被调用。

网关启动程序的开发

1
2
3
4
5
6
7
8
9
10
11
//开启网关代理
@EnableZuulProxy
//注意此处是SpringCloudApplication,而不是SpringBootApplication
@SpringCloudApplication
public class ZuulGatewayApplication {

public static void main(String[] args) {

SpringApplication.run(ZuulGatewayApplication.class, args);
}
}

编写配置文件

1
2
3
4
5
6
7
8
9
server:
port: 9000
spring:
application:
name: ad-gateway
eureka:
client:
service-url:
defaultZone: http://server1:8000/eureka/

自定义网关过滤器

实现打印访问日志,记录访问的延迟,从请求到返回经历了多少时间的功能。

编写Pre filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

@Slf4j
//当作组件进行注入
@Component
public class PreRequestFilter extends ZuulFilter {
//定义过滤器类型
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
//定义执行顺序,数字越小,执行顺序越高
@Override
public int filterOrder() {
return 0;
}
//验证
@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {
//记录当前时间,请求上下文会一直传递下去
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());

return null;
}
}

编写Post filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
* Created by Qinyi.
*/
@Slf4j
@Component
public class AccessLogFilter extends ZuulFilter {

@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}

@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}

@Override
public boolean shouldFilter() {
return true;
}

@Override
public Object run() throws ZuulException {

RequestContext context = RequestContext.getCurrentContext();
//取出当前请求的uri
HttpServletRequest request = context.getRequest();
Long startTime = (Long) context.get("startTime");
String uri = request.getRequestURI();
long duration = System.currentTimeMillis() - startTime;

log.info("uri: " + uri + ", duration: " + duration / 100 + "ms");

return null;
}
}
0%