设计模式学习笔记 - 项目实战三:设计实现一个支持自定义规则的灰度发布组件(实现)

概述

上两篇文章,我们讲解了灰度组件的需求和设计的思路。不管之前讲的限流、幂等框架,还是现在讲的灰度组件,功能性需求都不复杂,相反,非功能性需求是开发的重点。

本章,按照上篇文章的灰度组件的设计思路,讲解如何进行编码实现。不过,本章对实现的讲解,前前面两个实战项目有所不同。在前两个项目中,我们都是手把手从最基础的 MVP 代码将其。然后讲解如何 review 代码发现问题、重构代码解决问题,最终得到一份高质量的代码。考虑到前面两个项目的学习锻炼,你应该对开发套路、思考路径已经很熟悉了,所以,本章就不从最基础的将其了,而是重点讲解实现思路。


灰度组件功能需求整理

针对上连篇文章的开发需求和设计思路,我们还是按照老套路,从中剥离出 V1 版本要实现的内容。为了方便讲解,我把灰度组件的开发需求和设计思路,重新整理罗列了一下,放到了这里。

1.灰度规则的格式和存储方式

支持不同格式(JSON、YAML、XML 等)、不同存储方式(本地配置我呢间、Redis、Zookeeper、或者自研配置中心)的灰度规则配置方式。实际上,这一点和之前的限流框架中限流规则的格式和存储方式完全一致,代码实现也是相同的。所以这个就不重复了,你可以回头去看《限流框架(实现)》章节内容。

2.灰度组件的语法格式

我们支持三种灰度规则语法格式:具体值(比如 893)、区间值(比如 1020-1120)、比例值(比如 %30)。此外,对于更加复杂的灰度规则,比如只对 30 天内购买过某某商品并且退款次数少于 10 次的用户进行更改,我们通过编程的方式来实现。

3.灰度规则的内存组织方式

类似于限流框架中的限流规则,我们需要把灰度规则组织成支持快速查询的数据结构能够快速判定某个灰度对象(darkTarget,比如用户 ID),是否落在灰度规则设定的范围内。

4.灰度规则热更新

修改了灰度规则之后,我们不希望重新部署和重启系统,新的灰度规则就能生效,所以,我们需要支持灰度规则热更新。

在 V1 版本中,对于第一点灰度规则的合适和存储方式,我们只支持 YAML 格式本地文件的配置存储方式。对于剩下的三点,我们都要进行实现。考虑到 V1 版本要实现的内容比较多,我们分两步来实现代码,第一步先讲大的流程、框架搭建好,第二步再进一步添加、丰富、优化功能。

实现灰度组件基本功能

// 目录结构
com.example.darklaunch--DarkLaunch (框架的最顶层入口类)--DarkFeature (每个feature的灰度规则)--DarkRule (灰度规则)--DarkRuleConfig (用来映射配置到内存中)// Demo示例
public class Demo {public static void main(String[] args) {DarkLaunch darkLaunch = new DarkLaunch();DarkFeature darkFeature = darkLaunch.getDarkFeature("call_newapi_getUserById");System.out.println(darkFeature.enabled());System.out.println(darkFeature.dark(893));}
}// 灰度规则配置(dark-rule.yaml)放在classpath路径下
features:
- key: call_newapi_getUserByIdenabled: truerule: {893,342,1020-1120,%30}
- key: call_newapi_registerUserenabled: truerule: {1391198723, %10}
- key: newlog_loanenabled: truerule: {0-1000}

从 Demo 代码中,可以看出,对于业务系统来说,灰度组件的两个直接使用的类是 DarkLaunchDarkFeature

我们先来看 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 secondsprivate DarkRule rule;private ScheduledExecutorService executor;public DarkLaunch(int ruleUpdateTimeInterval) {loadRule();this.executor = Executors.newSingleThreadScheduledExecutor();this.executor.scheduleAtFixedRate(new Runnable() {@Overridepublic 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("dark-rule.yaml");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("close file error:", e);}}}if (ruleConfig == null) {throw new RuntimeException("can not load dark rule.");}// 更新规则并非直接在this.rule上进行// 而是通过创建一个新的 DarkRule,然后赋值给this.rule// 来避免更新规则和规则查询的并发冲突问题DarkRule darkRule = new DarkRule(ruleConfig);this.rule = darkRule;}public DarkFeature getDarkFeature(String featureKey) {return this.rule.getDarkFeature(featureKey);}
}

**再来看下 DarkRuleConfig 类。**这个类功能非常简单,只是用来将灰度规则映射到内存中。

public class DarkRuleConfig {private List<DarkFeatureConfig> features;public List<DarkFeatureConfig> getFeatures() {return features;}public void setFeatures(List<DarkFeatureConfig> features) {this.features = features;}public static class DarkFeatureConfig {private String key;private boolean enable;private String rule;public String getKey() {return key;}public void setKey(String key) {this.key = key;}public boolean isEnable() {return enable;}public void setEnable(boolean enable) {this.enable = enable;}public String getRule() {return rule;}public void setRule(String rule) {this.rule = rule;}}
}

从代码中,我们可以看出来,DarkRuleConfig 类嵌套了一个内部类 DarkFeatureConfig。这两个类跟配置文件中的两层嵌套结构完全对应 。

# 对应DarkRuleConfig
features:
- key: call_newapi_getUserById   # 对应DarkFeatureConfigenabled: truerule: {893,342,1020-1120,%30}
- key: call_newapi_registerUser  # 对应DarkFeatureConfigenabled: truerule: {1391198723, %10}
- key: newlog_loan               # 对应DarkFeatureConfigenabled: truerule: {0-1000}

再来看下 DarkRuleDarkRule 包含所有要灰度的业务功能的灰度规则。它用来支持根据业务功能标识(feature key),快速查询灰度规则(DarkFeture)。

public class DarkRule {private Map<String, DarkFeature> darkFeatures = new HashMap<>();public DarkRule(DarkRuleConfig darkRuleConfig) {List<DarkRuleConfig.DarkFeatureConfig> darkRuleConfigFeatures = darkRuleConfig.getFeatures();for (int i = 0; i < darkRuleConfigFeatures.size(); i++) {DarkRuleConfig.DarkFeatureConfig darkFeatureConfig = darkRuleConfigFeatures.get(i);darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));}}public DarkFeature getDarkFeature(String featureKey) {return this.darkFeatures.get(featureKey);}
}

最后,我们来看下 DarkFeature 类。 DarkFeature 类标识每个要灰度的业务功能的灰度规则。DarkFeature 将配置文件中的灰度规则,解析成一定的结构(比如 RangeSet),方便快速判定某个灰度对象是否落在灰度规则范围内。

public class DarkFeature {private String key;private boolean enabled;private int percentage;private RangeSet<Long> rangeSet = TreeRangeSet.create();public DarkFeature(DarkRuleConfig.DarkFeatureConfig darkFeatureConfig) {this.key = darkFeatureConfig.getKey();this.enabled = darkFeatureConfig.isEnable();String darkRule = darkFeatureConfig.getRule().trim();parseDarkRule(darkRule);}@VisibleForTestingprotected void parseDarkRule(String darkRule) {if (!darkRule.startsWith("{") && !darkRule.endsWith("}")) {throw new RuntimeException("failed to parse dark rule: " + darkRule);}String[] rules = darkRule.substring(1, darkRule.length() - 1).split(",");this.rangeSet.clear();this.percentage = 0;for (String rule : rules) {rule = rule.trim();if (StringUtils.isEmpty(rule)) {continue;}if (rule.startsWith("%")) {this.percentage = Integer.parseInt(rule.substring(1));} else if (rule.contains("-")) {String[] parts = rule.split("-");if (parts.length != 2) {throw new RuntimeException("failed to parse dark rule: " + darkRule);}long start = Long.parseLong(parts[0]);long end = Long.parseLong(parts[1]);if (start > end) {throw new RuntimeException("failed to parse dark rule: " + darkRule);}this.rangeSet.add(Range.closed(start, end));} else {long val = Long.parseLong(rule);this.rangeSet.add(Range.closed(val, val));}}}public boolean isEnabled() {return enabled;}public boolean dark(long darkTarget) {boolean selected = this.rangeSet.contains(darkTarget);if (selected) {return true;}long reminder = darkTarget % 100;if (reminder > 0 && reminder < this.percentage) {return true;}return false;}public boolean dark(String darkTarget) {long target = Long.parseLong(darkTarget);return dark(target);}
}

添加、优化灰度组件

在第一步中,我们完成了灰度组件的基本功能。在第二步中,我们再实现基于编程的灰度规则配置方式,用来支持更复杂、更加灵活的灰度规则。

我们需要对第一步实现的代码,进行一些改造。改造之后的目录结构如下所示。其中,DarkFeatureDarkRuleConfig 的代码基本不变, 新增了 IDarkFeature 接口,DarkLaunchDarkRule 的代码有所改动,用来支持编程实现灰度规则。

// 第一步的代码目录结构
com.example.darklaunch--DarkLaunch (框架的最顶层入口类)--DarkFeature (每个feature的灰度规则)--DarkRule (灰度规则)--DarkRuleConfig (用来映射配置到内存中)// 第二步的代码目录结构
com.example.darklaunch--DarkLaunch (框架的最顶层入口类,代码有所改动)--IDarkFeature (抽象接口)--DarkFeature (实现IDarkFeature接口,基于配置文件的灰度规则,代码不变)--DarkRule (灰度规则,代码有改动)--DarkRuleConfig (用来映射配置到内存中,代码不变)

我们先来看下 IDarkFeature 接口,它用来抽象从配置文件中得到的灰度规则,以及编程实现的灰度规则。具体代码如下所示:

public interface IDarkFeature {boolean isEnabled();boolean dark(long darkTarget);boolean dark(String darkTarget);
}

基于这个抽象接口,业务系统可以自己编程实现复杂的灰度规则,然后添加到 DarkRule 中。为了避免配置文件中的灰度规则热更新时,覆盖编程实现的灰度规则,在 DarkRule 中,我们对从配置文件中加载的灰度规则和编程实现的灰度规则分开存储。按照这个设计思路,我们对 DarkRule 类进行重构。重构之后的代码如下所示:

public class DarkRule {// 从配置文件中加载的灰度规则private Map<String, IDarkFeature> darkFeatures = new HashMap<>();// 编程实现的灰度规则private Map<String, IDarkFeature> programmedDarkFeatures = new ConcurrentHashMap<>();public void addProgrammingDarkFeature(String featureKey, IDarkFeature darkFeature) {programmedDarkFeatures.put(featureKey, darkFeature);}public void setDarkRuleFeatures(Map<String, IDarkFeature> newDarkFeatures) {this.darkFeatures = newDarkFeatures;}public IDarkFeature getDarkFeature(String featureKey) {IDarkFeature darkFeature = programmedDarkFeatures.get(featureKey);if (darkFeature != null) {return darkFeature;}return this.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 secondsprivate DarkRule rule;private ScheduledExecutorService executor;public DarkLaunch(int ruleUpdateTimeInterval) {loadRule();this.executor = Executors.newSingleThreadScheduledExecutor();this.executor.scheduleAtFixedRate(new Runnable() {@Overridepublic 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("dark-rule.yaml");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("close file error:", e);}}}if (ruleConfig == null) {throw new RuntimeException("can not load dark rule.");}// 修改:单独更新从配置文件中得到的灰度规则Map<String, IDarkFeature> darkFeatures = new HashMap<>();List<DarkRuleConfig.DarkFeatureConfig> darkFeatureConfigs = ruleConfig.getFeatures();for (DarkRuleConfig.DarkFeatureConfig darkFeatureConfig : darkFeatureConfigs) {darkFeatures.put(darkFeatureConfig.getKey(), new DarkFeature(darkFeatureConfig));}this.rule.setDarkRuleFeatures(darkFeatures);}// 新增,添加编程实现的灰度规则的接口public void addProgrammingDarkFeature(String featureKey, IDarkFeature darkFeature) {this.rule.addProgrammingDarkFeature(featureKey, darkFeature);}public IDarkFeature getDarkFeature(String featureKey) {return this.rule.getDarkFeature(featureKey);}
}

灰度组件的代码实现完了。我们在通过一个 Demo 看下,目前实现的灰度组件该如何使用。结合着 Demo,再去理解上面的代码,会更容易些。

// 灰度规则配置(dark-rule.yaml), 放到classpath路径下
features:
- key: call_newapi_getUserByIdenabled: truerule: {893,342,1020-1120,%30}
- key: call_newapi_registerUserenabled: truerule: {1391198723, %10}
- key: newlog_loanenabled: truerule: {0-1000}// 编程实现的灰度规则
public class UserPromotionDarkRule implements IDarkFeature {@Overridepublic boolean isEnabled() {return true;}@Overridepublic boolean dark(long darkTarget) {// 灰度规则自己想怎么写就怎么写return false;}@Overridepublic 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.addProgrammingDarkFeature("user_promotion", new UserPromotionDarkRule());IDarkFeature darkFeature = darkLaunch.getDarkFeature("user_promotion");System.out.println(darkFeature.isEnabled());System.out.println(darkFeature.dark(833));}
}

总结

到本章为止,项目实战环节就彻底结束了。在这一部分中,我们通过限流、幂等、灰度这三个实战项目,带你从需求分析、系统设计、代码实现三个环节,学习了如何进行功能性、非功能性需求分析,如何通过合理的设计,完成功能性需求,满足非功能性需求,以及如何编写高质量的代码实现。

实际上,项目本身的分析、设计、实现并不重要,不必对细节过于纠结。希望通过这三个例子,分享思考思路、开发套路,让你借鉴并举一反三地应用到你的项目开发中。这才是最有价值的,才是你学习的重点。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/6860.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

网络基础「HTTPS」

✨个人主页&#xff1a; 北 海 &#x1f389;所属专栏&#xff1a; Linux学习之旅 &#x1f383;操作环境&#xff1a; CentOS 7.6 腾讯云远程服务器 文章目录 1.基本概念1.1.HTTP协议面临的问题1.2.加密与解密1.3.数字摘要1.4.数字签名 2.解决方案2.1.「对称式加密」2.2.「非对…

MySql#MySql数据库基础

目录 一、什么是数据库 二、主流数据库 三、基本使用 1.连接服务器 2.使用 1.查看你数据库 2.创建数据库 ​编辑 ​编辑 ​编辑​编辑 3.使用数据库 ​编辑 4.创建数据库表 5.表中插入数据 6.服务器&#xff0c;数据库&#xff0c;表之间的关系 四、MySQL架构…

【算法】深度优先搜索岛屿数量

1、题目描述 有一个由0和1组成的二维矩阵&#xff0c;其中1代表陆地&#xff0c;0代表水&#xff0c;岛屿由水平或垂直方向上相邻的陆地连接形成。 假设矩阵的四周均被水包围&#xff0c;请计算岛屿的数量。 输入&#xff1a;matrix [[1,1,0,0],[0,0,1,0],[0,0,0,0],[0,0,1,1],…

for...in 可以用const声明item

代码&#xff1a; function* foo() {yield 1;yield 2;yield 3;}const genr foo();for (const item of genr) {console.log(item);}for (const i 0; i < 5; i) {console.log("i", i);}在这两段代码中&#xff0c;尽管两者都包含 for 循环&#xff0c;但它们的用途…

如何在 Gin 框架中处理多个 websocket 连接?

在Gin框架中处理多个WebSocket连接&#xff0c;你可以使用gorilla/websocket包。以下是一步步的指南&#xff1a; 首先&#xff0c;在你的终端运行go get github.com/gorilla/websocket来安装gorilla/websocket包。 创建一个Connection结构体来保存WebSocket连接和发送通道。 …

Git在无法访问github的访问方法

Git无法下载github上的源代码 代理的情况 问题&#xff1a;Failed to connect to github.com port 443 after 21100 ms: Couldnt connect to server 提示我们需要为Git单独配置代理。 查看我们的代理端口  为git 设置全局代理 git config --global http.proxy 127.0.0.1:&l…

C++中的回溯搜索法(Backtracking)

回溯搜索法&#xff08;Backtracking&#xff09;是一种通过试错的方法来解决问题的策略。在C中&#xff0c;这种方法通常用于解决诸如组合问题、划分问题、排列问题等&#xff0c;尤其在涉及到约束满足问题&#xff08;CSP&#xff0c;Constraint Satisfaction Problem&#x…

在C++中二维数组初始化的几种不同方法

在 C 中初始化二维数组可以有几种不同的方法&#xff0c;这取决于你想要的数组类型和初始化数据的具体情况。以下是一些常用的初始化方法&#xff1a; 1. 静态初始化 如果你知道数组的大小和初始值&#xff0c;可以直接在声明时初始化。这种方法使用嵌套的大括号 {} 来逐行指…

Apache反代理Tomcat项目,分离应用服务器和WEB服务器

项目的原理是使用单独的机器做应用服务器&#xff0c;再用单独的机器做WEB服务器&#xff0c;从网络需要访问我们的应用的话&#xff0c;就会先经过我们的WEB服务器&#xff0c;再到达应用程序&#xff0c;这样子的好处是我们可以保护应用程序的机器位置&#xff0c;同时还可以…

LNMP一键安装包

LNMP一键安装包是什么? LNMP一键安装包是一个用Linux Shell编写的可以为CentOS/RHEL/Fedora/Debian/Ubuntu/Raspbian/Deepin/Alibaba/Amazon/Mint/Oracle/Rocky/Alma/Kali/UOS/银河麒麟/openEuler/Anolis OS Linux VPS或独立主机安装LNMP(Nginx/MySQL/PHP)、LNMPA(Nginx/MySQ…

代码随想录leetcode200题之链表

目录 1 介绍2 训练3 参考 1 介绍 本博客用来记录代码随想录leetcode200题中链表部分的题目。 2 训练 题目1&#xff1a;203移除链表元素 C代码如下&#xff0c; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* Lis…

【学一点儿前端】Bad value with message: unexpected token `.`. 问题及解决方法

问题 今天从vue3的项目copy一段代码到vue2项目&#xff0c;编译后访问页面报错了 Bad value with message: unexpected token ..注意到错误字符‘.’&#xff0c;这个错误通常发生在处理 JavaScript 或者 HTML 中的动态表达式中&#xff0c;日常使用二分法不断缩小报错代码范…

JavaScript String indexOf() 方法

一、定义和用法&#xff1a; indexOf() 方法返回值在字符串中第一次出现的位置。 如果未找到该值&#xff0c;则 indexOf() 方法返回 -1。 indexOf() 方法区分大小写。 二、语法 string.indexOf(substring, start) 1、参数 substring必需。要搜索的字符串。start可选。开…

2024“天一永安杯“宁波第七届网络安全大赛极安云科战队部分WP

“天一永安杯”2024 宁波第七届网络安全大赛暨第九届大学生网络技术与信息安全大赛 大赛竞赛形式 一、线上初赛 参赛人员&#xff1a;各单位自行选拔3人&#xff08;设队长1名&#xff09;组成团队&#xff0c;不足3人不允许参赛。 竞赛时间&#xff1a;8&#xff1a;30-12&…

LLMs:《Better Faster Large Language Models via Multi-token Prediction》翻译与解读

LLMs&#xff1a;《Better & Faster Large Language Models via Multi-token Prediction》翻译与解读 目录 《Better & Faster Large Language Models via Multi-token Prediction》翻译与解读 Abstract 2、Method方法 Memory-efficient implementation 高效内存实…

IOS APP 常见UI控件

UILabel&#xff08;标签&#xff09;UITextField&#xff08;文本输入框&#xff09;UITextView&#xff08;文本视图&#xff09;UIButton&#xff08;按钮&#xff09;UIImageView&#xff08;图像视图&#xff09;UISwitch&#xff08;开关&#xff09;UIStepper&#xff0…

详解LLMOps,将DevOps用于大语言模型开发

大家好&#xff0c;在机器学习领域&#xff0c;随着技术的不断发展&#xff0c;将大型语言模型&#xff08;LLMs&#xff09;集成到商业产品中已成为一种趋势&#xff0c;同时也带来了许多挑战。为了有效应对这些挑战&#xff0c;数据科学家们转向了一种新型的DevOps实践LLM-OP…

python数据分析——在数据分析中有关概率论的知识

参数和统计量 前言一、总体二、样本三、统计抽样四、随机抽样4.1. 抽签法4.2. 随机数法 五、分层抽样六、整群抽样七、系统抽样八、统计参数九、样本统计量十、样本均值和样本方差十一、描述样本集中位置的统计量11.1. 样本均值11.2. 样本中位数11.3. 样本众数 十二、描述样本分…

windows和linux生成代码patch补丁

windows和linux生成代码patch补丁 如需转载请标明出处&#xff1a;http://blog.csdn.net/itas109 文章目录 windows和linux生成代码patch补丁前言1. .patch补丁2. 差异文件按目录结构导出到文件夹问题1. "Hunk #1 FAILED at 1 (different line endings)" 环境&#…

K8S 哲学 - 服务发现 services

apiVersion: v1 kind: Service metadata:name: deploy-servicelabels:app: deploy-service spec: ports: - port: 80targetPort: 80name: deploy-service-podselector: app: deploy-podtype: NodePort service 的 endPoint &#xff08;ep&#xff09; 主机端口分配方式 两…