【架构入门系列】从业务到平台的思维转变

语言: CN / TW / HK

一个需求:抽奖系统

如果你接到了一个开发需求:开发一个抽奖活动的后台系统。第一反应如果是打开IDEA,新建一个项目,输入项目名:lottery。然后开始根据需求着手开始进行设计数据库,api,然后进行开发,这是太平常不过的开发流程。

过了一段时间,你又接到一个类似的需求:开发另一个抽奖活动的后台系统,但是里面的一些业务逻辑、流程处理不太一样,然后就把前一个完成的后台系统复制过来,准备开始修改。

是时候停止这种看起来很高效但实际上很愚蠢的开发方式了!本文将告诉你如何将你的思想转变过来,给你一个将业务、将系统、甚至将自己升华的机会。我会用偏后端的思维、尽量易懂的图文、尽量精简的代码、尽量多的例子来将你的等级从 熟练的业务驯服者 提升为 初阶架构猎人

反思:做错了什么

使用复制、粘贴、修改的方法去开发一个类似的新的业务系统,从开发单个系统上来看开发效率上确实挺高的,但仍然存在以下问题:

  • 代码复用性低。类似的业务系统可能越来越多,复制粘贴导致代码的重复率太高,可复用性太低
  • 测试、部署效率太低。需要当做一个新的后台服务新建一套测试、构建、部署、发布流程并进行实施,对一些通用的业务逻辑进行了重复的测试,构建发布流程也需要单独管理
  • 收集数据难度大。类似的业务系统的数据被分散在不同的数据库、日志中,如果想收集汇总分析一些通用的业务日志难度大
  • 业务系统太过私有化,无法提供类似SAAS服务那样让任何人都能创建自己的抽奖活动

简而言之,老鼠的视力很差,它没有办法看到远处的事物,但是我们为了日后业务系统的重复利用,就需要尽量避免鼠目寸光的重复开发。

所以,需要 将业务系统抽象为业务平台 ,支持通用的业务流程,让业务平台为日后的类似的业务系统提供灵活多变的基础业务实现方案,至于特殊的业务流程就单独开发一个特殊的业务系统,两者共同结合提供完整的服务。

另外,平台的使用者应该类似 SAAS ,具备为所有人服务的能力,提供给能让每个用户甚至每个组织都能创建属于自己的独有环境,可以通过在平台上的简单配置构建出属于个体的服务。

如图所示,业务系统与业务平台共同为用户服务。业务平台包含某个业务的通用流程,可能刚好能满足业务的所有需求,那么此时就不需要另外开发一个业务系统。但如果有一些比较特殊的业务流程或者必须单独创建一个业务方便管理控制的流程的话,那么可以将该业务系统与业务平台结合到一起使用,业务系统提供定制接口,业务平台提供通用接口,业务系统也能通过授权的调用接口来调用业务平台获取、修改数据。

通用业务

首先我们需要想清楚,如何从平台的角度考虑,将设计业务逻辑才能比较通用?如何区分是否为通用的业务逻辑,从而进行业务拆分?

通用性

目的:解决业务实现的N种可能。在业务平台上尽量支持多的、通用的业务逻辑。

如何解决:把眼光放长远一些,我们这个业务系统现在需要支持哪些业务逻辑?日后需要支持哪些?然后进行有选择的取舍,不需要考虑太多,否则系统将被设计的特别复杂且用到的部分太少。一个简单有效的方法就是参照以前做过的系统和当前需要实现的系统,将通用的逻辑抽取出来。

举几个例子:

  • 在业务流程上的考量。比如上个活动只开了一天,奖品也都相同,但是这次的活动需要开两场,每场的奖品可能不一样。所以我们能抽象出: 一个抽奖活动可能存在多个活动场次,每个场次对应的奖品、时间可能都不同 ,还需要支持随时进行修改,这就将活动拆分为活动与场次两个层次。所以在数据库设计的时候就需要添加一个字段来对应场次这个维度
  • 在数据库表字段上的考量。 奖品的兑换方式可能是通过快递发送实物奖品、直接展示兑换码 ,所以对于表字段的种类要考虑多一些,比如需要保存用户的收货地址、联系方式。在单个字段的属性选择也要适当考虑可扩展一些,比如字段长度、字段类型

边界处理、业务拆分

这里的边界指的是业务拆分时的边界,比如,哪些属于平台通用逻辑?就能把这些通用逻辑放到平台里。哪些属于业务特有逻辑?这些逻辑就不需要加到平台里。

比如,对于抽奖活动,配置奖品、分发奖品是 抽奖平台 通用逻辑,但用户排行榜则不够通用,除了游戏相关的场景,很少需要用到排行榜的功能,那么排行榜功能就应该属于业务特有逻辑。

在整个抽奖的业务流程中,还有一个业务流程是当抽奖次数用光,就可以通过分享的方式增加抽奖次数。所以抽奖业务通用逻辑依然可以继续拆分,对于分享功能,是不属于抽奖平台通用逻辑的,那么可以将分享功能也单独抽出来成为 分享平台 ,使之也能服务于其他的平台或业务系统。

看似当前已经拆得比较清晰了,但还没有结束,因为这是仅仅针对于【分享成功->增加一次抽奖机会】这样的流程,如果需要分享多次才能增加一次机会呢?如果分享之后每次增加不同的抽奖机会呢?谁负责维护【分享-增加机会】这个奖励机制呢?这就需要一个引擎、流程控制中心来处理,不如就叫 任务调度平台

任务调度平台的核心业务流程是【完成任务-触发奖励】,至于是谁完成的任务,触发谁的奖励对它来说并不重要。只要它能够将任务的触发者与被触发者的对应关系、触发条件、被触发者的奖励接口调用方式维护在内部,当检测到触发者达到了触发条件,能够调用到对应的奖励即可

数据处理

大数据量的存储

既然是平台,会因为使用者使用时长、数据量方面就会比一般的特有业务系统多,在设计时需要考虑到提前为书数据量较大的某些表进行 水平分表 设计,贴一个简单的分表的例子:

在创建数据库时按上图进行创建,在代码中进行增删改查时用下面的方法根据分表字段(shard key)来获取表名:

func GetTableNameByActivityId(activityId int64) string {
  if activityId > 0 {
      return "activity_prize_redeem_" + strconv.FormatInt(activityId % 16, 10)
  }
  return "activity_prize_redeem"
}

分表时,根据哪个字段(shard key)进行分表也是需要考虑的,需要使数据能够均衡的分布在多张表中,并且不影响正常的查询,这样才能通过分表的方式将数据均匀分布到不同的表中,根据shard key进行查询时效率与不分表时一样,但如果使用另外的一个字段查询数据可能需要遍历所有的表才能将数据查询到,所以shard key的选择是与业务查询需求、数据均匀分布相关。

分表之后,最直接的影响是开发的时候需要对SQL语句进行动态调整,某些ORM框架不支持导致的开发效率降低,但是改动的代码不多,一劳永逸。

高性能查询

对于平台来说,并发访问量也可能较大,那么缓存、队列、ES必不可少。

对于缓存,现在基本上说的都是使用Redis,可以使用的集群模式有主从、哨兵、集群。使用的策略也有延迟加载、直写,还需要考虑到一些击穿、失效的缓存问题,具体可以看 这里

对于消息队列,在业务解耦、流量削峰方面是一个非常重要的中间件,具体可以看 这里

对于Elastic Search,如果业务上有全文检索的需求就是要结合EFK一起上的一个模块,具体使用方式、场景请自行查阅。

运营埋点、报表

数据太重要了!分析用户行为,预测市场走向,还是以后设计系统的参考指标,我们作为技术开发人员,也需要为运营人员着想,需要统计出运营人员感兴趣的一些数据,最好是直接找他们提前询问清楚,哪些数据他们需要,以便我们在设计数据库的时候将一些关键数据用一些字段保存下来。

比如在抽奖活动中,运营人员需要知道web平台、安卓平台、IOS平台分别参与抽奖的人分别有多少,但是设计出来的数据库字段、代码中打印出的日志可能都不存在或者难以通过现有设计进行统计,那么就需要对这个统计需求进行单独的设计。

日志、操作记录表

在代码中输出日志是必须的,比如通常需要在访问API、某个API里的关键逻辑、结合关键数据打印成功或失败信息。

另外,对于某些业务场景需要严格统计数据操作之前的情况、操作之后的情况、操作类型、操作人员、操作时间。虽然也可以通过输出日志的方式,但不够规范,查询统计难度大,容易丢失。所以需要单独设计出一张操作流水表,将重要信息持久化起来。

在每个需要统计的重要操作执行之后,使用消息队列或者另外的线程去插入一条操作记录到这张表中,如下

go service.Record(⊧.SysOperateRecord{
  BizType:     dao.BizType_Customer,
  OperType:    dao.OperType_INSERT,
  OperContent: fmt.Sprintf("batch insert one new customer: %s", customer),
  Operator:    userinfo.Id,
  CreateTime:  currentTime,
  UpdateTime:  currentTime,
})

平台管理

类SAAS独立环境

如何让用户有自己的独立环境?让他们可以在平台上简单配置之后创建自己的应用?

其实非常简单,在只考虑用户而不是租户的命名空间下,在系统层面和数据库设计层面多进行一层考虑即可,比如在数据库的主表/主实体上增加两个字段: namespace_idapp_id

  • namespace_id代表一个用户拥有的唯一且与其他用户相互隔离的命名空间。一个用户创建的所有业务系统、数据都在一个namespace_id下面,所以该用户创建的所有业务系统的数据都能用namespace_id查询得到。
  • app_id代表具体的某个业务系统(应用)的唯一id。所以该用户创建的某个特定的业务系统的数据能用app_id查询到。

多平台统一管理

在多个平台(抽奖、分享、任务平台)建立起之后,需要一个后台管理平台将这几个平台统一管理起来,方便为用户提供统一的配置。

以创建一个业务系统为例,如果使用管理平台进行配置,那么用户只需要在管理平台上填写不同平台的对应业务配置即可,在管理平台中将自动处理:

  1. 管理平台的创建应用功能将先创建一个统一的namespace_id、app_id
  2. 将namespace_id、app_id、不同平台的对应配置通过内部调用写入到对应平台的数据库配置表中

对配置好平台配置进行修改、删除,对业务数据进行统计的操作可通过管理平台的调整配置、统计数据接口进行

技术选型

微服务

为什么说适合用微服务架构来开发平台?

  • 平台业务的扩张。往平台的方向考虑,业务逻辑会越来越复杂,代码量会越来越庞大,为了服务的独立部署测试、调整,服务的拆分成为必然。
  • 平台的统一管理。通过API网关、配置中心、服务发现注册中心、熔断器等组件,可以将多个服务统一管理起来。比如在网关添加鉴权模块、收集所有请求日志,在配置中心统一管理服务的配置文件,在熔断器中对不同服务进行管理配置。以上都是为了 将零散的服务通过相关组件统一成整体

业务决定架构

业务如何决定架构?

  • 业务的并发量、稳定性。如果并发量是业务需要满足的一个条件,则需要能够支撑大量请求的高可用架构。比如每个服务节点的多节点负载均衡、数据库使用MySQL还是MongoDB的选择、数据库架构的主从复制读写分离、中间件的选择、前端的缓存与CDN技术
  • 业务的第三方支持。如推送、搜索功能自己实现的话比较麻烦,一般使用第三方服务,这种第三方与自己服务的结合也是架构中的体现
  • 业务需求引发的内部交互。如一般应用的注册功能,都会使用 消息队列 这个中间件将写数据库行为与其他行为(发短信、发邮件)解耦;还可能设计一些 定时任务 从某些数据源定期获取数据更新到另一个数据源

这里给出另一个比较能体现业务决定架构思想的一篇实例文章参考

性能

如果只考虑性能,那么不能选择微服务架构,因为单体应用在性能上是完全碾压微服务架构的。如果为了全局考虑之后还是得选择微服务,那么如何在使用微服务的情况下尽量提升访问的性能呢?

首先需要通过压测的方式测试出性能的瓶颈在哪里,这篇文章可能有所帮助。

至少需要保证能在性能问题发生之后能够立即解决,所以至少要做到服务能够在任意时刻能够版本回滚、能够动态扩展到更多机器上。然后通过对应的方式如优化代码、使用缓存、队列、添加服务器、改进中间件架构的方式去解决性能问题。

部署方式

对一个架构师来讲,代码从提交到上线也需要考虑到,甚至需要考虑为团队构建DevOps体系。简单来说,使用自动化流水线将代码提交、测试、构建、部署自动化起来,并在项目上线的过程中协助开发测试运维进行代码版本的切换、部署方式的实践。

为了日后服务的蓝绿部署、回滚、动态伸缩,必须使用容器技术如Docker与容器编排平台如K8S来实现。至于具体的搭建方式、维护细节不用完全掌握,但一定要知道各种部署方式、服务维护方式和其利弊。

安全考虑

业务安全

对于抽奖系统,因为奖品有实际价值,所以直接跟钱挂钩。为了防止恶意用户撸羊毛,必须对可能存在的抽奖后门进行封杀。

对于抽奖系统,最大的后门是:中奖接口。如何判断用户是真正的玩了这个游戏并一路闯关达到终点夺得奖励的呢?对于web平台来说真的是一个比较困难的问题,因为没办法跟游戏一样实时获得用户的数据进行详细的判定,所以只能在最大程度上增加中奖接口的校验规则。如下:

  • 游戏逻辑校验。根据前端设计,先测试出夺得奖品最快用时是多少,然后在后台加入判断:如果用时少于最快用时,封杀。另外可以根据游戏业务逻辑判断:必须通过点击【同意协议】、【开始游戏】这两个按钮(访问这两个接口)才能成功调用中奖接口。
  • 验证码。防止恶意用户使用脚本自动刷奖,中奖接口添加验证码方式校验。但这极大程度上影响用的体验,谨慎使用。
  • 黑名单控制。对于反复调用很多次的用户,直接将该用户id加入到黑名单中,在一段时间内禁止接口访问。

网站安全

这是一些比较通用的web安全问题,一般在框架内部已经解决,但也需要确认是否开启相关安全机制。

  • SQL注入。如果使用的是ORM框架则不需要担心这个问题,因为框架内部已经使用转义的方式帮你处理好会影响SQL语句的特殊字符。如果涉及到复杂的SQL手动拼接,那么必须在开发时使用占位符的方式来拼接字符串来解决该问题
  • csrf。像Go语言的Beego框架,Ruby语言的Ruby On Rails框架,Java语言的Spring Security中就已经提供解决方案,但并不是默认开启,需要在前后端统一配置才能生效,后端配置之后,会在session存储一个csrf的token,等待下次请求前端传过来这个token来验证。所以对于某些安全性较高的操作是需要添加csrf防范的
  • xss。在框架里可能没有现成方案,那么需要在前后端都加以防范,比如在前端JS中要避免把不可信的数据当作代码执行了,在后端需要配置专门的过滤器,在过滤器中使用转义工具将影响JS执行的特殊字符进行转义
  • https。防止中间人攻击,提升网站安全性、搜索权重,众人皆知,不再赘述
  • 权限控制。避免普通用户能够使用管理员的权限类似的问题,需要在后端进行限制
  • 密码管理。即使数据库被人破解,也不能让别人看到你的明文密码。对于不需要解密的密码,比如用户登录的密码,使用bcrypt算法或md5加盐进行加密;对于需要解密的密码,使用AES或对称加密算法加密保存,加解密使用的密钥需要另外保存

等等等等,网站安全问题是在太多,没办法都写出来,不过以上问题比较常见,需要在开发时外加注意,这些虽然不会在架构设计图上展示出来,但是会是一个存在的架构隐患

总结

我们从业务、数据、技术、安全方面综合考虑如何将简单的业务设计成为一个平台,看重的是长远的收益,提升的是自己的能力。作为开发人员的你也许已经烦透写业务代码,不妨换一种开发思想挑战一下自己吧。