CategoryResourceRepost/极客时间专栏/设计模式之美/开源与项目实战:项目实战/98 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(实现).md
louzefeng d3828a7aee mod
2024-07-11 05:50:32 +00:00

473 lines
19 KiB
Markdown
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.

<audio id="audio" title="98 | 项目实战三:设计实现一个支持自定义规则的灰度发布组件(实现)" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/34/82/3428e6f0005a899ae9f3479e75c1c782.mp3"></audio>
上两节课,我们讲解了灰度组件的需求和设计思路。不管是之前讲过的限流、幂等框架,还是现在正在讲的灰度组件,这些框架、组件、类库的功能性需求都不复杂,相反,非功能性需求是开发的重点、难点。
今天我们按照上节课给出的灰度组件的设计思路讲解如何进行编码实现。不过今天对实现的讲解跟前面两个实战项目有所不同。在前面两个项目中我都是手把手地从最基础的MVP代码讲起然后讲解如何review代码发现问题、重构代码解决问题最终得到一份还算高质量的代码。考虑到已经有前面两个项目的学习和锻炼了你应该对开发套路、思考路径很熟悉了所以今天我们换个讲法就不从最基础的讲起了而是重点讲解实现思路。
话不多说,让我们正式开始今天的学习吧!
## 灰度组件功能需求整理
针对上两节课给出的开发需求和设计思路我们还是按照老套路从中剥离出V1版本要实现的内容。为了方便我讲解和你查看我把灰度组件的开发需求和设计思路重新整理罗列了一下放到了这里。
### 1.灰度规则的格式和存储方式
我们希望支持不同格式JSON、YAML、XML等、不同存储方式本地配置文件、Redis、Zookeeper、或者自研配置中心等的灰度规则配置方式。实际上这一点跟之前的限流框架中限流规则的格式和存储方式完全一致代码实现也是相同的所以在接下来的讲解中就不重复啰嗦了你可以回过头去看下[第92讲](https://time.geekbang.org/column/article/243961)。
### 2.灰度规则的语法格式
我们支持三种灰度规则语法格式具体值比如893、区间值比如1020-1120、比例值比如%30。除此之外对于更加复杂的灰度规则比如只对30天内购买过某某商品并且退货次数少于10次的用户进行灰度我们通过编程的方式来实现。
### 3.灰度规则的内存组织方式
类似于限流框架中的限流规则我们需要把灰度规则组织成支持快速查找的数据结构能够快速判定某个灰度对象darkTarget比如用户ID是否落在灰度规则设定的范围内。
### 4.灰度规则热更新
修改了灰度规则之后,我们希望不重新部署和重启系统,新的灰度规则就能生效,所以,我们需要支持灰度规则热更新。
在V1版本中对于第一点灰度规则的格式和存储方式我们只支持YAML格式本地文件的配置存储方式。对于剩下的三点我们都要进行实现。考虑到V1版本要实现的内容比较多我们分两步来实现代码第一步先将大的流程、框架搭建好第二步再进一步添加、丰富、优化功能。
## 实现灰度组件基本功能
在第一步中我们先实现基于YAML格式的本地文件的灰度规则配置方式以及灰度规则热更新并且只支持三种基本的灰度规则语法格式。基于编程实现灰度规则的方式我们留在第二步实现。
我们先把这个基本功能的开发需求用代码实现出来。它的目录结构及其Demo示例如下所示。代码非常简单只包含4个类。接下来我们针对每个类再详细讲解一下。
```
// 代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
// Demo示例
public class DarkDemo {
public static void main(String[] args) {
DarkLaunch darkLaunch = new DarkLaunch();
DarkFeature darkFeature = darkLaunch.getDarkFeature(&quot;call_newapi_getUserById&quot;);
System.out.println(darkFeature.enabled());
System.out.println(darkFeature.dark(893));
}
}
// 灰度规则配置(dark-rule.yaml)放置在classpath路径下
features:
- key: call_newapi_getUserById
enabled: true
rule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser
enabled: true
rule: {1391198723, %10}
- key: newalgo_loan
enabled: true
rule: {0-1000}
```
从Demo代码中我们可以看出对于业务系统来说灰度组件的两个直接使用的类是DarkLaunch类和DarkFeature类。
**我们先来看DarkLaunch类。**这个类是灰度组件的最顶层入口类。它用来组装其他类对象,串联整个操作流程,提供外部调用的接口。
DarkLaunch类先读取灰度规则配置文件映射为内存中的Java对象DarkRuleConfig然后再将这个中间结构构建成一个支持快速查询的数据结构DarkRule。除此之外它还负责定期更新灰度规则也就是前面提到的灰度规则热更新。
为了避免更新规则和查询规则的并发执行冲突在更新灰度规则的时候我们并非直接操作老的DarkRule而是先创建一个新的DarkRule然后等新的DarkRule都构建好之后再“瞬间”赋值给老的DarkRule。你可以结合着下面的代码一块看下。
```
public class DarkLaunch {
private static final Logger log = LoggerFactory.getLogger(DarkLaunch.class);
private static final int DEFAULT_RULE_UPDATE_TIME_INTERVAL = 60; // in seconds
private DarkRule rule;
private ScheduledExecutorService executor;
public DarkLaunch(int ruleUpdateTimeInterval) {
loadRule();
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
loadRule();
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}
public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}
private void loadRule() {
// 将灰度规则配置文件dark-rule.yaml中的内容读取DarkRuleConfig中
InputStream in = null;
DarkRuleConfig ruleConfig = null;
try {
in = this.getClass().getResourceAsStream(&quot;/dark-rule.yaml&quot;);
if (in != null) {
Yaml yaml = new Yaml();
ruleConfig = yaml.loadAs(in, DarkRuleConfig.class);
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error(&quot;close file error:{}&quot;, e);
}
}
}
if (ruleConfig == null) {
throw new RuntimeException(&quot;Can not load dark rule.&quot;);
}
// 更新规则并非直接在this.rule上进行
// 而是通过创建一个新的DarkRule然后赋值给this.rule
// 来避免更新规则和规则查询的并发冲突问题
DarkRule newRule = new DarkRule(ruleConfig);
this.rule = newRule;
}
public DarkFeature getDarkFeature(String featureKey) {
DarkFeature darkFeature = this.rule.getDarkFeature(featureKey);
return darkFeature;
}
}
```
**我们再来看下DarkRuleConfig类。**这个类功能非常简单,只是用来将灰度规则映射到内存中。具体的代码如下所示:
```
public class DarkRuleConfig {
private List&lt;DarkFeatureConfig&gt; features;
public List&lt;DarkFeatureConfig&gt; getFeatures() {
return this.features;
}
public void setFeatures(List&lt;DarkFeatureConfig&gt; features) {
this.features = features;
}
public static class DarkFeatureConfig {
private String key;
private boolean enabled;
private String rule;
// 省略getter、setter方法
}
}
```
从代码中我们可以看出来DarkRuleConfig类嵌套了一个内部类DarkFeatureConfig。这两个类跟配置文件的两层嵌套结构完全对应。我把对应关系标注在了下面的示例中你可以对照着代码看下。
```
&lt;!--对应DarkRuleConfig--&gt;
features:
- key: call_newapi_getUserById &lt;!--对应DarkFeatureConfig--&gt;
enabled: true
rule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser &lt;!--对应DarkFeatureConfig--&gt;
enabled: true
rule: {1391198723, %10}
- key: newalgo_loan &lt;!--对应DarkFeatureConfig--&gt;
enabled: true
rule: {0-1000}
```
**我们再来看下DarkRule。**DarkRule包含所有要灰度的业务功能的灰度规则。它用来支持根据业务功能标识feature key快速查询灰度规则DarkFeature。代码也比较简单具体如下所示
```
public class DarkRule {
private Map&lt;String, DarkFeature&gt; darkFeatures = new HashMap&lt;&gt;();
public DarkRule(DarkRuleConfig darkRuleConfig) {
List&lt;DarkRuleConfig.DarkFeatureConfig&gt; darkFeatureConfigs = darkRuleConfig.getFeatures();
for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {
darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));
}
}
public DarkFeature getDarkFeature(String featureKey) {
return darkFeatures.get(featureKey);
}
}
```
**我们最后来看下DarkFeature类。**DarkFeature类表示每个要灰度的业务功能的灰度规则。DarkFeature将配置文件中灰度规则解析成一定的结构比如RangeSet方便快速判定某个灰度对象是否落在灰度规则范围内。具体的代码如下所示
```
public class DarkFeature {
private String key;
private boolean enabled;
private int percentage;
private RangeSet&lt;Long&gt; rangeSet = TreeRangeSet.create();
public DarkFeature(DarkRuleConfig.DarkFeatureConfig darkFeatureConfig) {
this.key = darkFeatureConfig.getKey();
this.enabled = darkFeatureConfig.getEnabled();
String darkRule = darkFeatureConfig.getRule().trim();
parseDarkRule(darkRule);
}
@VisibleForTesting
protected void parseDarkRule(String darkRule) {
if (!darkRule.startsWith(&quot;{&quot;) || !darkRule.endsWith(&quot;}&quot;)) {
throw new RuntimeException(&quot;Failed to parse dark rule: &quot; + darkRule);
}
String[] rules = darkRule.substring(1, darkRule.length() - 1).split(&quot;,&quot;);
this.rangeSet.clear();
this.percentage = 0;
for (String rule : rules) {
rule = rule.trim();
if (StringUtils.isEmpty(rule)) {
continue;
}
if (rule.startsWith(&quot;%&quot;)) {
int newPercentage = Integer.parseInt(rule.substring(1));
if (newPercentage &gt; this.percentage) {
this.percentage = newPercentage;
}
} else if (rule.contains(&quot;-&quot;)) {
String[] parts = rule.split(&quot;-&quot;);
if (parts.length != 2) {
throw new RuntimeException(&quot;Failed to parse dark rule: &quot; + darkRule);
}
long start = Long.parseLong(parts[0]);
long end = Long.parseLong(parts[1]);
if (start &gt; end) {
throw new RuntimeException(&quot;Failed to parse dark rule: &quot; + darkRule);
}
this.rangeSet.add(Range.closed(start, end));
} else {
long val = Long.parseLong(rule);
this.rangeSet.add(Range.closed(val, val));
}
}
}
public boolean enabled() {
return this.enabled;
}
public boolean dark(long darkTarget) {
boolean selected = this.rangeSet.contains(darkTarget);
if (selected) {
return true;
}
long reminder = darkTarget % 100;
if (reminder &gt;= 0 &amp;&amp; reminder &lt; this.percentage) {
return true;
}
return false;
}
public boolean dark(String darkTarget) {
long target = Long.parseLong(darkTarget);
return dark(target);
}
}
```
## 添加、优化灰度组件功能
在第一步中,我们完成了灰度组件的基本功能。在第二步中,我们再实现基于编程的灰度规则配置方式,用来支持更加复杂、更加灵活的灰度规则。
我们需要对于第一步实现的代码进行一些改造。改造之后的代码目录结构如下所示。其中DarkFeature、DarkRuleConfig的基本代码不变新增了IDarkFeature接口DarkLaunch、DarkRule的代码有所改动用来支持编程实现灰度规则。
```
// 第一步的代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类)
--DarkFeature(每个feature的灰度规则)
--DarkRule(灰度规则)
--DarkRuleConfig(用来映射配置到内存中)
// 第二步的代码目录结构
com.xzg.darklaunch
--DarkLaunch(框架的最顶层入口类,代码有改动)
--IDarkFeature(抽象接口)
--DarkFeature(实现IDarkFeature接口基于配置文件的灰度规则代码不变)
--DarkRule(灰度规则,代码有改动)
--DarkRuleConfig(用来映射配置到内存中,代码不变)
```
我们先来看下IDarkFeature接口它用来抽象从配置文件中得到的灰度规则以及编程实现的灰度规则。具体代码如下所示
```
public interface IDarkFeature {
boolean enabled();
boolean dark(long darkTarget);
boolean dark(String darkTarget);
}
```
基于这个抽象接口业务系统可以自己编程实现复杂的灰度规则然后添加到DarkRule中。为了避免配置文件中的灰度规则热更新时覆盖掉编程实现的灰度规则在DarkRule中我们对从配置文件中加载的灰度规则和编程实现的灰度规则分开存储。按照这个设计思路我们对DarkRule类进行重构。重构之后的代码如下所示
```
public class DarkRule {
// 从配置文件中加载的灰度规则
private Map&lt;String, IDarkFeature&gt; darkFeatures = new HashMap&lt;&gt;();
// 编程实现的灰度规则
private ConcurrentHashMap&lt;String, IDarkFeature&gt; programmedDarkFeatures = new ConcurrentHashMap&lt;&gt;();
public void addProgrammedDarkFeature(String featureKey, IDarkFeature darkFeature) {
programmedDarkFeatures.put(featureKey, darkFeature);
}
public void setDarkFeatures(Map&lt;String, IDarkFeature&gt; newDarkFeatures) {
this.darkFeatures = newDarkFeatures;
}
public IDarkFeature getDarkFeature(String featureKey) {
IDarkFeature darkFeature = programmedDarkFeatures.get(featureKey);
if (darkFeature != null) {
return darkFeature;
}
return darkFeatures.get(featureKey);
}
}
```
因为DarkRule代码有所修改对应地DarkLaunch的代码也需要做少许改动主要有一处修改和一处新增代码具体如下所示我在代码中都做了注释就不再重复解释了。
```
public class DarkLaunch {
private static final Logger log = LoggerFactory.getLogger(DarkLaunch.class);
private static final int DEFAULT_RULE_UPDATE_TIME_INTERVAL = 60; // in seconds
private DarkRule rule = new DarkRule();
private ScheduledExecutorService executor;
public DarkLaunch(int ruleUpdateTimeInterval) {
loadRule();
this.executor = Executors.newSingleThreadScheduledExecutor();
this.executor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
loadRule();
}
}, ruleUpdateTimeInterval, ruleUpdateTimeInterval, TimeUnit.SECONDS);
}
public DarkLaunch() {
this(DEFAULT_RULE_UPDATE_TIME_INTERVAL);
}
private void loadRule() {
InputStream in = null;
DarkRuleConfig ruleConfig = null;
try {
in = this.getClass().getResourceAsStream(&quot;/dark-rule.yaml&quot;);
if (in != null) {
Yaml yaml = new Yaml();
ruleConfig = yaml.loadAs(in, DarkRuleConfig.class);
}
} finally {
if (in != null) {
try {
in.close();
} catch (IOException e) {
log.error(&quot;close file error:{}&quot;, e);
}
}
}
if (ruleConfig == null) {
throw new RuntimeException(&quot;Can not load dark rule.&quot;);
}
// 修改:单独更新从配置文件中得到的灰度规则,不覆盖编程实现的灰度规则
Map&lt;String, IDarkFeature&gt; darkFeatures = new HashMap&lt;&gt;();
List&lt;DarkRuleConfig.DarkFeatureConfig&gt; darkFeatureConfigs = ruleConfig.getFeatures();
for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {
darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));
}
this.rule.setDarkFeatures(darkFeatures);
}
// 新增:添加编程实现的灰度规则的接口
public void addProgrammedDarkFeature(String featureKey, IDarkFeature darkFeature) {
this.rule.addProgrammedDarkFeature(featureKey, darkFeature);
}
public IDarkFeature getDarkFeature(String featureKey) {
IDarkFeature darkFeature = this.rule.getDarkFeature(featureKey);
return darkFeature;
}
}
```
灰度组件的代码实现就讲完了。我们再通过一个Demo来看下目前实现的灰度组件该如何使用。结合着Demo再去理解上面的代码会更容易些。Demo代码如下所示
```
// 灰度规则配置(dark-rule.yaml)放到classpath路径下
features:
- key: call_newapi_getUserById
enabled: true
rule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser
enabled: true
rule: {1391198723, %10}
- key: newalgo_loan
enabled: true
rule: {0-100}
// 编程实现的灰度规则
public class UserPromotionDarkRule implements IDarkFeature {
@Override
public boolean enabled() {
return true;
}
@Override
public boolean dark(long darkTarget) {
// 灰度规则自己想怎么写就怎么写
return false;
}
@Override
public boolean dark(String darkTarget) {
// 灰度规则自己想怎么写就怎么写
return false;
}
}
// Demo
public class Demo {
public static void main(String[] args) {
DarkLaunch darkLaunch = new DarkLaunch(); // 默认加载classpath下dark-rule.yaml文件中的灰度规则
darkLaunch.addProgrammedDarkFeature(&quot;user_promotion&quot;, new UserPromotionDarkRule()); // 添加编程实现的灰度规则
IDarkFeature darkFeature = darkLaunch.getDarkFeature(&quot;user_promotion&quot;);
System.out.println(darkFeature.enabled());
System.out.println(darkFeature.dark(893));
}
}
```
## 重点回顾
好了,今天的内容到此就讲完了。我们一块来总结回顾一下,你需要重点掌握的内容。
到今天为止,项目实战环节就彻底结束了。在这一部分中,我们通过限流、幂等、灰度这三个实战项目,带你从需求分析、系统设计、代码实现这三个环节,学习了如何进行功能性、非功能性需求分析,如何通过合理的设计,完成功能性需求的同时,满足非功能性需求,以及如何编写高质量的代码实现。
实际上,项目本身的分析、设计、实现并不重要,不必对细节过于纠结。我希望通过这三个例子,分享我的思考路径、开发套路,让你借鉴并举一反三地应用到你平时的项目开发中。我觉得这才是最有价值的,才是你学习的重点。
如果你学完这一部分之后,对于项目中的一些通用的功能,能够开始下意识地主动思考代码复用的问题,考虑如何抽象成框架、类库、组件,并且对于如何开发,也不再觉得无从下手,而是觉得有章可循,那我觉得你就学到了这一部分的精髓。
## 课堂讨论
在DarkFeature类中灰度规则的解析代码设计的不够优雅你觉得问题在哪里呢又该如何重构呢
欢迎留言和我分享你的想法。如果有收获,也欢迎你把这篇文章分享给你的朋友。