学习资料百度网盘: https://pan.baidu.com/s/1LxIxcHDO7SYB96SE-GZfuQ
密码:dor4
两个服务放在同一个项目里面,然后分别启动,从而实现了服务拆分
1. 在配置类中注册RestTemplate
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication{
public static void main(String[] args){
SpringApplication.run(OrderApplication.class, args);
}
}
2. 在OrderService中远程调用UserService服务
@Service
public class OrderService{
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
public OrderqueryOrderById(Long orderId){
// 1. 查询订单
Order order = orderMapper.findById(orderId);
// 2. url路径
String url = "http://localhost:8081/user/" + order.getUserId();
// 3. 利用RestTemplate发起http请求,查询用户
User user = restTemplate.getForObject(url, User.class);
// 4. 封装User到Order
order.setUser(user);
// 5. 返回order
return order;
}
}
1. 在cloud-demo父工程下,创建一个子模块eureka-server
2. 引入eureka-server依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-serverartifactId>
dependency>
3. 编写启动类
@SpringBootApplication
@EnableEurekaServer //该启动类设置为eureka-server服务
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}
4. 编写配置文件application.yml
# eureka服务信息配置
server:
port: 10086
spring:
application:
name: eureka-server
# 注册到eureka中
eureka:
client:
service-url: #eureka的地址信息
defaultZone: http://127.0.0.1:10086/eureka
5. 启动微服务,然后在浏览器访问:http://127.0.0.1:10086
1. 在user-service和order-service服务模块中,引入eureka-client依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
2. 修改文件application.yml
# 设置服务信息
spring:
application:
name: user-service
# 将服务注册到http://127.0.0.1:10086/eureka服务中
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
3. 启动eureka-server服务及user-service服务,然后在浏览器访问:http://127.0.0.1:10086
启动eureka-server服务及user-service服务,我们发现当前已在Eureka中注册的实例栏中有了user-service的信息。
复制之后并不是又多了一个user-service模块,而是只是多了一个UserApplication启动服务
可以看到eureka-server管理页面数值变化
1. 修改orde-service代码用服务名称代替ip端口
我们要去http://127.0.0.1:10086/eureka
中拉取user-service
服务的实例列表,并通过负载均衡找到其中一个实例。不过这些动作不用我们去做,只需要添加一些注解@LoadBalance
即可。
2. 在order-service的OrderApplication中,给RestTemplate这个Bean添加一个@LoadBalanced注解
3. 重启order-service服务,再次访问http://localhost:8080/order/101
发现user-service服务的两个实例都打印了sql信息,并且选择其中一个发送请求。所以我们发现spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。
上述操作已经完成了负载均衡了,这里就说说关于负载均衡的SpringCloud底层完成负载均衡的Ribbon组件。
SpringCloudRibbon的底层采用了一个拦截器,拦截了RestTemplate发出的请求,对地址做了修改
负载均衡的规则都定义在IRule接口中,而IRule有很多不同的实现类
默认的实现就是ZoneAvoidanceRule,是一种轮询方案.
配置文件方式:在order-service的application.yml文件中,添加新的配置也可以修改规则(针对某个服务的负载均衡策略)
user-service: # 给某个微服务配置负载均衡规则,这里是user-service服务
ribbon:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 负载均衡规则
Ribbon采用懒加载:第一次访问时才会去创建LoadBalanceClient,请求时间会很长。
Ribbon采用饥饿加载:会在项目启动时创建LoadBalanceClient,降低第一次访问的耗时。
文件位置:例如order-service要访问user-service服务,那就在order-service的application.yml中配置user-service服务
# 配置单个clients
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: user-service # 开启饥饿加载的服务名称
ribbon:
eager-load:
enabled: true
clients:
- user-service # 配置多个饥饿加载服务
- other-service
.zip是windows版本
.tar.gz是Linux版本
startup.cmd -m standalone
但是我的电脑需要输入这样的命令
.\ startup.cmd -m standalone
因为项目是在eureka的基础上创建的,所以引入nacos的时候,需要删除以前的eureka一些配置,直接看下面示例就行了。
中引入SpringCloudAlibaba的管理依赖<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-alibaba-dependenciesartifactId>
<version>2.2.6.RELEASEversion>
<type>pomtype>
<scope>importscope>
dependency>
user-service
和order-service
中的pom文件中引入nacos-discovery依赖<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
spring:
cloud:
nacos:
server-addr: localhost:8848
一个服务可以有多个实例,例如我们的user-service,可以有
127.0.0.1:8081
127.0.0.1:8082
127.0.0.1:8083
假如这些实例分布于全国各地的不同机房
127.0.0.1:8081(在上海机房)
127.0.0.1:8082(在上海机房)
127.0.0.1:8083(在杭州机房)
也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成Nacos服务分级存储模型
给user-service配置集群
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称-杭州
把UserApplication3添加到杭州集群,先找到user-service模块下的application.yml,集群配置如下:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: SH # 集群名称-上海
默认的ZoneAvoidanceRule
并不能实现根据同集群优先来实现负载均衡。因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。
给order-service配置集群信息,修改order-service的application.yml文件,添加集群配置:
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称
修改order-service的application.yml文件,修改负载均衡规则为优先选择同集群
user-service:
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则
实际部署中会出现这样的场景
- 服务器设备性能有差异,部分实例所在机器性能较好,另一些较差,我们希望性能好的机器承担更多的用户请求。
- 但默认情况下NacosRule是同集群内随机挑选,不会考虑机器的性能问题。
- 因此,Nacos提供了权重配置来控制访问频率,权重越大则访问频率越高。
在nacos控制台,找到user-service的实例列表,点击编辑,即可修改权重
注意:如果权重修改为0,则该实例永远不会被访问
不同namespace之间相互隔离,例如不同namespace的服务互相不可见
1. 默认情况下所有的服务都在一个名为public的namespace
2. 我们可以点击页面新增按钮,添加一个namespace
3. 然后填写表单,命名空间ID不填,让它自动生成
4. 就能在页面看到一个新的namespace
5. 给微服务配置namespace
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ
namespace: 492a7d5d-237b-46a1-a99a-fa8e98e4b0f9 # 命名空间,填nacos上面生成的ID
6. 重启order-service后,访问控制台,可以看到下面的结果
7. 此时访问order-service,因为namespace不同,会导致找不到userservice,控制台会报错
1. 为什么需要nacos配置管理
当微服务部署的实例越来越多,达到数十、数百时,逐个修改微服务配置就会让人抓狂,而且很容易出错。我们需要一种统一配置管理方案,可以集中管理所有实例的配置。
其实也不是说服务的所有配置都交给nacos管理,只是一些经常改动的配置需要nacos统一管理。例如数据库的驱动器、地址、账号、密码就不是经常改动的,所以就不用放在nacos统一管理了。
2. 怎样在nacos添加配置管理
在nacos首页找到配置列表,并且点击添加
然后在弹出的表单中,填写配置信息,然后发布
3. 在服务模块user-service中,引入nacos-config依赖
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-configartifactId>
dependency>
4. 配置bootstrap.yaml文件
在user-service中\src\main\resources添加一个bootstrap.yaml文件
spring:
application:
name: user-service # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
5. 测试是否能通过bootstrap.yaml拉取到配置
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}") //通过value读取配置中的pattern.dateformat
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
在页面访问http://localhost:8081/user/now,可以看到效果
我们最终的目的,是修改nacos中的配置后,微服务中无需重启即可让配置生效,也就是配置热更新。
方式一:在@Value注入的变量所在类上添加注解@RefreshScope
在user-service的Controller层添加@RefreshScope
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope
public class UserController {
@Autowired
private UserService userService;
@Value("${pattern.dateformat}") //通过value读取配置中的pattern.dateformat
private String dateformat;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
}
}
方式二:使用@ConfigurationProperties注解代替@Value注解
在user-service服务中,添加一个类,读取patterrn.dateformat属性
@Component
@Data
@ConfigurationProperties(prefix = "pattern")
public class PatternProperties {
private String dateformat;
}
在UserController中使用这个类代替@Value
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private PatternProperties patternProperties;
@GetMapping("now")
public String now(){
return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
}
}
查看效果
启动UserApplication启动类,这时候再去改动nacos统一管理配置,就会热配置到User Application中,而不需要重新启动UserApplication。
项目依然保持运行状态,不能重新启动
第二次的配置文件如下
第二次访问形式如下
1. 我们重新来看一下bootstrap.yaml配置文件
spring:
application:
name: user-service # 服务名称
profiles:
active: dev #开发环境dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名
2. 多环境配置共享规则
我们知道user-service服务需要经过dev(开发)、test(测试)、release(发布)等过程,我们当然知道不同的过程就会有不同的环境配置,但是这些不同的环境配置中也有一些相同的参数值,可不可以把这些相同的参数值放到一个地方,尽量一改动,这个服务的环境全部改动呢?
其实上述bootstrap.yaml文件是加载了user-service.yaml配置文件和user-service-dev.yaml配置文件。是的,无论你是加载user-service-dev.yaml、还是user-service-test.yaml,配置文件是默认会加载的。那么user-service.yaml就成为了多环境共享配置文件了。
那么这个user-service.yaml配置文件在nacos的配置就跟其他配置文件一样了,具体操作可在上面找到。
3. 当nacos、服务本地同时出现相同属性时,优先级有高低之分
1. 配置Nacos
进入nacos的conf目录,修改配置文件cluster.conf.example,重命名为cluster.conf,然后添加如下内容。
127.0.0.1:8845
127.0.0.1.8846
127.0.0.1.8847
2. 修改nacos\conf\application.properties文件,添加数据库配置
spring.datasource.platform=mysql
db.num=1
db.url.0=jdbc:mysql://127.0.0.1:3306/nacos?characterEncoding=utf8&connectTimeout=1000&socketTimeout=3000&autoReconnect=true&useUnicode=true&useSSL=false&serverTimezone=UTC
db.user.0=root
db.password.0=123
3. 将nacos文件夹复制三份,分别命名为:nacos1、nacos2、nacos3
4. 分别修改三个文件夹中的application.properties
nacos1
server.port=8845
nacos2
server.port=8846
nacos1=3
server.port=8847
5. 分别启动三个nacos节点
这一次直接双击nacos\bin\startup.cmd就可以启动了。
注意:后面的启动报错,有可能是内存不足
6. 为三个naocs节点配置nginx反向代理
找到nginx/conf/nginx.conf文件,配置如下
upstream nacos-cluster { # nginx就在这三个nacos进行负载均衡
server 127.0.0.1:8845;
server 127.0.0.1:8846;
server 127.0.0.1:8847;
}
server {
listen 80;
server_name localhost;
location /nacos { #访问http://localhost/nacos
proxy_pass http://nacos-cluster; #就会跳到下面http://localhost/nacos-cluster,即上面的三个地址
}
}
7. 启动nginx服务器
8. 输入localhost/nacos,访问浏览器
1. 引入依赖
我们在order-service服务的pom文件中引入feign的依赖
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
2. 添加注解
在order-service的启动类添加注解开启Feign的功能
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {
// ...
}
3. 编写Feign客户端
在order-service中新建一个接口,内容如下:
并且在@FeignClient会对user-service模块的实例进行Ribbon负载均衡
@FeignClient("user-service") //远程调用user-service服务模块下的实例
public interface UserClient {
@GetMapping("/user/{id}") //匹配到user-service模块下的/user/{id}对应的方法
User findById(@PathVariable("id") Long id); //所以这里findById方法对应user-service的queryById方法
}
4. 远程调用
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private UserClient userClient;
public Order queryOrderById(Long orderId) {
// 1.查询订单
Order order = orderMapper.findById(orderId);
// 2.用Feign远程调用
User user = userClient.findById(order.getUserId());
// 3.封装user到Order
order.setUser(user);
// 4.返回
return order;
}
}
1. Feign可以支持很多的自定义配置
一般情况下,默认值就能满足我们使用,如果要自定义时,下有两种Feign自定义的方式。下面以日志为例来演示如何自定义配置。
2. 基于application.yml自定义Feign
# 可以针对单个服务
feign:
client:
config:
userservice: # 针对某个微服务的配置
loggerLevel: FULL # 日志级别NONE、BASIC、HEADERS、FULL
# 也可以针对多个服务
feign:
client:
config:
default: # 这里用default就是全局配置,如果是写服务名称,则是针对某个微服务的配置
loggerLevel: FULL # 日志级别
3. 基于Java代码方式自定义Feign
// 如果想要全局生效
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
// 如果想要局部生效
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class)
public class DefaultFeignConfiguration {
@Bean
public Logger.Level feignLogLevel(){
return Logger.Level.BASIC; // 日志级别为BASIC
}
}
1. 为什么Feign需要优化?
Feign底层发起http请求,依赖于其他框架,包括:
- URLConnection:不支持连接池;
- Apache HttpClient:支持连接池;
- OKHttp:支持连接池;
Feign底层默认使用URLConnection
2. 如何优化Feign?
使用支持连接池的Apache HttpClient或者OKHttp代替默认的URLConnection
3. 使用Apache HttpClient替代URLConnection
1. 在order-service的pom文件中引入Apache HttpClient依赖
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-httpclientartifactId>
dependency>
2. 在order-service的application.yml中添加配置
# 这里包含了对日志级别的优化,一般选择BASIC或NONE
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息
# 这里包含了对底层连接的优化,使用Apache HttpClient来替代URLConnection
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数
3. 测试是否使用了Apache HttpClient
在FeignClientFactoryBean中的loadBalance方法中打断点
Debug方式启动order-service服务,可以看到这里的client,底层就是Apache HttpClient
上面演示的过程中,我们通过访问的order-service服务模块获取数据,但是一般情况下不能直接访问。
因为有些微服务模块是不给外人访问的,所以我们需要用Gateway统一网关来限定外部访问。
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-gatewayartifactId>
dependency>
<dependency>
<groupId>com.alibaba.cloudgroupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discoveryartifactId>
dependency>
3. 编写启动类
@SpringBootApplication
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
4. 编写基础配置和路由规则
我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://user-service # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
5. 重启测试
访问http://localhost:10010/user/1时,符合/user/**规则,请求转发到uri:http://userservice/user/1。nacos根据userservice服务名发现服务,给出服务列表,并根据负载均衡返回其中一个实例。
6. 总体流程
我们在配置文件中写的断言规则只是字符串,这些字符串会被Predicate Factory读取并处理,转变为路由判断的条件,例如Path=/user/**是按照路径匹配,这个规则是由PathRoutePredicateFactory
类来处理的,像这样的断言工厂在SpringCloudGateway还有十几个,我们只需要掌握Path这种路由工程就可以了。
GatewayFilter是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理:
1. 添加AddRequestHeader过滤器
Spring提供了31种不同的路由过滤器工厂,下面我们以AddRequestHeader为例来讲解
只需要修改gateway服务的application.yml文件,即可添加过滤器
路由过滤器
# 当前过滤器卸载userservice路由下,因此仅仅对的访问userservice的请求有效
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Truth, bcb is freaking awesome! # 添加请求头
默认过滤器
# 当前过滤器写在gateway下,则可以对所有路由都生效
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://user-service
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Truth, bcb is freaking awesome!
2. 测试:来到UserController修改queryById方法,代码如下
@GetMapping("/{id}")
public User queryById(@PathVariable("id") Long id,@RequestHeader(value = "Truth", required = false) String truth) {
log.info(truth);
return userService.queryById(id);
}
重启网关服务以及user-service服务,访问http://localhost:10010/user/1,可以看到控制台打印出了对应的日志
全局过滤器的作用也是处理一切进入网关的请求和微服务响应,与GatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现。
1. 实现GlobalFilter接口
@Order(-1)
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}
1. 不同类型过滤器之间的优先级,如下
2. 同一类型过滤器的优先级
默认过滤器:按照在配置文件中的排序,order从1开始计算
路由过滤器:按照在配置文件中的排序,order从1开始计算
全局过滤器:在GlobalFilter实现类的类名加上注解的@Order()
1. 什么是跨域问题
浏览器禁止请求的发起者与服务端发生跨域ajax请求,请求被浏览器拦截的问题
- 域名不同: www.taobao.com 和 www.taobao.org 和 www.jd.com
- 域名相同,端口不同:localhost:8080和localhost8081
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期
1. 微服务部署问题及其解决办法。
问题1:依赖关系复杂,容易出现兼容性问题
- 将应用的Libs(函数库)、Deps(依赖)、配置、应用一起打包
问题2:操作系统环境差异问题
- 因为无论是Ubuntu还是CentOS环境,都是基于Linux内核,那就直接把Linux内核打包就好了
1. Docker重要组件
1. 注册器(Registry)
- 一个 Docker Registry 中可以包含多个仓库(Repository);
2. 仓库(Repository)
- 一个仓库会包含同一个软件不同版本的镜像,我们可以通过 <仓库名>:<标签> 的格式来指定具体是这个软件哪个版本的镜像。如果不给出标签,将以 latest 作为默认标签。
3. 镜像(Image)
- 回忆一下虚拟机VM安装的时候是不是要导入centos镜像,这个centos镜像就是Image。其他的应用也有各自的镜像,例如MySQL等等。Image需要从Repository拉取到Docker容器中才能运行。
4. 容器(Container)
- 镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
5. Dockerfile
- Dockerfile 是一个用来构建镜像Image的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。
1. 版本问题
Docker CE支持64位版本CentOS7,并且要求内核版本不低于3.10,CentOS7满足最低内核的要求,所以我们在CentOS 7安装Docker
2. 如果之前安装过旧版本的Docker,可以使用下面命令卸载
yum remove docker \
docker-client \
docker-client-latest \
docker-common \
docker-latest \
docker-latest-logrotate \
docker-logrotate \
docker-selinux \
docker-engine-selinux \
docker-engine \
docker-ce
3. 安装yum工具
yum install -y yum-utils \
device-mapper-persistent-data \
lvm2 --skip-broken
4. 更新本地镜像源
# 设置docker镜像源
yum-config-manager \
--add-repo \
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
sed -i 's/download.docker.com/mirrors.aliyun.com\/docker-ce/g' /etc/yum.repos.d/docker-ce.repo
yum makecache fast
yum install -y docker-ce
Docker应用需要用到各种端口,逐一去修改防火墙装置。非常麻烦,因此建议大家直接关闭防火墙
# 关闭
systemctl stop firewalld
# 禁止开机启动防火墙
systemctl disable firewalld
启动docker
# 启动docker
systemctl start docker
# 查看是否启动成功
systemctl status docker
# 创建/etc/docker文件
sudo mkdir -p /etc/docker
# 向文件写入内容
sudo tee /etc/docker/daemon.json <<-'EOF'
{
"registry-mirrors": ["https://n0dwemtq.mirror.aliyuncs.com"]
}
EOF
# 重新加载
sudo systemctl daemon-reload
# 重新启动
sudo systemctl restart docker
1. 拉取、查看镜像
首先去镜像仓库搜索nginx镜像,比如DockerHub:
根据查看到的镜像名称,拉取自己需要的镜像,通过命令:docker pull nginx
通过命令:docker images 查看拉取到的镜像
2. 保存、导入镜像
保存命令:docker save -o [保存的目标文件名称] [镜像名称]
# 把镜像nginx:latest保存到nginx.tar文件
docker save -o nginx.tar nginx:latest
docker rmi nginx:lateset
然后运行命令,加载本地文件
docker load -i nginx.tar
docker run:创建并运行一个容器,处于运行状态
docker pause:让一个运行的容器暂停
docker unpause:让一个容器从暂停状态恢复运行
docker stop:停止一个运行的容器
docker start:让一个停止的容器再次运行
docker rm:删除一个容器
docker run --name containerName -p 80:80 -d nginx
docker run :创建并运行一个容器
–name : 给容器起一个名字,比如叫做mn
-p :将宿主机端口与容器端口映射,冒号左侧是宿主机端口,右侧是容器端口
-d:后台运行容器
nginx:镜像名称,例如nginx
2. 创建nginx镜像
docker run --name mn -p 80:80 -d nginx
docker ps
3. 向nginx所在的服务区发起访问
访问http://192.168.133.128:80
进入容器
在我看来就是进入了虚拟机终端里面的另一个终端,仔细品一下吧
# 我要进入mn容器中,并且能够进入输入输出,并且打开交互命令
docker exec -it mn bash
- docker exec:进入容器,执行一个命令
- -it:给当前进入的容器创建一个标准输入、输出终端、允许我们与容器交互
- mn:要进入的容器的名称
- bash:进入容器后执行的命令,bash是一个linux终端交互命令
1. 原先的Docker是容器与数据耦合
在之前的nginx案例中,修改nginx的html页面时,需要进入nginx内部,并且因为没有编辑器,修改文件也很麻烦,这就是容器与数据耦合带来的后果。
- 不便于修改:当我们要修改Nginx的html内容时,需要进入容器内部修改,很不方便
- 数据不可复用:在容器内的修改对外是不可见的,所有修改对新创建的容器是不可复用的
- 升级维护困难:数据在容器内,如果要升级容器必然删除旧容器,所有数据都跟着删除了
要解决这个问题,必须将数据与容器解耦,这就是数据卷。
2. 数据卷原理
数据卷(volume)是一个虚拟目录,指向宿主机文件系统中的某个目录。
一旦完成数据卷挂载,对容器的一切操作都会作用在数据卷对应的宿主机目录了。
这样,我们操作宿主机的/var/lib/docker/volumes/html目录,就等于操作容器内的/usr/share/nginx/html目录了
# 创建数据卷
docker volume create html
# 查看所有数据
docker volume ls
# 查看数据卷详细信息卷
docker volume inspect html
4. 指定数据卷挂载的目录
docker run \
--name mn \
-v html:/root/html \
-p 8080:80
nginx \
1. 创建Dockerfile文件
在/docker目录
下,新建一个Dockerfile 文件
,并在文件内添加以下内容:
`FROM` 定制的镜像都是基于 FROM 的镜像,这里的 nginx 就是定制需要的基础镜像。
`RUN` 用于执行后面跟着的命令行命令(等同于,在终端操作的 shell 命令)
2. 开始构建镜像
在 Dockerfile 文件的存放目录/docker下,执行docker build -t nginx:v3 .
命令。
nginx:v3
:镜像名称:镜像标签
.
:代表本次执行的上下文路径
需求:搭建一个JavaWeb镜像,然后在上面运行Java代码
步骤1:新建一个空文件夹docker-demo
步骤2:拷贝课前资料中的docker-demo.jar文件到docker-demo这个目录
步骤3:拷贝课前资料中的jdk8.tar.gz文件到docker-demo这个目录
步骤4:拷贝课前资料提供的Dockerfile到docker-demo这个目录
Dockerfile内容如下
# 指定基础镜像
FROM ubuntu:16.04
# 配置环境变量,JDK的安装目录
ENV JAVA_DIR=/usr/local
# 拷贝jdk和java项目的包
COPY ./jdk8.tar.gz $JAVA_DIR/
COPY ./docker-demo.jar /tmp/app.jar
# 安装JDK
RUN cd $JAVA_DIR \
&& tar -xf ./jdk8.tar.gz \
&& mv ./jdk1.8.0_144 ./java8
# 配置环境变量
ENV JAVA_HOME=$JAVA_DIR/java8
ENV PATH=$PATH:$JAVA_HOME/bin
# 暴露端口
EXPOSE 8090
# 入口,java项目的启动命令
ENTRYPOINT java -jar /tmp/app.jar
步骤5:将准备好的docker-demo上传到虚拟机任意目录,然后进入docker-demo目录下
步骤6:运行命令docker build -t javaweb:1.0 .
步骤7:运行镜像docker run --name web -p 8090:8090 -d javaweb:1.0
步骤8:最后访问 http://ip:8090/hello/count,其中的ip改成你的虚拟机ip,结果如下:
1. 什么是DockerCompose
Docker Compose可以基于Compose文件帮我们快速的部署分布式应用,而无需手动一个个创建和运行容器,Compose文件不是一个文本文件,通过指令定义集群中的每个容器如何运行。
2. DockerCompose的使用实例
还记得Docker启动镜像吗?其实就是将Docker run启动命令都放在Docker Compose文件中,只是语法稍有差异。
version: "3.8"
services:
mysql:
image: mysql:5.7.25
environment:
MYSQL_ROOT_PASSWORD: 123
volumes:
- "/tmp/mysql/data:/var/lib/mysql"
- "/tmp/mysql/conf/hmy.cnf:/etc/mysql/conf.d/hmy.cnf"
web:
build: .
ports:
- "8090:8090"
3. DockerCompose的下载安装
1. 百度网盘下载docker-compose文件,通过xftp放到/usr/local/bin目录下
2. 修改文件权限
chmod +x /usr/local/bin/docker-compose
执行后文件变绿了,代表文件可以执行了
3. Base自动补全命令
curl -L https://raw.githubusercontent.com/docker/compose/1.29.1/contrib/completion/bash/docker-compose > /etc/bash_completion.d/docker-compose
如果出错了,无法下载,那就执行如下命令
echo "199.232.68.133 raw.githubusercontent.com" >> /etc/hosts
1. 查看课前资料提供的cloud-demo文件夹,里面已经编写好了docker-compose文件
2. 查看docker-compose.yml文件
version: "3.2"
services: # 包含了下面五个服务nacos、mysql、userservice、orderservice、gateway
nacos: # 由于所有服务都需要注册到nacos,所以nacos先启动
image: nacos/nacos-server # 镜像
environment:
MODE: standalone # 单点模式
ports:
- "8848:8848" # 端口
mysql:
image: mysql:5.7.25 # 镜像
environment:
MYSQL_ROOT_PASSWORD: 123 # 密码
volumes: # 数据挂载
- "$PWD/mysql/data:/var/lib/mysql" # $PWD是相对于/cloud-demo/docker-compose.yml文件下的目录的当前
- "$PWD/mysql/conf:/etc/mysql/conf.d/"
userservice:
build: ./user-service
orderservice:
build: ./order-service
gateway:
build: ./gateway
ports:
- "10010:10010"
3. 修改自己的cloud-demo项目,将数据库、nacos地址都命名为docker-compose中的服务名
具体可以看视频讲解
4. 打包
接下来需要将我们的每个微服务都打包。因为之前查看到Dockerfile中的jar包名称都是app.jar,因此我们的每个微服务都需要用这个名称。
可以通过修改pom.xml中的打包名称来实现,每个微服务都需要修改:
<build>
<finalName>appfinalName>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
5. 拷贝jar包到部署目录
编译打包好的app.jar文件,需要放到Dockerfile的同级目录中。注意:每个微服务的app.jar放到与服务名称对应的目录,别搞错了。
6. 部署
最后,我们需要将文件整个cloud-demo文件夹上传到虚拟机中,理由DockerCompose部署。上传到任意目录
进入cloud-demo目录,然后运行下面的命令:docker-compose up -d
究其原因,原来是因为nacos还未启动成功,导致其他微服务同时启动时无法注册成功,这时我们可以运行
docker-compose restart gateway userservice orderservice
8. 重新启动项目
重启这三个微服务,重启完成后可以查看日志,看看服务是否正常启动了.
9. 开启项目
最后可以访问http://10.100.2.210:10010/order/101?authorization=admin,ip记得改成虚拟机ip,结果如图,说明服务正常启动了.
1. 同步通讯问题
- 耦合度高:每次加入新的需求,都要修改原来的代码
- 性能下降:调用者需要等待服务提供者响应,如果调用链过长则响应时间等于每次调用的时间
- 资源浪费:调用链中的每个服务都在等待响应过程中,不能释放请求占用的资源,高并发场景下会极度浪费系统资源
- 级联失败:如果服务提供者出现问题,所有调用都会跟着出问题,迅速导致整个微服务集群故障
- 服务解耦:支付服务不再调用订单服务来,而是支付服务直接发布消息,订单服务订阅到消息并且执行服务
- 性能提升:支付服务发送订单消息后,直接做其他的事情了,而不是还在等待订单服务执行
- 解决级联失败问题:服务没有了相互调用,就不用担心级联失败问题
- 流量削峰:由Broker承担流量压力分配
MQ,中文是消息队列(MessageQueue),字面来看就是存放消息的队列。也就是事件驱动架构中的Broker。
1. 下载镜像
docker pull rabbitmq:3-management
2. 安装MQ
docker run \
-e RABBITMQ_DEFAULT_USER=itcast \ # MQ页面登陆的账号
-e RABBITMQ_DEFAULT_PASS=123321 \ # MQ页面登陆的密码
--name mq \ # 给MQ起个名字
--hostname mq1 \ # 起个主机名,分布式集群的时候使用
-p 15672:15672 \ # MQ提供一个管理平台的UI,通过这个端口访问
-p 5672:5672 \ # 用来消息通信的端口
-d \ # 后台运行
rabbitmq:3-management # 镜像的名称
3. 运行后访问
运行后访问http://192.168.133.128:15672/,其中ip需要改成自己虚拟机的ip,可以看到
channel:操作MQ的工具
exchange:路由消息到队列中
queue:缓存消息
virtual host:虚拟主机,对exchange、queue等资源的逻辑分组
- BasicQueue和WorkQueue都是基于Queue队列来完成消息发送,并没有使用到交换机。
- 发布订阅使用到Exchange交换机。
1. 课前资料提供了一个Demo工程,mq-demo
2. 查看项目结构
3. 代码查看
publisher实现
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class PublisherTest {
@Test
public void testSendMessage() throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.133.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.发送消息
String message = "hello, rabbitmq!";
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("发送消息成功:【" + message + "】");
// 5.关闭通道和连接
channel.close();
connection.close();
}
}
consumer实现
package cn.itcast.mq.helloworld;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
// 1.建立连接
ConnectionFactory factory = new ConnectionFactory();
// 1.1.设置连接参数,分别是:主机名、端口号、vhost、用户名、密码
factory.setHost("192.168.133.128");
factory.setPort(5672);
factory.setVirtualHost("/");
factory.setUsername("itcast");
factory.setPassword("123321");
// 1.2.建立连接
Connection connection = factory.newConnection();
// 2.创建通道Channel
Channel channel = connection.createChannel();
// 3.创建队列
String queueName = "simple.queue";
channel.queueDeclare(queueName, false, false, false, null);
// 4.订阅消息
channel.basicConsume(queueName, true, new DefaultConsumer(channel){
@Override
public void handleDelivery(
String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body) throws IOException {
// 5.处理消息
String message = new String(body);
System.out.println("接收到消息:【" + message + "】");
}
});
System.out.println("等待接收消息。。。。");
}
}
4. 运行
先运行PublisherTest,再运行ConsumerTest,在ConsumerTest控制台中可以获得如下结果
SpringAMQP是基于RabbitMQ封装的一套模板,并且还利用SpringBoot对其实现了自动装配,使用起来非常方便。
1. 在父工程中引入mq-demo依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2. 消息发送
首先配置MQ地址,在publisher服务的application.yml中添加配置
spring:
rabbitmq:
host: 10.100.2.210 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
package cn.itcast.mq.spring;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringAmqpTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSimpleQueue() {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, spring amqp!";
// 发送消息
rabbitTemplate.convertAndSend(queueName, message);
}
}
3. 消息接收
首先配置MQ地址,在comsumer服务的application.yml中添加配置:
spring:
rabbitmq:
host: 10.100.2.210 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
然后在consumer服务的cn.itcast.mq.listener包中新建一个类SpringRabbitListener,代码如下:
package cn.itcast.mq.listener;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
@Component
public class SpringRabbitListener {
@RabbitListener(queues = "simple.queue")
public void listenSimpleQueueMessage(String msg) throws InterruptedException {
System.out.println("spring 消费者接收到消息:【" + msg + "】");
}
}
Work queues,也被称为(Task queues),任务模型。简单来说就是让多个消费者绑定到一个队列,共同消费队列中的消息。
1. 在父工程中引入mq-demo依赖
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-amqpartifactId>
dependency>
2. 消息发送
首先配置MQ地址,在publisher服务的application.yml中添加配置
spring:
rabbitmq:
host: 10.100.2.210 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
然后在publisher服务中编写测试类SpringAmqpTest,并利用RabbitTemplate实现消息发送:
/**
* workQueue
* 向队列中不停发送消息,模拟消息堆积。
*/
@Test
public void testWorkQueue() throws InterruptedException {
// 队列名称
String queueName = "simple.queue";
// 消息
String message = "hello, message_";
for (int i = 0; i < 50; i++) {
// 发送消息
rabbitTemplate.convertAndSend(queueName, message + i);
Thread.sleep(20);
}
}
3. 消息接收
首先配置MQ地址,在comsumer服务的application.yml中添加配置:
spring:
rabbitmq:
host: 10.100.2.210 # 主机名
port: 5672 # 端口
virtual-host: / # 虚拟主机
username: itcast # 用户名
password: 123321 # 密码
然后在consumer服务的cn.itcast.mq.listener包中新建一个类SpringRabbitListener,代码如下:
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue1(String msg) throws InterruptedException {
System.out.println("消费者1接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(20);
}
@RabbitListener(queues = "simple.queue")
public void listenWorkQueue2(String msg) throws InterruptedException {
System.err.println("消费者2........接收到消息:【" + msg + "】" + LocalTime.now());
Thread.sleep(200);
}
4. 测试
启动ConsumerApplication后,在执行publisher服务中刚刚编写的发送测试方法testWorkQueue。可以看到消费者1很快完成了自己的25条消息。消费者2却在缓慢的处理自己的25条消息。
也就是说消息是平均分配给每个消费者,并没有考虑到消费者的处理能力。这样显然是有问题的。
在spring中有一个简单的配置,可以解决这个问题。我们修改consumer服务的application.yml文件,添加配置:
spring:
rabbitmq:
listener:
simple:
prefetch: 1 # 每次只能获取一条消息,处理完成才能获取下一个消息
运行consumer服务可以看到,结果是处理消息快的处理的次数也多,即能者多劳.