Springcloud 微服务实战笔记 Ribbon
使用
@Configurationpublic class CustomConfiguration {@Bean@LoadBalanced // 开启负载均衡能力public RestTemplate restTemplate() {return new RestTemplate();}}
可看到使用Ribbon,非常简单,只需将@LoadBalanced注解加在RestTemplate的Bean上,就可以实现负载均衡。
@LoadBalanced
从该注解的源码上的注释,可看到LoadBalancerClient类来配置的
/*** Annotation to mark a RestTemplate or WebClient bean to be configured to use a* LoadBalancerClient.* @author Spencer Gibb*/
@Target({ ElementType.FIELD, ElementType.PARAMETER, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Qualifier
public @interface LoadBalanced {}
查看LoadBalancerClient源码,如下:
/*** Represents a client-side load balancer.** @author Spencer Gibb*/
public interface LoadBalancerClient extends ServiceInstanceChooser {/*** Executes request using a ServiceInstance from the LoadBalancer for the specified* service.* @param serviceId The service ID to look up the LoadBalancer.* @param request Allows implementations to execute pre and post actions, such as* incrementing metrics.* @param <T> type of the response* @throws IOException in case of IO issues.* @return The result of the LoadBalancerRequest callback on the selected* ServiceInstance.*/<T> T execute(String serviceId, LoadBalancerRequest<T> request) throws IOException;/*** Executes request using a ServiceInstance from the LoadBalancer for the specified* service.* @param serviceId The service ID to look up the LoadBalancer.* @param serviceInstance The service to execute the request to.* @param request Allows implementations to execute pre and post actions, such as* incrementing metrics.* @param <T> type of the response* @throws IOException in case of IO issues.* @return The result of the LoadBalancerRequest callback on the selected* ServiceInstance.*/<T> T execute(String serviceId, ServiceInstance serviceInstance,LoadBalancerRequest<T> request) throws IOException;/*** Creates a proper URI with a real host and port for systems to utilize. Some systems* use a URI with the logical service name as the host, such as* http://myservice/path/to/service. This will replace the service name with the* host:port from the ServiceInstance.* @param instance service instance to reconstruct the URI* @param original A URI with the host as a logical service name.* @return A reconstructed URI.*/URI reconstructURI(ServiceInstance instance, URI original);}
* Implemented by classes which use a load balancer to choose a server to send a request* to.** @author Ryan Baxter*/
public interface ServiceInstanceChooser {/*** Chooses a ServiceInstance from the LoadBalancer for the specified service.* @param serviceId The service ID to look up the LoadBalancer.* @return A ServiceInstance that matches the serviceId.*/ServiceInstance choose(String serviceId);}
ServiceInstanceChooser看类名便可知用来帮我们从同一个服务的多个服务实例中根据负载均衡策略选择出所需的服务实例。
choose:根据传入的服务名ServiceId,从负载均衡器中选择一个对应服务的实例。
execute:使用从负载均衡器中挑选出的服务实例来执行请求内容。
reconstructURI:为系统构建一个合适的host:port形式的URI。在分布式系统中,我们使用逻辑上的服务名称作为host来构建URI(替代服务实例的host:port形式)进行请求,比如http://myservice/path/to/service。在该操作的定义中,前者ServiceInstance对象是带有host和port的具体服务实例,而后者URI对象则是使用逻辑服务名定义为host 的 URI,而返回的 URI 内容则是通过ServiceInstance的服务实例详情拼接出的具体host:post形式的请求地址。
通过搜索@LoadBalanced使用地方发现,只有org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration使用到了@LoadBalanced
/*** Auto-configuration for Ribbon (client-side load balancing).** @author Spencer Gibb* @author Dave Syer* @author Will Tran* @author Gang Li*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RestTemplate.class)
@ConditionalOnBean(LoadBalancerClient.class)
@EnableConfigurationProperties(LoadBalancerRetryProperties.class)
public class LoadBalancerAutoConfiguration {@LoadBalanced@Autowired(required = false)private List<RestTemplate> restTemplates = Collections.emptyList();@Autowired(required = false)private List<LoadBalancerRequestTransformer> transformers = Collections.emptyList();@Beanpublic SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {return () -> restTemplateCustomizers.ifAvailable(customizers -> {for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {for (RestTemplateCustomizer customizer : customizers) {customizer.customize(restTemplate);}}});}@Bean@ConditionalOnMissingBeanpublic LoadBalancerRequestFactory loadBalancerRequestFactory(LoadBalancerClient loadBalancerClient) {return new LoadBalancerRequestFactory(loadBalancerClient, this.transformers);}@Configuration(proxyBeanMethods = false)@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")static class LoadBalancerInterceptorConfig {@Beanpublic LoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,LoadBalancerRequestFactory requestFactory) {return new LoadBalancerInterceptor(loadBalancerClient, requestFactory);}@Bean@ConditionalOnMissingBeanpublic RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {return restTemplate -> {List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());list.add(loadBalancerInterceptor);restTemplate.setInterceptors(list);};}}/*** Auto configuration for retry mechanism.*/@Configuration(proxyBeanMethods = false)@ConditionalOnClass(RetryTemplate.class)public static class RetryAutoConfiguration {@Bean@ConditionalOnMissingBeanpublic LoadBalancedRetryFactory loadBalancedRetryFactory() {return new LoadBalancedRetryFactory() {};}}/*** Auto configuration for retry intercepting mechanism.*/@Configuration(proxyBeanMethods = false)@ConditionalOnClass(RetryTemplate.class)public static class RetryInterceptorAutoConfiguration {@Bean@ConditionalOnMissingBeanpublic RetryLoadBalancerInterceptor ribbonInterceptor(LoadBalancerClient loadBalancerClient,LoadBalancerRetryProperties properties,LoadBalancerRequestFactory requestFactory,LoadBalancedRetryFactory loadBalancedRetryFactory) {return new RetryLoadBalancerInterceptor(loadBalancerClient, properties,requestFactory, loadBalancedRetryFactory);}@Bean@ConditionalOnMissingBeanpublic RestTemplateCustomizer restTemplateCustomizer(final RetryLoadBalancerInterceptor loadBalancerInterceptor) {return restTemplate -> {List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());list.add(loadBalancerInterceptor);restTemplate.setInterceptors(list);};}}}
这段自动装配的代码的含义不难理解,就是利用了RestTempllate的拦截器,使用RestTemplateCustomizer对所有标注了@LoadBalanced的RestTemplate Bean添加了一个LoadBalancerInterceptor拦截器,而这个拦截器的作用就是对请求的URI进行转换获取到具体应该请求哪个服务实例ServiceInstance。
@LoadBalanced @Autowired(required = false)
这两个注解在一起的意思:只注入@LoadBalanced注解的Bean ,带有@LoadBalanced注解的RestTemplate会在原有的拦截器基础上加上LoadBalancerInterceptor拦截器
@Beanpublic SmartInitializingSingleton loadBalancedRestTemplateInitializerDeprecated(final ObjectProvider<List<RestTemplateCustomizer>> restTemplateCustomizers) {return () -> restTemplateCustomizers.ifAvailable(customizers -> {for (RestTemplate restTemplate : LoadBalancerAutoConfiguration.this.restTemplates) {for (RestTemplateCustomizer customizer : customizers) {customizer.customize(restTemplate);}}});}
@Bean@ConditionalOnMissingBeanpublic RestTemplateCustomizer restTemplateCustomizer(final LoadBalancerInterceptor loadBalancerInterceptor) {return restTemplate -> {List<ClientHttpRequestInterceptor> list = new ArrayList<>(restTemplate.getInterceptors());list.add(loadBalancerInterceptor);restTemplate.setInterceptors(list);};}
可看到RestTemplate是可以设置拦截器。
org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor实现如下:
/*** @author Spencer Gibb* @author Dave Syer* @author Ryan Baxter* @author William Tran*/
public class LoadBalancerInterceptor implements ClientHttpRequestInterceptor {private LoadBalancerClient loadBalancer;private LoadBalancerRequestFactory requestFactory;public LoadBalancerInterceptor(LoadBalancerClient loadBalancer,LoadBalancerRequestFactory requestFactory) {this.loadBalancer = loadBalancer;this.requestFactory = requestFactory;}public LoadBalancerInterceptor(LoadBalancerClient loadBalancer) {// for backwards compatibilitythis(loadBalancer, new LoadBalancerRequestFactory(loadBalancer));}@Overridepublic ClientHttpResponse intercept(final HttpRequest request, final byte[] body,final ClientHttpRequestExecution execution) throws IOException {final URI originalUri = request.getURI();String serviceName = originalUri.getHost();Assert.state(serviceName != null,"Request URI does not contain a valid hostname: " + originalUri);return this.loadBalancer.execute(serviceName,this.requestFactory.createRequest(request, body, execution));}}
可看出:将LoadBalancerClient以及LoadBalancerRequestFactory创建的请求封装到LoadBalancerInterceptor中,并在intercept()方法中,使用LoadBalancerClient执行请求。
RestTemplate调用过程
RestTemplate.getForObject/getForEntity... --> RestTemplate.excute --> RestTemplate.doExecute --> org.springframework.http.client.ClientHttpRequestFactory#createRequest
--> org.springframework.http.client.AbstractClientHttpRequest#execute
--> org.springframework.http.client.AbstractClientHttpRequest#executeInternal
--> org.springframework.http.client.InterceptingClientHttpRequest#executeInternal
--> org.springframework.http.client.InterceptingClientHttpRequest.InterceptingRequestExecution#execute
--> org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor#intercept
--> org.springframework.cloud.client.loadbalancer.LoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest)
--> org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest)
整个流程最终可看到执行拦截器的execute方法,最终调用负载均衡的execute方法
最终调用org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor#intercept
@Overridepublic ClientHttpResponse intercept(final HttpRequest request, final byte[] body,final ClientHttpRequestExecution execution) throws IOException {final URI originalUri = request.getURI();String serviceName = originalUri.getHost();Assert.state(serviceName != null,"Request URI does not contain a valid hostname: " + originalUri);return this.loadBalancer.execute(serviceName,this.requestFactory.createRequest(request, body, execution));}
org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient#execute(java.lang.String, org.springframework.cloud.client.loadbalancer.LoadBalancerRequest, java.lang.Object)
public <T> T execute(String serviceId, LoadBalancerRequest<T> request, Object hint)throws IOException {ILoadBalancer loadBalancer = getLoadBalancer(serviceId);Server server = getServer(loadBalancer, hint);if (server == null) {throw new IllegalStateException("No instances available for " + serviceId);}RibbonServer ribbonServer = new RibbonServer(serviceId, server,isSecure(server, serviceId),serverIntrospector(serviceId).getMetadata(server));return execute(serviceId, ribbonServer, request);}
此处是RibbonLoadBalancerClient的execute方法,首先获取负载均衡器(getLoadBalancer),然后通过负载均衡器获取服务,看下getServer()方法实现:
protected Server getServer(ILoadBalancer loadBalancer, Object hint) {if (loadBalancer == null) {return null;}// Use 'default' on a null hint, or just pass it on?return loadBalancer.chooseServer(hint != null ? hint : "default");}
public Server chooseServer(Object key) {if (counter == null) {counter = createCounter();}counter.increment();if (rule == null) {return null;} else {try {return rule.choose(key);} catch (Exception e) {logger.warn("LoadBalancer [{}]: Error choosing server for key {}", name, key, e);return null;}}}
可看到最终是调用策略器的choose方法,选择服务器。
参考资料:
Spring Cloud微服务实战
相关文章:
Springcloud 微服务实战笔记 Ribbon
使用 Configurationpublic class CustomConfiguration {BeanLoadBalanced // 开启负载均衡能力public RestTemplate restTemplate() {return new RestTemplate();}}可看到使用Ribbon,非常简单,只需将LoadBalanced注解加在RestTemplate的Bean上࿰…...
CSS基础笔记-04cascade-specificity-inheritance
CSS基础笔记系列 《CSS基础笔记-01CSS概述》《CSS基础笔记-02动画》CSS基础笔记-03选择器 前言 Cascading Style Sheets,关键就在于这个cascading,对于这个术语理解,感觉对于我这种CSS新手有点儿不太friendly。本文记录下我对这个术语的理…...
Spring应用的部署与管理
一、前言 部署是将开发好的应用发布到服务器上,使其能够被用户访问的关键步骤。Spring框架提供了灵活的部署选项,本文将介绍Spring应用的常见部署方式和一些建议,帮助开发者顺利将应用投放到生产环境。 二、传统部署方式:WAR包 传…...
B端产品经理学习-需求挖掘
B端产品需求挖掘 目录 识别和管理干系人 决策人和负责人需求挖掘 针对用户进行需求挖掘 用户访谈结果整理 B端产品的需求来源是非常复杂的,要考虑多个方面;如果你是一个通用性的产品,要考虑市场、自身优劣势、干系人。而定制型B端产品会…...
整数规划基本原理
1.1 定义 规划中的变量(部分或全部)限制为整数时,称为整数规划。若在线性规划模型中,变量限制为整数,则称为整数线性规划。目前所流行的求解整数规划的方法,往往只适用于整数线性规划。目前还没有一种方法…...
秋招复习之堆
目录 前言 堆 堆的常用操作 堆的实现(大根堆) 1. 堆的存储与表示 2. 访问堆顶元素 3. 元素入堆 4. 堆顶元素出堆 Top-k 问题 方法一:遍历选择 方法二:排序 方法三:堆 总结 前言 秋招复习之堆。 堆 「堆 heap…...
算法训练营Day36(贪心-重叠区间)
都算是 重叠区间 问题,大家可以好好感受一下。 都属于那种看起来好复杂,但一看贪心解法,惊呼:这么巧妙! 还是属于那种,做过了也就会了,没做过就很难想出来。 不过大家把如下三题做了之后&#…...
如何利用Oracle官方网站不登录账号下载和安装非最新版本的JDK(版本自由选择)
一、JDK概述 JDK(Java Development Kit)是Java开发工具集,是针对Java编程语言的软件开发环境。它包含了Java编译器、JRE(Java运行时环境)以及其他一些用于开发、调试和测试Java应用程序的工具,是Java开发人…...
税法相关的基础知识
文章目录 税法原则1.税法基本原则2.税法适用原则 来和大家聊聊税法相关的基础知识 税法原则 1.税法基本原则 2.税法适用原则...
ListNode 2487. 从链表中移除节点,单调栈的应用
一、题目 1、题目描述 给你一个链表的头节点 head 。 移除每个右侧有一个更大数值的节点。 返回修改后链表的头节点 head 。 2、接口描述 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nu…...
vue3中pdf打印问题处理
1 get请求参数问题 之前的请求是post得不到参数,今天发现的问题很奇怪,从前端进入网关,网关居然得不到参数。 前端代码 const print () > {let linkUrlStr proxy.$tool.getUrlStr(proxy.$api.invOrder.psiInvOrder.printSalOutstock,{a…...
如何向嵌入式设备中添加tcpdump工具
说明:tcpdump是一个在网络设备调试中一个非常重要的工具,它并不像hexdump等工具集成在busybox里面,也不像其他的软件一样只需要依赖linux标准的库就可以实现,它需要pcap相关的库和加密的相关库。 本文主要是基于realtek 83系列的…...
伦茨科技Apple Find My认证芯片-ST17H6x芯片
深圳市伦茨科技有限公司(以下简称“伦茨科技”)发布ST17H6x Soc平台。成为继Nordic之后全球第二家取得Apple Find My「查找」认证的芯片厂家,该平台提供可通过Apple Find My认证的Apple查找(Find My)功能集成解决方案。…...
uni-app 前后端调用实例 基于Springboot 数据列表显示实现
锋哥原创的uni-app视频教程: 2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中..._哔哩哔哩_bilibili2023版uniapp从入门到上天视频教程(Java后端无废话版),火爆更新中...共计23条视频,包括:第1讲 uni…...
python渗透工具编写学习笔记:10、网络爬虫基础/多功能编写
目录 前言 10.1 概念 10.2 调度器/解析器 10.3 存储器/去重器 10.4 日志模块 10.5 反爬模块 10.6 代理模块 前言 在渗透工具中,网络爬虫有着不可忽视的作用,它能够快速而精准的搜寻、提取我们所需要的信息并按照我们所需要的格式排列,…...
Python武器库开发-武器库篇之子域名扫描器开发(四十一)
Python武器库开发-武器库篇之子域名扫描器开发(四十一) 在我们做红队攻防或者渗透测试的过程中,信息收集往往都是第一步的,有人说:渗透的本质就是信息收集,前期好的信息收集很大程度上决定了渗透的质量和攻击面,本文将…...
通俗易懂的15个Java Lambda表达式案例
文章目录 1. **实现Runnable接口**:2. **事件监听器**(如Swing中的ActionListener):3. **集合遍历**(使用forEach方法):4. **过滤集合**(使用Stream API):5. …...
十七:爬虫-JS逆向(上)
1、什么是JS、JS反爬是什么?JS逆向是什么? JS:JS全称JavaScript是互联网上最流行的脚本语言,这门语言可用于HTML 和 web,更可广泛用于服务器、PC、笔记本电脑、平板电脑和智能手机等设备。JavaScript 是一种轻量级的编程语言。JavaScript 是…...
How to implement anti-crawler strategies to protect site data
How to implement anti-crawler strategies to protect site data 信息校验型反爬虫User-Agent反爬虫Cookie反爬虫签名验证反爬虫WebSocket握手验证反爬虫WebSocket消息校验反爬虫WebSocket Ping反爬虫 动态渲染反爬虫文本混淆反爬虫图片伪装反爬虫CSS偏移反爬虫SVG映射反爬虫字…...
王国维的人生三境界,这一生至少当一次傻瓜
一、人生三境界 古今之成大事业、大学问者,必经过三种之境界。“昨夜西风凋碧树,独上高楼,望尽天涯路。”此第一境也。“衣带渐宽终不悔,为伊消得人憔悴。”此第二境也。“众里寻他千百度,蓦然回首,那人却…...
Jmeter二次开发实操问题汇总(JDK问题,jar包问题)
前提 之前写过一篇文章:https://qa-lsq.blog.csdn.net/article/details/119782694 只是简单尝试了一下生成一个随机手机号码。 但是如果在工作中一个实际场景要用的二次开发,可能会遇到一些问题。 比如这样一个场景: Mobile或者前端调用部分…...
网络安全B模块(笔记详解)- 数字取证
数据分析数字取证-attack 1.使用Wireshark查看并分析Windows 7桌面下的attack.pcapng数据包文件,通过分析数据包attack.pcapng找出恶意用户的IP地址,并将恶意用户的IP地址作为Flag(形式:[IP地址])提交; 解析:http.request.method==POST Flag:[172.16.1.102] 2.继续…...
阿里云服务器8080端口安全组开通图文教程
阿里云服务器8080端口开放在安全组中放行,Tomcat默认使用8080端口,8080端口也用于www代理服务,阿腾云atengyun.com以8080端口为例来详细说下阿里云服务器8080端口开启教程教程: 阿里云服务器8080端口开启教程 阿里云服务器8080端…...
vmlinux, vmlinux.bin, bzImage; cmake的find_package(Clang)新增了哪些变量( 比较两次记录的所有变量差异)
vmlinux, vmlinux.bin, bzImage cd /bal/linux-stable/ file vmlinux #vmlinux: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=b99bbd9dda1ec2751da246d4a7ae4e6fcf7d789b, not stripped #文件大小 20MB, 19940148Bfile ar…...
webpack配置入门
webpack是需要一定配置才能使用的,否则无任何效果。在开始webpack学习之前必须理解以下5个核心概念。 5大核心配置 1.entry(入口) webpack从那个文件开始打包,支持单文件入口(vue和react脚手架单入口)和多文件入口 2.output(输…...
Elasticsearch 8.X进阶搜索之“图搜图”实战
Elasticsearch 8.X “图搜图”实战 1、什么是图搜图? "图搜图"指的是通过图像搜索的一种方法,用户可以通过上传一张图片,搜索引擎会返回类似或者相关的图片结果。这种搜索方式不需要用户输入文字,而是通过比较图片的视…...
LLM之RAG实战(十三)| 利用MongoDB矢量搜索实现RAG高级检索
想象一下,你是一名侦探,身处庞大的信息世界,试图在堆积如山的数据中找到隐藏的一条重要线索,这就是检索增强生成(RAG)发挥作用的地方,它就像你在人工智能和语言模型世界中的可靠助手。但即使是最…...
UI动效设计师通往高薪之路,AE设计从基础到进阶教学
一、教程描述 UI动效设计,顾名思义即动态效果的设计,用户界面上所有运动的效果,也可以视其为界面设计与动态设计的交集,或者可以简单理解为UI设计中的动画效果,是UI设计中不可或缺的组成部分。现在UI设计的要求越来越…...
APK多渠道加固打包笔记之360加固宝
知识储备 首先需要知道V1,V2,V3签名的区别,可以参考之前的文章:AndroidV1,V2,V3签名原理详解 前言:一般开发者会指定使用自己创建的证书,如果没有指定,则会默认使用系统的证书,该默认的证书存储在C:\Users…...
编程天赋和努力哪个更重要?
编程天赋和努力在编程中都非常重要,但它们的侧重点不同。 编程天赋通常指的是与生俱来的、在逻辑思维、抽象思维、创造力等方面的能力,这些能力可以帮助程序员更快地理解问题、更高效地设计和实现解决方案。天赋的确可以帮助程序员更容易地入门和更快地掌…...
亳州市网站建设客服电话/seo关键词优化技术
哨兵模式哨兵哨兵简介主机“宕机”哨兵哨兵的作用启用哨兵模式配置哨兵哨兵工作原理主从切换阶段一:监控阶段阶段二:通知阶段阶段三:故障转移阶段主从切换总结集群集群简介现状问题集群架构集群作用Redis集群结构设计数据存储设计集群内部通讯…...
php企业网站无限制源码/免费网页在线客服系统
Docker是一个客户端-服务器架构,Docker客户端与守护进程进行通信,Docker守护进程大大提升了docker容器的构建和运行。Docker客户端和守护进程可以在同一个系统上运行,也可以远程连接Docker的守护进程。他们之间使用Rest API,通过U…...
建免费的网站/百度图片搜索引擎入口
前言: 一开始接触redux的时候最令我记住的一句话是:You Might Not Need Redux(那我还写这篇文章干嘛?手动滑稽) 回归正题,本文主要是围绕redux的作者Dan的视频,由浅入深了解redux redux基础用法…...
网站地图怎么设置/汕头seo关键词排名
介绍OSPF 开放最短路径优先 (OSPF) 协议是一种链路状态路由协议,旨在替代距离矢量路由协议 RIP。OSPF 是一种无类路由协议,它使用区域概念实现可扩展性。RFC 2328 将 OSPF 度量定义为一个独立的值,该值称为开销。 Cisco IOS 使用带宽作为 OSP…...
怎么做网站劫持/免费建立个人网站官网
组合总和 题目描述:给定一个无重复元素的数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。 candidates 中的数字可以无限制重复被选取。 说明: 所有数字(包括 target)都是…...
企业网站空间购买/电子商务是干什么的
本文默认读者已经对Docker有一定了解,且清楚使用Docker进行部署的优势。 1.安装Docker(Mac) 官网:https://docs.docker.com/docker-for-mac/install/ 1.1 下载 Docker for Mac 地址:https://store.docker.com/editions…...