learn.lianglianglee.com/专栏/SpringCloud微服务实战(完)/09 热更新一样更新服务的参数配置——分布式配置中心.md.html
2022-05-11 18:57:05 +08:00

813 lines
22 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="/static/favicon.png">
<title>09 热更新一样更新服务的参数配置——分布式配置中心.md.html</title>
<!-- Spectre.css framework -->
<link rel="stylesheet" href="/static/index.css">
<!-- theme css & js -->
<meta name="generator" content="Hexo 4.2.0">
</head>
<body>
<div class="book-container">
<div class="book-sidebar">
<div class="book-brand">
<a href="/">
<img src="/static/favicon.png">
<span>技术文章摘抄</span>
</a>
</div>
<div class="book-menu uncollapsible">
<ul class="uncollapsible">
<li><a href="/" class="current-tab">首页</a></li>
</ul>
<ul class="uncollapsible">
<li><a href="../">上一级</a></li>
</ul>
<ul class="uncollapsible">
<li>
<a href="/专栏/SpringCloud微服务实战/00 开篇导读.md.html">00 开篇导读.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/01 以真实“商场停车”业务切入——需求分析.md.html">01 以真实“商场停车”业务切入——需求分析.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/02 具象业务需求再抽象分解——系统设计.md.html">02 具象业务需求再抽象分解——系统设计.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/03 第一个 Spring Boot 子服务——会员服务.md.html">03 第一个 Spring Boot 子服务——会员服务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/04 如何维护接口文档供外部调用——在线接口文档管理.md.html">04 如何维护接口文档供外部调用——在线接口文档管理.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html">05 认识 Spring Cloud 与 Spring Cloud Alibaba 项目.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/06 服务多不易管理如何破——服务注册与发现.md.html">06 服务多不易管理如何破——服务注册与发现.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/07 如何调用本业务模块外的服务——服务调用.md.html">07 如何调用本业务模块外的服务——服务调用.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html">08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/SpringCloud微服务实战/09 热更新一样更新服务的参数配置——分布式配置中心.md.html">09 热更新一样更新服务的参数配置——分布式配置中心.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/10 如何高效读取计费规则等热数据——分布式缓存.md.html">10 如何高效读取计费规则等热数据——分布式缓存.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html">11 多实例下的定时任务如何避免重复执行——分布式定时任务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/12 同一套服务如何应对不同终端的需求——服务适配.md.html">12 同一套服务如何应对不同终端的需求——服务适配.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html">13 采用消息驱动方式处理扣费通知——集成消息中间件.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html">14 Spring Cloud 与 Dubbo 冲突吗——强强联合.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html">15 破解服务中共性问题的繁琐处理方式——接入 API 网关.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/16 服务压力大系统响应慢如何破——网关流量控制.md.html">16 服务压力大系统响应慢如何破——网关流量控制.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/17 集成网关后怎么做安全验证——统一鉴权.md.html">17 集成网关后怎么做安全验证——统一鉴权.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/18 多模块下的接口 API 如何统一管理——聚合 API.md.html">18 多模块下的接口 API 如何统一管理——聚合 API.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/19 数据分库后如何确保数据完整性——分布式事务.md.html">19 数据分库后如何确保数据完整性——分布式事务.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/20 优惠券如何避免超兑——引入分布式锁.md.html">20 优惠券如何避免超兑——引入分布式锁.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/21 如何查看各服务的健康状况——系统应用监控.md.html">21 如何查看各服务的健康状况——系统应用监控.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/22 如何确定一次完整的请求过程——服务链路跟踪.md.html">22 如何确定一次完整的请求过程——服务链路跟踪.md.html</a>
</li>
<li>
<a href="/专栏/SpringCloud微服务实战/23 结束语.md.html">23 结束语.md.html</a>
</li>
</ul>
</div>
</div>
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
<div class="sidebar-toggle-inner"></div>
</div>
<script>
function add_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.add('show')
}
function remove_inner() {
let inner = document.querySelector('.sidebar-toggle-inner')
inner.classList.remove('show')
}
function sidebar_toggle() {
let sidebar_toggle = document.querySelector('.sidebar-toggle')
let sidebar = document.querySelector('.book-sidebar')
let content = document.querySelector('.off-canvas-content')
if (sidebar_toggle.classList.contains('extend')) { // show
sidebar_toggle.classList.remove('extend')
sidebar.classList.remove('hide')
content.classList.remove('extend')
} else { // hide
sidebar_toggle.classList.add('extend')
sidebar.classList.add('hide')
content.classList.add('extend')
}
}
function open_sidebar() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.add('show')
overlay.classList.add('show')
}
function hide_canvas() {
let sidebar = document.querySelector('.book-sidebar')
let overlay = document.querySelector('.off-canvas-overlay')
sidebar.classList.remove('show')
overlay.classList.remove('show')
}
</script>
<div class="off-canvas-content">
<div class="columns">
<div class="column col-12 col-lg-12">
<div class="book-navbar">
<!-- For Responsive Layout -->
<header class="navbar">
<section class="navbar-section">
<a onclick="open_sidebar()">
<i class="icon icon-menu"></i>
</a>
</section>
</header>
</div>
<div class="book-content" style="max-width: 960px; margin: 0 auto;
overflow-x: auto;
overflow-y: hidden;">
<div class="book-post">
<p id="tip" align="center"></p>
<div><h1>09 热更新一样更新服务的参数配置——分布式配置中心</h1>
<p>几乎每个项目中都涉及到配置参数或配置文件,如何避免硬编码,通过代码外的配置,来提高可变参数的安全性、时效性,弹性化配置显得尤其重要。本篇就带你一起聊聊软件项目的配置问题,特别是微服务架构风格下的配置问题。</p>
<h3>参数配置的演变</h3>
<p>早期软件开发时,当然也包括现在某些小伙伴开发时,存在硬编码的情况,将一些可变参数写死在代码中。弊端也显而易见,当参数变更时,必须重新构建编译代码,维护成本相当高。</p>
<p>后来,业界形成规则,将一些可变参数抽取出来,形成多种格式的配置文件,如 properties、yml、json、xml 等等,将这些参数集中管理起来,发生变更时,只需要更新配置文件即可,不再需要重新编码代码、构建发布代码块,明显比硬编码强大太。弊端也有:</p>
<ul>
<li>关键信息暴露在配置文件中,安全性低。</li>
<li>配置文件变更后,服务也面临重启的问题</li>
</ul>
<p>再接着出现了分布式配置,将配置参数从项目中解耦出来,项目使用时,及时向配置中心获取或配置中心变更时向项目中推送,优势很明显:</p>
<ol>
<li>省去了关键信息暴露的问题</li>
<li>配置参数无须与代码模块耦合在一起,可以灵活的管理权限,安全性更高</li>
<li>配置可以做到实时生效,对一些规则复杂的代码场景很有帮助</li>
<li>面对多环境部署时,能够轻松应对</li>
</ol>
<p>微服务场景下,我们也更倾向于采用分布式配置中心的模式,来管理配置,当服务实例增多时,完全不用担心配置变得复杂。</p>
<h3>开源组件介绍</h3>
<p>Spring Cloud Config 就是 Spring Cloud 项目下的分布式配置组件,当然其自身是没有办法完成配置功能的,需要借助 Git 或 MQ 等组件来共同完成,复杂度略高。</p>
<p>业内不少公司开源不少分布式配置组件,比如携程的 Apollo阿波罗淘宝的 Diamond百度的 Disconf360 的 QConf ,阿里的 Nacos 等等,也可以基于 Zookeeper 等组件进行开发完成,技术选型产品还是比较多的。</p>
<p>本案例中采用 Nacos 作为选型,为什么选 Nacos ?首先是 Spring Cloud Alibaba 项目下的一员,与生态贴合紧密 。其次Nacos 作为服务注册中心,已经在项目使用,它兼有配置中心的功能,无须额外引入第三方组件,增加系统复杂度,一个组件完成 Spring Cloud Config 和 Eureka 两个组件的功能。</p>
<p>Nacos 在基础层面通过<code>DataId</code><code>Group</code>来定位唯一的配置项,支持不同的配置格式,如 JSON , TEXT , YMAL 等等,不同的格式,遵从各自的语法规则即可。</p>
<h3>Nacos 配置管理</h3>
<blockquote>
<p>拿用户手机号绑定系统的功能为例,商场做促销活动,当天用户绑定手机号,并开通月卡,积分赠送翻倍,还有机会抽取活动大礼包,活动结束,恢复成原样。这是经常见到的玩法吧。</p>
</blockquote>
<p>在 parking-member 项目的 pom.xml 中增加 jar 引入,不过前期我们已经应用到了 nacos 的服务注册中心功能,已经被引入到项目中去。</p>
<pre><code class="language-xml"> &lt;dependency&gt;
&lt;groupId&gt;com.alibaba.cloud&lt;/groupId&gt;
&lt;artifactId&gt;spring-cloud-starter-alibaba-nacos-config&lt;/artifactId&gt;
&lt;/dependency&gt;
</code></pre>
<p>下面就将将与 nacos 的连接配置进去,由于 Spring Boot 项目中存在两种配置文件,一种是 application 的配置,一种是 bootstrap 的配置,究竟配置在哪个文件中呢?我们先来看 bootstrap 与 application 有什么区别。</p>
<p><img src="assets/2020-05-05-021555.jpg" alt="img" /></p>
<p>(截图来源于官方文档:<a href="https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#_the_bootstrap_application_context">https://cloud.spring.io/spring-cloud-static/Greenwich.SR1/single/spring-cloud.html#<em>the</em>bootstrap<em>application</em>context</a>)</p>
<p>nacos 与 spring-cloud-config 配置上保持一致,必须将采用 bootstrap.yml/properties 文件,优先加载该配置,填写在 application.properties/yml 中无效。</p>
<p>bootstrap.properties</p>
<pre><code class="language-properties">spring.cloud.nacos.config.server-addr=127.0.0.1:8848
spring.profiles.active=dev
spring.application.name=member-service
spring.cloud.nacos.config.shared-data-ids=${spring.application.name}-${spring.profiles.active}.properties
spring.cloud.nacos.config.refreshable-dataids=${spring.application.name}-${spring.profiles.active}.properties
</code></pre>
<p>此时采用 spring.application.namespring.application.name{spring.profiles.active}动态配置的原因,是为后期进行多环境构建做准备,当然也可以直接写死 nacos 配置文件但这样更不利于扩展维护。refreshable-dataids 配置项为非选必须,但如果缺失,所有的 nacos 配置项将不会被自动刷新,必须采用另外的方式刷新配置项,才能正常应用到服务中。</p>
<h4>代码中使用配置项</h4>
<p>打开会员绑定手机号的方法处,定义一个内部变量,接受配置中心的参数值。可以直接采用 Spring 的 [@Value ]方式取值即可,在 Controller 层或是 Service 层都可以使用,类中必须采用 [@RefreshScope ]注解,才能将值实时同步过来。</p>
<p>在 nacos 中定义两个配置项 onoff<em>bindmobile 和 on</em>bindmobile_mulriple分别代表一个开关和积分倍数。同时定义两个局部变量接受 nacos 中两个配置项的值。</p>
<blockquote>
<p>即使参数外部化配置后,代码中也必须预留出位置,供代码逻辑进行切换,也就是事先这个逻辑是预置进去的,是否执行逻辑,逻辑中相关分支等等,完全看配置参数的值。 比如注册时,往往会夹杂一些送积分、注册送优惠券及多大面额的优惠券,注册有机会抽奖等,等一个活动结束自动切换到另一个活动,如果事先未预置进去,代码是没有办法执行这个逻辑的。</p>
</blockquote>
<p>整体代码如下:</p>
<pre><code class="language-java"> @Value(&quot;${onoff_bindmobile}&quot;)
private boolean onoffBindmobile;
@Value(&quot;${on_bindmobile_mulriple}&quot;)
private int onBindmobileMulriple;
@Override
public int bindMobile(String json) throws BusinessException{
Member member = JSONObject.parseObject(json, Member.class);
int rtn = memberMapper.insertSelective(member);
//invoke another service
if (rtn &gt; 0) {
MemberCard card = new MemberCard();
card.setMemberId(member.getId());
//判定开关是否打开
if (onoffBindmobile) {
//special logic
card.setCurQty(&quot;&quot; +50 * onBindmobileMulriple);
}else {
//normal logic
card.setCurQty(&quot;50&quot;);
}
memberCardClient.addCard(JSONObject.toJSONString(card));
log.info(&quot;creata member card suc!&quot;);
}
return rtn;
}
</code></pre>
<h4>Nacos 中配置参数项</h4>
<p>bootstrap 配置文件中已经做了约定:配置项的 Key 与 Value 值的格式为 properties 文件,打开 nacos 控制台,进行&quot;配置管理&quot;-&gt;&quot;配置列表&quot;页面,点击右上角&quot;新增&quot;</p>
<p><img src="assets/2020-05-05-021557.jpg" alt="img" /></p>
<p>输入上文的约定的两个配置项,配置项可以应对多环境配置要求,在其它环境,建立对应的配置即可,输入 Data ID 比如 member-service-prd.properties 或者 member-service-uat.propertiesGroup 采用默认值即可,当然如果需要分组的话,需要定义 Group 值,保存即可。</p>
<blockquote>
<p>配置项的 value 类型需要与代码中约定的一致,否则解析时会出错。</p>
</blockquote>
<p>若要变更配置项的值,修改后,发布即可,可以看到配置项的值是立即生效的。主要是借助于上文中提到的 [@RefreshScope ]注解,与 bootstrap 配置文件中 spring.cloud.nacos.config.refreshable-dataids 配置来完成的。如果缺失其中的一个,配置项生效只有重启应用,这与我们打造程序的易维护性、健壮性是相违背的。</p>
<p>可以将相应的数据库连接、第三方应用接入的密钥、经常发生变更的参数项都填充到配置中心,借助配置中心自身的权限控制,可以确保敏感项不泄露,同时针对不同的部署环境也可以很好的做好配置隔离。</p>
<p>至此,基于 Nacos 的配置中心配置完成,依照此配置,可以正常的迁移到其它服务中去。留下个思考题,在项目中采用类似 properties 文件的外部化配置,还是比较常见的,如何确保当中的关键信息不被泄漏呢,比如数据库的用户名、密码,第三方核心的 Key 等等。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/SpringCloud微服务实战/08 服务响应慢或服务不可用怎么办——快速失败与服务降级.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/SpringCloud微服务实战/10 如何高效读取计费规则等热数据——分布式缓存.md.html">下一页</a>
</div>
</div>
</div>
</div>
</div>
</div>
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
</div>
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"7099759a2ef03d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
</body>
<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag() {
dataLayer.push(arguments);
}
gtag('js', new Date());
gtag('config', 'G-NPSEEVD756');
var path = window.location.pathname
var cookie = getCookie("lastPath");
console.log(path)
if (path.replace("/", "") === "") {
if (cookie.replace("/", "") !== "") {
console.log(cookie)
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
}
} else {
setCookie("lastPath", path)
}
function setCookie(cname, cvalue) {
var d = new Date();
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
var expires = "expires=" + d.toGMTString();
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
}
function getCookie(cname) {
var name = cname + "=";
var ca = document.cookie.split(';');
for (var i = 0; i < ca.length; i++) {
var c = ca[i].trim();
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
}
return "";
}
</script>
</html>