微服务是一种架构方式,最终肯定需要技术架构去实施。微服务的实现方式很多,但是最火的莫过于Spring Cloud了。
SpringCloud是Spring旗下的项目之一,官网地址:http://projects.spring.io/spring-cloud/
Spring最擅长的就是集成,把世界上最好的框架拿过来,集成到自己的项目中。
SpringCloud也是一样,它将现在非常流行的一些技术整合到一起,实现了诸如:配置管理,服务发现,智能路由,负载均衡,熔断器,控制总线,集群状态等等功能。其主要涉及的组件包括:
netflix
以上只是其中一部分。接下来的就通过案例来学习这几个部分。
使用Spring提供的RestTemplate来进行远程调用。服务提供者:user-service,服务消费者:user-consumer。页面访问user-consumer工程,user-consumer远程调用user-service工程,得到结果返回给浏览器。
服务提供者:user-service,可以理解为搭建了一个SSM项目,提供了根据id查询用户的功能。
为了便于包管理,首先创建父工程(注意:是创建project)spring-cloud-demo,pom.xml内容如下:
pom
org.springframework.boot
spring-boot-starter-parent
2.0.4.RELEASE
UTF-8
UTF-8
1.8
Finchley.SR2
2.0.3
5.1.32
org.springframework.cloud
spring-cloud-dependencies
${spring-cloud.version}
pom
import
tk.mybatis
mapper-spring-boot-starter
${mapper.starter.version}
mysql
mysql-connector-java
${mysql.version}
org.projectlombok
lombok
org.springframework.boot
spring-boot-maven-plugin
在spring-cloud-demo上创建moduel工程user-service
1、导包
org.springframework.boot
spring-boot-starter-web
mysql
mysql-connector-java
tk.mybatis
mapper-spring-boot-starter
2、编写主配置文件application.yml
server:
port: 8081
#配置连接池DataSource
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/e3mall_32
username: root
password: 123456
application:
name: user-service
#mybatis相关配置
mybatis:
type-aliases-package: com.scu.pojo
3、启动类,pojo,mapper,service,web
启动类
@SpringBootApplication
@MapperScan("com.scu.mapper")
public class UserApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(UserApplicationStarter.class);
}
}
pojo
@Data
@Table(name="tb_user")
public class User {
@Id
@KeySql(useGeneratedKeys=true)
private Long id;
private String username;
private String password;
private String phone;
private String email;
private Date created;
private Date updated;
@Transient
private String note;
}
mapper
public interface UserMapper extends Mapper{
}
service
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public User queryUser(Long id){
return userMapper.selectByPrimaryKey(id);
}
}
web
@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("{id}")
public User queryUser(@PathVariable("id") long id) {
return userService.queryUser(id);
}
}
在spring-cloud-demo上创建moduel工程user-consumer
1、导包
org.springframework.boot
spring-boot-starter-web
2、编写配置文件application.yml
server:
port: 8082
3、编写启动类,pojo,web
启动类
@SpringBootApplication
public class ConsumerApplicationStarter {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplicationStarter.class);
}
}
pojo
@Data
public class User {
private Long id;
private String username;
private String password;
private String phone;
private String email;
private Date created;
private Date updated;
private String note;
}
web
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id) {
String url = "http://localhost:8080/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
文档结构如下
浏览器访问结果如下
那么这个小案例就做完了,简单回顾一下,刚才我们写了什么:
存在什么问题?
在consumer中,我们把url地址硬编码到了代码中,不方便后期维护
consumer需要记忆user-service的地址,如果出现变更,可能得不到通知,地址将失效
consumer不清楚user-service的状态,服务宕机也不知道
user-service只有1台服务,不具备高可用性
即便user-service形成集群,consumer还需自己实现负载均衡
其实上面说的问题,概括一下就是分布式服务必然要面临的问题:
服务管理
如何自动注册和发现
如何实现状态监管
如何实现动态路由
服务如何实现负载均衡
服务如何解决容灾问题
服务如何实现统一配置
以上的问题,我们都将在SpringCloud中得到答案。
首先我们来解决第一问题,服务的管理。
Eureka就好比是滴滴,负责管理、记录服务提供者的信息。服务调用者无需自己寻找服务,而是把自己的需求告诉Eureka,然后Eureka会把符合你需求的服务告诉你。
同时,服务提供方与Eureka之间通过“心跳”机制进行监控,当某个服务提供方出现问题,Eureka自然会把它从服务列表中剔除。
这就实现了服务的自动注册、发现、状态监控。
还是使用上面的案例,只是这时候,让服务注册到Eureka,消费者来进行调用,而不是消费者直接从服务提供方调用服务,让Eureka成为了中间平台。
1、创建Eureka-Server模块
2、导包
org.springframework.cloud
spring-cloud-starter-netflix-eureka-server
3、编写启动类
@SpringBootApplication
@EnableEurekaServer //启用Eureka服务
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class);
}
}
4、主配置文件application.yml
server:
port: 10001
spring:
application:
name: eureka-server #给应用取名
eureka:
client:
register-with-eureka: false #是否注册自己的信息到EurekaServer,默认是true
service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
defaultZone: http://127.0.0.1:${server.port}/eureka
注意,eureka既是服务端又是客户端,集群的时候每个客户端用来将自己注册到其它eureka中去,这里只有一个eureka,启动会报错,它会自己注册自己默认的端口是8761,看源码,首先会put进去
到这里知道格式怎么写了。
但是添加了 register-with-eureka: false后就不注册自己(其实是自己也发现不了自己差不多意思),但是不能被其他Eureka发现。这个时候启动初始可能会报错,但是之后就不再报错了。一般这个默认的true也不需要去修改了,只要自己注册了自己也不会报错。
之后还需要修改user-service和user-consumer两个应用的一些配置。
修改user-service工程
1、添加eureka相关包
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
2、在启动器上添加@EnableDiscoveryClient注解开启Eureka客户端功能
@EnableDiscoveryClient
@SpringBootApplication
@MapperScan("com.scu.mapper")
public class UserApplicationStarter {
public static void main(String[] args) {
SpringApplication.run(UserApplicationStarter.class);
}
}
3、修改配置文件
添加应用名,进行注册,完整配置如下
server:
port: 8081
#配置连接池DataSource
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/e3mall_32
username: root
password: 123456
application:
name: user-service
#mybatis相关配置
mybatis:
type-aliases-package: com.scu.pojo
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka #注册到eureka
instance:
prefer-ip-address: true #当调用getHostname获取实例的hostname时,返回ip而不是host名称
ip-address: 127.0.0.1 # 指定自己的ip信息,不指定的话会自己寻找
修改user-consumer工程
1、添加eureka相关包
org.springframework.cloud
spring-cloud-starter-netflix-eureka-client
为什么还是客户端依赖?因为对于eureka来说,不管是服务提供者还是服务消费者都是它的客户端。
2、修改启动类添加@EnableDiscoveryClient注解
@EnableDiscoveryClient
@SpringBootApplication
public class ConsumerApplicationStarter {
@Bean
public RestTemplate getRestTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(ConsumerApplicationStarter.class);
}
}
3、修改配置文件
添加应用名,进行注册,完整配置如下
server:
port: 8082
spring:
application:
name: user-consumer
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka
instance:
prefer-ip-address: true
ip-address: 127.0.0.1
4、修改Controller,用DiscoveryClient类的方法,根据服务名称,获取服务实例
@RestController
@RequestMapping("consumer")
public class ConsumerController {
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient discoveryClient;
@GetMapping("{id}")
public User queryById(@PathVariable("id") Long id) {
//根据服务id获取实例 实际是双重map 该方法得到最终的values
List instances = discoveryClient.getInstances("user-service");
//从实例当中取出ip和端口
ServiceInstance instance = instances.get(0);
String url = "http://"+instance.getHost()+":"+instance.getPort()+"/user/"+id;
User user = restTemplate.getForObject(url, User.class);
return user;
}
}
页面进行访问
注册了两个应用,没问题。
访问也没问题,说明远程调用没问题。
修改eureka-server的配置文件,注释掉register-with-eureka: false,集群的时候需要是true,否则其他eurekaServer发现不了。看文档解释如下
/**
* Indicates whether or not this instance should register its information with eureka
* server for discovery by others.
*
* In some cases, you do not want your instances to be discovered whereas you just
* want do discover other instances.
*/
两次运行的配置文件如下
server:
port: 10001
spring:
application:
name: eureka-server #给应用取名
eureka:
client:
#register-with-eureka: false #注意集群的时候需要是true,否则其他eurekaServer发现不了
service-url:
defaultZone: http://127.0.0.1:10002/eureka
第二次运行的配置文件
server:
port: 10002
spring:
application:
name: eureka-server #给应用取名
eureka:
client:
#register-with-eureka: false #注意集群的时候需要是true,否则其他eurekaServer发现不了。同时这样就会吧自己注册到注册中心了。
service-url:
defaultZone: http://127.0.0.1:10001/eureka
弄两台,先运行一台,再修改配置文件,接着直接复制修改即可
然后改下名字,EurekaServerApplication2
然后运行,页面查看
既然有了两个EurekaServer,那么服务提供者user-service和服务发现者user-consumer也需要注册两个EurekaServer地址。两个配置文件中的修改如下
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10001/eureka,http://127.0.0.1:10002/eureka
服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。
服务注册
服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-erueka=true
参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。第一层Map的Key就是服务名称,第二层Map的key是服务的实例id。
服务续约
在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);
有两个重要参数可以修改服务续约的行为:
```yaml
eureka:
instance:
lease-expiration-duration-in-seconds: 90
lease-renewal-interval-in-seconds: 30
```
也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。
但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。
eureka:
instance:
lease-expiration-duration-in-seconds: 10 # 10秒即过期
lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
获取服务列表
当服务消费者启动是,会检测eureka.client.fetch-registry=true
参数的值,如果为true,则会从Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒
会重新获取并更新数据。我们可以通过下面的参数来修改:
```yaml
eureka:
client:
registry-fetch-interval-seconds: 5
```
生产环境中,我们不需要修改这个值。
但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。
失效剔除
有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。
可以通过eureka.server.eviction-interval-timer-in-ms
参数对其进行修改,单位是毫秒,生成环境不要修改。
这个会对我们开发带来极大的不便,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如10S
这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。
但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:
eureka:
server:
enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)