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

概述

从本章开始,我们进入项目实现模块。在开源实战中,我带你一块分析了几个比较著名的开源项目,比如 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-1
  limits:
    - api: /v1/user
      limit: 100
    - api: /v1/order
      limit: 50
- appId: app-2
  limits:
    - api: /v1/user
      limit: 50
    - api: /v1/order
      limit: 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 挂了,限流逻辑无法正常运行,这个时候,业务接口也要能正常服务才行。

总结

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

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

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

  • 28
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
MapObject(简称MO)是一种用于开发GIS应用程序的可视化编程组件,可以在VB、VC、Delphi、C#等开发环境中使用。其主要功能包括地图显示、地图浏览、地图查询、符号绘制、地图分析等。 MO的核心是Map对象,它代表了一张地图。Map对象包含了地图数据、地图的显示方式、地图的空间参考等信息。在MO中,地图数据通常以Shapefile格式存储,可以包括点、线、面等空间要素。 MO的编程模型是基于事件的,即程序通过响应组件的事件来完成操作。例如,当用户在地图上点击时,会触发Map控件的MouseDown事件,程序可以在该事件中编写代码来响应用户的操作。 MO的程序设计入门可以从以下几个方面来学习: 1. 创建地图控件:在VB、VC等开发环境中,可以通过向窗体添加Map控件来创建地图控件。在Delphi中,需要在窗体上添加一个TMapControl控件,再通过代码创建Map对象并将其与TMapControl关联起来。 2. 加载地图数据:可以使用Map对象的AddLayer方法,将Shapefile文件加载到地图中。加载后,可以设置要素的显示方式、标注等属性。 3. 地图操作:可以通过Map控件提供的方法,实现地图的缩放、平移、旋转等操作。例如,可以通过调用Map控件的ZoomIn方法实现地图放大,通过调用Map控件的Pan方法实现地图平移。 4. 地图查询:可以使用Map对象的SelectByShape方法,根据指定的查询条件进行地图查询。查询结果可以在地图上高亮显示。 5. 符号绘制:可以使用Map对象中的Symbol对象,绘制点、线、面等符号。例如,可以使用Symbol对象的DrawPoint方法,绘制一个点符号。 以上是MO的一些基础概念和编程入门,希望可以帮助你进行MO的学习和应用开发。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值