设计模式学习笔记 - 项目实战一:设计实现一个支持各种算法的限流框架(分析)

news/2025/3/16 6:16:20/

概述

从本章开始,我们进入项目实现模块。在开源实战中,我带你一块分析了几个比较著名的开源项目,比如 Spring、Mybatis、Google Guava 等,剖析了它们背后蕴含的设计思想、原则和模式。

前面开源实战是学习别人怎么做,那现在就是代码一块进行项目实战。在这个过程中,会带你实践之前学过的设计思想、原则和模式,给你展示怎么应用这些理论知识,让你已开发出优秀的软件。

在项目实战中,一共有三个项目:限流框架、幂等框架、灰度发布组件,带你一起来实现。针对每一个项目,都会从分析、设计、实现这三个部分来进行讲解。

项目本身不是重点,重点还是学习它们背后的开发套路。这才是最有价值的部分。

接下来的三篇文章,先讲第一个实现账目,限流框架。本章,先讲其中的分析环节,介绍项目背景,分析项目需求。


项目背景

先讲下需求诞生的背景。这个背景和下一个实战项目幂等框架也有关系,所以讲的会比较多,希望你能耐心看完,不然会影响后面的学习。

公司成立初期,团队人少。公司集中精力开发一个金融理财产品(把这个项目叫做 X 项目)。整个项目只做了简单的前后端分离,后端的所有代码都在一个 GitHub 仓库中,整个后端作为一个应用来部署,没有划分微服务。

遇到了行业分口,公司发展的不错,公司开始招聘更多人,开发更多的金融产品,比如专注房贷的理财产品、专注供应链的产品、专注消费贷的借款端产品等等。在产品形态上,每个金融产品都做成了独立 App。

对于不同的金融产品,尽管移动端长得不一样,都是后端的很多功能、代码都可以复用。为了快速上线,针对每个应用,公司都成立一个新的团队,然后拷贝 X 项目的代码,在此基础上修改、添加新的功能。

这样成立新团队,拷贝老代码,改改就能上线一个新产品的开发模式,在一开始很受欢迎。产品上线快,也给公司赢得了竞争上的优势。但时间一长,这样的开发模式暴露出来的问题就越来越多了。而且,随着公司的发展,公司也过了急速扩张期,人招的太多,公司开始考虑开发效率的问题。

因为所有项目的代码都是从 X 项目拷贝来的,多个团队同事维护相似的代码,显然是重复劳动,写作起来也非常麻烦。任何团队发现代码的 bug,都需要同步到其他团队做相同的修改。而且,各个团队对代码独立开发,改的面目全非,即便要添加一个通用的功能,每个团队也要基于自己的代码再重复开发。

此外,公司成立初期,各个方面条件有限,只能招到开发水平一般的员工,而且追求快速上线,所以,X 项目的代码质量都很差,结构混乱、命名不规范、到处都是临时解决方案、埋了很多坑,在烂代码之上不停地对其烂代码,时间长了,代码的可读性越来越差、维护成本越来越高,甚至搞过了重新开发的成本。

这个时候该怎么办?

我们可以把公共的功能、代码抽离出来,形成一个独立的项目,部署成一个公共服务平台。所有金融产品的后端还是参照 MVC 三层架构独立开发,不过,它们只实现自己特有的功能,对于一些公共的功能,通过远程调用公共服务平台提供的接口来实现。

这里提到的公共服务平台,有限类似现在比较火的 “中台” 或 “微服务”。不过为了减少部署、维护多个微服务的成本,我们把所有公共的功能,放到一个项目中开发,放到一个应用中部署。只不过,我们要未雨绸缪,实现按照领域模型,将代码的模块化做好,等到真正有哪个模块的接口调用过于集中,性能出现瓶颈时,再把它们拆分出来,设计出独立的微服务来开发和部署。

经过这样的拆分之后,我们可以指派一个团队,集中维护公共服务平台的代码。开发一个新的金融产品,也只需要更少的人员来参与,因为他们只需要开发、维护产品特有的功能代码就可以了。整体上,维护成本降低了。此外,公共服务平台的代码集中到了一个团队手里,重构起来不需要协同其他团队和项目,也便于我们重构、改善代码质量。

需求背景

对于一个公共平台来说,接口请求来自很多不同的系统(后面统称为调用方),比如各种金融产品的后端系统。在系统上线一段时间里,我们遇到了很多问题,比如,因为调用方代码 bug、不正确地使用服务(比如启动 Job 来调用接口获取数据)、业务上面的突发流量(比如促销活动),导致来自某个调用方的接口请求数突增,过度争用服务的线程资源,而来自其他调用方的接口请求,因此来不及响应而排队等待,导致接口请求的响应事件大幅增加,甚至出现超时。

为了解决这个问题,你有什么好的建议呢?

可以开发接口限流功能,限制每个调用方接口请求的频率。当超过预先设定的访问频率后,我们就触发限流熔断,比如,限制调用方 app -1 对公共服务平台总的接口请求频率不超过 1000次 / 秒,超过之后的接口请求都会被拒绝。此外,为了更加精细化地限流,除了限制每个调用方对公共服务平台的接口请求频率 之外,还希望能对单个某个接口的访问频率进行限制,比如限制 app-1 对 /user/query 的访问频率为每秒不超过 100 次。

我们希望开发出来的东西有一定的影响力,即便做不到在行业内有影响力,起码也要做到在公司范围内有影响力。所以,从一开始,我们就不想把这个限流功能,做成只有我们项目可用。我们希望把它开发成一个通用框架,能够应用到各个业务系统重,甚至可以集成到微服务治理平台中。实际上,这也体现了业务开发中要具备的抽象意识、框架意识。要善于识别出通用的功能模块,将它抽象成通用的空间、组件、类库等。

需求分析

刚刚花了很大篇幅啦已介绍项目背景和需求背景,接下来我们再对需求进行更加详细的分析和整理。

前面已经讲过一些需求分析的方法,比如画线框图、写用户用户、测试驱动开发等等。这里,我们借助用户用例和测试驱动开发思想,先去思考,如果框架最终被开发出来之后,它会如何被使用。

我们一般会找一个框架的应用场景,针对这个场景写一个框架使用的 Demo 程序,这样能够直观的看到框架长什么样子。知道了框架应该长什么样,就相当于应试教育中确定了考试题目。针对明确的考题去想解决方案,这是我们最擅长的。

首先我们需要设置限流规则。为了做到在不修改代码的前提下修改规则,我们一般会把规则放到配置文件中(比如 XML、YAML 配置文件)。在继承了限流框架的应用启动时,限流框架会将限流规则,按照实现定义的语法,解析并加载到内存中。我写了一个限流规则的 Demo 配置,如下所示:

configs:
- appId: app-1limits:- api: /v1/userlimit: 100- api: /v1/orderlimit: 50
- appId: app-2limits:- api: /v1/userlimit: 50- api: /v1/orderlimit: 50

接口在收到请求后,应用会将请求发送给限流框架,限流框架会告知应用,这个接口请求是允许继续处理,还是出发限流熔断。如果我们用代码来讲这个过程表示出来的话,就是下面这个 Demo 的样子。如果项目使用的是 Spring 框架,我们可以利用 Spring AOP,把这段限流代码放在统一的切面中,在切面中拦截接口请求,解析出请求对应的调用方 APP ID 和 URL,然后验证是否对此调用方的这个接口请求进行限流。

String appId = "app-1"; // 调用方APP-ID
String url = "http://www.demo.com/v1/user/12345"; // 请求url
RateLimiter rateLimiter = new RateLimiter();
boolean passed = rateLimiter = limit(appId, url);
if (passed) {// 放行接口请求,继续后续的处理
} else {// 接口请求被限流
}

结合刚刚的 Demo,从使用的角度来说,限流框架主要包含两部分功能:配置限流规则和提供编程接口(RateLimiter 类)验证请求时否被限流。不过,作为通用框架,除了功能性需求外,非功能性需求也十分重要,有时候会决定一个框架的成败,比如,框架的易用性、扩展性、灵活性、性能、容错性等。

对于限流框架,我们来看它都有哪些非功能性需求。

易用性方面,我们希望限流规则的配置、编程接口的使用都很简单。我们希望提供各种不同的限流算法,比如基于内存的单机限流算法、基于 Redis 的分布式限流算法,能够让使用者自由选择。此外,因为大部分项目都是基于 Spring 开发的,我们还希望限流框架能非常方便的集成到使用 Spring 框架的项目中。

扩展性、灵活性方面,我们希望能够灵活地扩展各种限流算法。同时,我们还希望支持不同格式(JSON、XML、YAML 等格式)、不同数据源(本地配置文件或 Zookeeper 集中配置等)的限流规则配置方式。

性能方面,因为每个接口请求都要被检查是否限流,这或多或少会增加接口请求的响应时间。而对于响应时间比较敏感的接口服务来说,我们要让限流框架尽可能低延迟,尽可能减少对接口请求本身响应时间的影响。

容错性方面,接入限流框架是为了提供系统的可用性、稳定性,不能因为限流框架异常,反过来影响到服务本身的可用性。所以,限流框架要有高度的容错性。比如,分布式限流算法依赖集中存储器 Redis。如果 Redis 挂了,限流逻辑无法正常运行,这个时候,业务接口也要能正常服务才行。

总结

本章,我们主要对限流框架做了大的项目背景、需求背景介绍,以及更加具体的需求分析,明确了解要做什么,为下面两篇文章(设计和实现)做准备。

本章的讲解中,基本的功能需求其实没有多少,但将非功能性需求考虑进去之后,明显就复杂多了。还是那句老话,写出能用的代码简单,写出好用的代码很难。对于限流框架来说,非功能性需求是设计与实现的难点。怎么做到易用、灵活、可扩展、低延迟、高容错,才是开发的重点。

此外,本章还提到了一些需求分析的方法,比如画线框图、写用户用例、测试驱动开发等等。针对限流框架,我们借助用户用例和测试驱动开发的思想,先去思考,如果框架最终被开发出来之后,它会被如何使用。针对具体的场景去做分析,更加清晰直观。


http://www.ppmy.cn/news/1441759.html

相关文章

RabbitMQ消息是如何分发的,消息是怎么路由的, RabbitMQ中的交换机类型有哪些

目录 面试官:讲一下RabbitMQ消息如何分发和消息怎么路由的?消息分发消息路由RabbitMQ中的交换机类型示例Spring Boot代码示例1. 直接路由(Direct Exchange)2. 扇出路由(Fanout Exchange)3. 主题路由(Topic Exchange)4. 头路由(Headers Exchange)该文章专注于面试,面…

c++:数据结构链表list的模拟实现

文章目录 链表的知识回顾前期工作构造节点迭代器注意构造迭代器解引用*迭代器迭代器->迭代器迭代器- -判断两个迭代器是否相等 链表empty_init构造拷贝构造swapoperatorbegin和endinsertpush_backpush_fronterasepop_backpop_frontsizeemptyclear析构 链表的知识回顾 链表是…

PHP 错误 Unparenthesized `a ? b : c ? d : e` is not supported

最近在一个新的服务器上测试一些老代码的时候得到了类似上面的错误: [Thu Apr 25 07:37:34.139768 2024] [php:error] [pid 691410] [client 192.168.1.229:57183] PHP Fatal error: Unparenthesized a ? b : c ? d : e is not supported. Use either (a ? b : …

Docker从无到有

主要为windows下docker的安装与使用~ 初始Docker Docker理解 对于docker的加简介,我们可以官网获取它的概念,接下来就从什么是docker、为什么要使用docker以及它的作用来进行一个快速入门 前提:项目在发布时,不仅需要其jar包同…

static为什么不能修饰String类

在Java中,static 关键字用于修饰类成员(字段、方法、内部类)以及代码块,它主要表示这些成员或代码块与类本身关联,而不是与类的实例关联。当你提到 static 不能修饰 String 类时,我猜你可能是在思考为什么 …

软考之零碎片段记录(二十七)+复习巩固(十三、十四)

学习 1. 案例题 涉及到更新的。肯能会是数据流的终点E, P, D 数据流转。可能是 P->EP->D(数据更新)P->P(信息处理)D->P(提取数据信息) 2. 案例2 补充关系图时会提示不增加新的实体。则增加关联关系 3. 案例3 用例图 extend用于拓展,当一个用例…

网络攻击日益猖獗,安全防护刻不容缓

“正在排队登录”、“账号登录异常”、“断线重连”......伴随着社交软件用户的一声声抱怨,某知名社交软件的服务器在更新上线2小时后,遭遇DDoS攻击,导致用户无法正常登录。在紧急维护几小时后,这款软件才恢复正常登录的情况。 这…

conda 与 pip 工具笔记

前言 conda与pip是Python开发中常用的两种工具,conda本质是环境、包管理工具,pip是包管理工具,两者的功能有一定的重叠。本文主要记录开发工作中与两者相关的使用说明与注意事项。 推荐用conda创建隔离的虚拟环境,用pip进行包安…