ramostear.comramostear.com 谭朝红的技术分享博客

格言 编程是一门技术,也是一门艺术 !

实现SaaS(软件及服务)架构所面临的三大技术挑战

实现SaaS(软件及服务)架构所面临的三大技术挑战

​ 在当下的世界中,软件应用无处不在,随处都可见电子产品的身影,一个没有任何电子产品的生活是什么样子让人很难想象。而支撑起这些电子产品的是无数个应用程序和其背后所隐藏的实现技术。

​ 无论你需要做什么,都可以找到大量的应用程序供你选择。软件的本质工作是简化复杂的业务流程,让生活更简单,更有趣。然而,在享受软件带来便捷的同时,也伴随着一定的副作用,比如你需要安装大量的应用软件,且需要为这些应用软件支付一大笔费用,同时还需要关心软件升级维护的问题。为了最大程度的降低使用软件所带来的副作用,SaaS(软件及服务)应势而生。

​ 在开始探讨本文的主题之前,先花一分钟了解什么是SaaS应用程序以及使用SaaS软件的优势。

1. 什么是SaaS应用软件?

​ SaaS是Software as a Service(软件即服务)的缩写,它是云计算的主要体现形式之一,其他的还有诸如平台即服务(PaaS),基础设施即服务(IaaS)以及数据即服务(DaaS)等。简而言之,SaaS是一种软件交付模式,旨在以“即用即付”服务的方式为客户提供软件服务,客户不需要安装软件便可获得应用程序提供的所有功能。

​ SaaS是一种基于云计算的软件交付系统,客户只需要通过浏览器便可轻松订阅应用程序所提供的功能。这种“按需使用”或“软件+服务”的软件交付系统,被广泛应用于各大中小企业,组织中,以帮助他们降低软件的使用成本。当下,SaaS软件正被用于各种行业的业务领域中,例如:客户关系管理软件(CRM),企业资源规划软件(ERP),信息管理软件(MIS),人力资源管理软件(HRM),地理信息管理软件(GIS)以及学习管理软件(SIS)等等。

2. 使用SaaS应用软件的优势

​ SaaS软件交付模式的概念最早于20世纪60年代被提出,到20世纪90年代,随着互联网技术的不断发展,SaaS的发展速度也随之加快,许多软件提供商开始通过互联网托管和提供软件服务,而SaaS(软件即服务)这个术语也是在这一时期确定下来的。使用SaaS软件在以下几个方面可以获得较大优势:

  • 1 在软件使用早期,可以大幅度降低硬件,带宽,安装和运营成本。
  • 2 可以以更小的人员和资金的开销,准确的预测业务数据,更快的完成软件的升级和维护工作。
  • 3 与传统软件相比,用户通过互联网获的软件服务,从而降低了管理软件所带来的安全风险。
  • 4 快速部署,快速接入。由于不需要自行安装和部署软件,只需要向服务提供商注册信息,并为订阅的服务付费,便可获得软件服务。从而减少了获得软件的等待时间。
  • 5 跨平台操作。借助互联网,客户可以在任何时间,任何地点,通过浏览器便可接入服务。

​ 任何有价值的东西,在其出现之前都会历经各种困难与挑战,没有什么事物是一蹴而就的。接下来,将介绍构建一个SaaS软件在系统测试,数据安全和升级维护三个方面面临的巨大挑战。

3.实现SaaS软件挑战一#测试

​ 任何有价值的软件在交付之前,都需要进行大量的测试。这和我们买衣服一样,在确定付款前,都需要在试衣间试穿各种样式的衣服。SaaS软件的测试与传统的软件测试基本相同,通过执行各种逻辑验证,确保作为服务提供的软件在质量上是否符合客户的需求。SaaS软件的测试通常会包含数据安全测试,业务逻辑测试,数据集成测试,接口兼容测试,可伸缩测试和高并发测试。但SaaS软件的测试也有有别于传统软件测试的地方。

​ 与传统软件开发相比,SaaS软件的测试在测试周期和实施等方面都面临着诸多的挑战。尽管实现SaaS软件的技术框架会有所不同,但在测试环节都将面临一些常见的技术挑战。

3.1. 安全性测试

​ SaaS软件可以为使用者带来巨大的好处,但仍然有很多用户在质疑SaaS应用软件的数据安全。基于云计算应用的数据安全一直以来都是一个居高不下的热点话题。因此,针对SaaS软件的安全性测试必须慎重对待,需要有专门的测试策略和工具。

​ 与其他任何的云计算平台一样,在云计算环境下维护数据的安全性和完整性具有很大的风险与挑战,和传统的软件相比,SaaS应用软件的安全性测试更为复杂。在SaaS应用软件测试中,需要模拟多个租户下,不同安全级别的隐私要求,权限分配粒度,资源隔离等级和用户行为模式。传统的测试手段很难测试并发现SaaS软件中存在的安全漏洞,软件测试场景也很难完全发现漏洞并消除这些安全威胁。

3.2. 可伸缩和高可用测试

​ 可伸缩(也称可扩展)性是SaaS软件服务提供商重要的商业模型指标之一,要求SaaS应用软件可以根据客户量的大小进行水平方向的伸缩。简单来说,SaaS平台可以根据当前用户量的多少,动态地增加或者减少运行实例的数量;而高可用(性能)是客户衡量SaaS软件好坏的一个重要指标。

​ 如何成功的测试SaaS软件的可伸缩和高可用性,需要有专门的测试策略,才能组织出可用的测试场景,并且需要比传统软件测试更多的测试样本数据和测试标准。这些数据和标准需要仔细的考虑SaaS软件的应用场景,才可能被量化和设计。另外,还需要考虑如何在不同租户类型,不同的用户数量组合,不同使用环境(移动端,PC端)的复杂条件下,对系统的性能,峰值和负载能力进行测试。

3.3. 集成和开放API接口测试

​ SaaS应用软件在一定程度上需要集成第三方的业务系统,同时还有可能需要开放一定的API接口,以支持从其他平台集成或迁移数据。在何种情况下,保护数据的安全性和完整性将给测试带来巨大的压力。在SaaS应用软件的集成测试和API测试中,需要对入站和出站数据进行验证,以及对所有API的功能,安全性,性能以及文档的完整性进行测试。即便是这样,你也很难提前组织起所有的测试场景,而且这个过程非常的耗时。也许第一版的API还没有测试完,新版本的API已经添加到测试列表。实现SaaS软件是一个不断迭代的过程,因此很难在短时间内一次测试就涵盖所有的API。

4. 实现SaaS软件挑战二#数据安全

​ 对于SaaS软件而言,更为复杂和艰难的工作是如何保障SaaS平台中用户数据的安全。不管是大型企业还是小公司,他们对于数据的安全性要求都是一样严苛的。例如常见的CRM系统,HR系统,ERP系统和财务管理系统等,它们都存储了大量高度敏感的用户信息,如果SaaS软件的数据安全无法得到保障,不仅是客户的数据会遭受破坏,服务提供商的信誉也会收到严重的影响,甚至会伴随着相关法律的处理。因此,相比于传统的软件,SaaS软件的安全性要求更高,系统结构更为复杂,实现难度也更大。

​ 构建一个SaaS平台,在满足高性能和可伸缩的条件下,还需要着力保障用户数据的高度安全,这主要体现在以下三个方面:

  • 1 多租户数据隔离:多租户架构是SaaS软件的一个重要评定标准,如何对各个租户的数据进行识别,分割和存储需要在效费比,安全性和性能上取得一个平衡。
  • 2 数据备份与恢复:由于各租户订阅服务的组合不同,使用软件的时间段也不尽相同,对不同租户的数据进行备份和恢复的难度也相当大。另外,在操作其中一个租户数据时,需要保障其他所有租户的数据不会受到影响。在发生系统故障时,还需要及时恢复租户数据,面对庞大的数据量,数据恢复的复杂度和难度都比单体应用高很多。
  • 3 数据入站和出站校验:SaaS软件允许用户在任何地点通过浏览器获取服务,这就要求SaaS软件在数据传输,用户输入,系统输出等环节有着更高的安全性要求。相比于单体架构的应用程序,需要更高级别的安全传输加密/解密手段,更细粒度的用户认证和鉴权措施。另外,对用户的日志的收集,追踪和审计工作比单体软件更难。

​ 保护存储在SaaS平台中的数据安全,需要仔细的分析平台中每一个业务流程,细化权限下放的粒度,严格把控访问接入设置以及数据的存储规则。即便如此,由于SaaS系统通常需要面对比单体架构软件更为庞大的数据量,且业务流程更为复杂,更新周期相对较短,因此如何确保存储在SaaS平台中的数据不易被破坏或者泄露是一个永久存在的工作。

5.实现SaaS软件挑战三#升级维护

​ SaaS应用软件免去了客户使用软件时的安装,维护,升级等工作,但这些繁杂的工作并未因为SaaS而消亡,而是转移到了SaaS软件服务商的手中,且工作变得更为复杂和艰难。

​ SaaS软件需要全天候为客户提供可用的服务,因为你完全不知道客户会在什么时候登入系统开展工作。这就要求SaaS服务提供商在升级和维护软件的工程中不能影响当前用户使用软件。简单来说,SaaS软件的升级维护不能采取“冷启动”的方式来完成,需要采用“热部署”的方式,让客户基本上感觉不到升级工作正在进行。反观传统软件上线或更新过程,往往会出现各种问题:不一致的运行环境,过多的人为干预系统的构建和部署,代码改动引起不可控的质量,向下不兼容,服务中断,更长的更新时间,数据丢失等问题。

​ 对于SaaS系统,系统的升级维护工作不能暂停当前客户正在执行的业务,避免业务数据丢失,因此需要一种全新的软件发布机制,通过可视化,自动化的操作,实现持续,无缝,零重启的软件交付过程。在升级维护时,SaaS软件主要面临以下几个挑战:

  • 1 版本可回退:如果新上线的功能模块遇到重大问题,可以回退到之前的版本而不影响用户的正常业务。
  • 2 系统向下兼容:新版本的系统需要尽可能的向下兼容旧系统的数据。在最坏的情况下,当升级过程发生时,用户正在使用旧版本提交数据,如果适配旧版本提交的数据,需要慎重考虑。
  • 3 灰度发布:灰度发布包含两个方面:前后端灰度发布和移动端的灰度发布。
  • 4 零重启:零重启要求在不终止服务的情况下完成系统的升级工作,这就要求SaaS平台具备热部署的特性,确保SaaS平台能够保持7x24小时的持续服务能力。

​ SaaS软件易于使用是相对于软件使用者而言的,对于SaaS软件的提供者来说,软件的复杂性,安全性和可用性都面临者全新的技术难题,克服这些问题并不容易。作为SaaS软件的实现者,不能看着油漆是干燥的,就想当然的坐上去,在油漆真正干燥前,一切都是脆弱的,需要耗费大量的时间去处理诸多问题,才能让用户放心的坐上去。

​ 市场对于SaaS软件的需求逐年递增,各种类型的SaaS软件产品也层出不穷,但真正成功的SaaS软件却凤毛麟角。究其原因,主要还是SaaS化的软件并不是简单的将传统软件改造成多租户架构那么简单,用户对于易用性,集成性,安全性,灵活性和定制性的要求越来越高,作为SaaS软件的实现者和提供者,需要付出比对待传统软件更多的精力,更严苛的要求,更谨慎的考虑,才能正确认识到实现SaaS软件将要面临的技术难题与挑战。

​ 基于云计算的SaaS软件交互模式,其多租户,多平台环境,高并发等特点给SaaS软件的实现带来了诸多的技术难点,作为SaaS软件的缔造者和提供者,需要全面和谨慎的考虑和处理这些技术问题,方可缩短软件承诺与用户期望之间的差距。提前识别并跟踪不断变化的需求与技术走向,谨慎的考虑架构与实现之间存在的问题,才能在打磨出一款有商业价值的SaaS产品。

首席技术官(CTO)在IT创业公司的角色转变

首席技术官(CTO)在IT创业公司的角色转变

在大多数的IT创业公司中,大多是技术驱动型的公司。面对不断涌现的行业新技术,企业需要快速的掌握并采用这些创新型技术,以确保企业在激烈的竞争中获得相对的竞争优势。以技术为驱动力的公司,为了能快速的处理公司所有的技术问题,从而催生了一个新的职位——首席技术官,英文简称CTO。如果你的公司是一家以技术为驱动力的创业公司,或者你是一位准备转型做CTO的开发人员,那你应该先充分了解首席技术官在IT创业公司中所扮演的角色以及该角色需要承担的责任。

​ 本文以首席技术官的职责和技能为主题,分析首席技术官在IT创业公司的各个发展阶段所扮演的角色,帮助你快速地对首席技术官(CTO)这个职位有一个全面的了解。

一、IT创业公司的CTO在扮演什么角色?

​ 首席技术官,从字面意思上看,主要负责与公司内部技术相关的工作,但这个定义太过于模糊和笼统。在国内外的IT创业公司中,对于CTO该干什么,不该干什么没有明确的定义和统一的标准,不同的IT创业公司因其业务的不同,对CTO的职能要求也不尽相同。虽然不同IT创业公司对CTO有着不同的职能划分,但是从CTO的工作本质上看,可以将CTO划分成技术领导型和技术管理型两类。

1-1、技术领导型

​ 技术领导型的首席技术官都拥有这超高的编码水平,在软件编码、架构设计、基础设施配置等领域都拥有丰富的专业知识和实践经验。他们自身具备的丰富的专业知识和开发经验,帮助其有能力对技术团队中其他的开发人员的代码进行评审,同时能为公司的工程项目作出风险评估。

1-2、技术管理型

​ 这一类型的首席技术官主要的工作任务是监督和管理工程项目的各个项目经理。技术管理型的首席技术官也具备较高的编码水平,但他们通常不参与具体的编码工作,而是将主要的精力放在公司技术战略走向并管理技术团队内部的工程。

​ 随着IT创业公司的不断发展,首席技术官的工作重心将从技术领导过渡到技术管理。在下面的内容中,将逐步分析IT创业公司在各个阶段中,首席技术官角色的演变过程。

二、IT创业公司的CTO角色会发生什么变化?

​ 所有IT企业的首席技术官都有一个贯穿公司始末的伟大使命:确保公司的技术战略完全服务于公司的业务战略。下面是一个IT创业公司从零(0)到一(1)的过程中CTO角色发生的转变。

2-1、创业早期

​ 在创业早期,所有的工作都是围绕着一个商业创意开始的。在这个阶段,公司可能还没有成立,所有的创始人在一起思想碰撞,一场又一场的头脑风暴,目的是完善创业,起草商业计划书。此时的CTO(通常是创始人)往往是一个拥有强大技术专长的人,他们的主要工作职责是验证商业方案的技术可行性,并提出实施软件产品开发的技术解决方案。

2-2、创业初期

​ 如果创业团队的商业方案得到了投资人和消费者的关注,创业团队会注册公司并开始着手构建第一个版本的软件产品。此时的CTO是技术团队的第一个(大多数情况下也是唯一一个)开发人员,他们需要在有限的时间内开发出一个最小化的可使用的产品——MVP(MVP是Minimum Viable Product的缩写)。

​ 在这个阶段,CTO的主要工作如下:

  1. 选择并确定公司的技术栈
  2. 设计软件架构
  3. 配置可扩展的基础架构(硬件和网络)
  4. 软件源代码编写
  5. 测试应用软件

​ 这些初期的工作需要CTO具备强大编码能力(有时是全栈开发),经验和相关的开发流程知识。可能一些创业初始的公司会聘用技术人员,CTO不需要单挑所有的工作,但他们依旧需要在早期为产品原型编写大量的源代码。

2-3、创业成长期

​ 公司进入快速成长期时,软件产品开始投放市场,CTO的工作重点转向软件的升级工作。软件升级让初始版本的的产品更具功能性和可用性,以便吸引更多的用户使用软件。为了加快软件开发速度,企业通常在这个阶段加快扩充技术团队人数的速度,CTO的角色也相应发生了转变,技术团队的管理事务开始进入CTO的工作日程表:

  1. 优化应用程序的部署流程,以提高开发人员的工作效率
  2. 确保应用程序能安全、可靠的运行
  3. 提出更经济、可扩展的基础设施配置方案

​ 在这个阶段,CTO除了参与编码工作外,还要负责管理不断扩大的技术队伍,其管理工作主要体现在以下两个方向:

  1. 负责监督招聘新职员的工作,需要对招聘技术人员的工作做最后的把关
  2. 建立公司的技术文化,如开发工具集、SDK版本、编码风格、书写规范、命名规范、注释规范、文档规范等等,良好的技术文化可以确保所有的开发人员能够遵循同一个标准和最佳实践开展编码工作,从而提高生产效率。

2-4、创业成熟期

​ 公司进入创业成熟期,技术团队的人数将由原来的几人增加到几十人甚至几千人,在这个时期,CTO必须要带领大批的开发人员按照既定的开发规范和工作流程开展编码工作。此时CTO的工作重点将完全转向管理,CTO可能会同时协调并监督多个开发团队,完成不同的软件工程任务。

​ 在此阶段,CTO需要花时间和精力去构建软件产品的愿景,跟踪并分析行业中最新的技术动向,并给出在下一个软件版本中实施相关技术的可行性方案。为了做出合理的、正确的技术选择,CTO在这个时期需要进一步了解新技术的详细流程和行业竞争者的技术状态,需要采用相关的创新技术,确保相比于竞争对手有一定的技术竞争优势。

​ 在本小节中,分析了在IT创业公司中,随着公司的发展,CTO从技术领导型向技术管理型转变的原因。在下面的章节中,将介绍IT创业公司的CTO都需要具备什么样的技能。

三、IT创业公司的CTO需要哪些必杀技?

​ 前面我们分析了IT创业公司在不同阶段中CTO工作职责的转变,CTO需要在软件开发和团队管理上都有着优于常人的能力。接下来,将介绍IT创业公司在各个发展阶段CTO需要具备的一些技能。

3-1、软件架构师

​ 在创业初期,CTO是公司唯一的技术权威和技术实现者,这就要求CTO需要拥有以下的技战水平:

  1. 设计并实现软件架构
  2. 选择并确定技术栈
  3. 设计并配置基础架构(硬件和网络)
  4. 选择并调试开发工具
  5. 设计并实现数据建模
  6. 优化并升级应用程序
  7. 确保应用程序的安全性和可用性
  8. 确保基础架构的经济适用性
  9. 跟踪并分析行业新技术,并对是否采用新技术作出决策

3-2、系统管理员

​ 在创业初期,CTO大多数都兼任系统管理员一职,负责对基础架构和应用程序进行管理,需要具备以下的工作能力:

  1. 软硬件的安全防护管理
  2. 域名、SSL证书和第三方服务管理
  3. 内部开发沟通渠道管理
  4. 基础架构性能监控管理
  5. 数据备份和日志审查管理

随着公司的壮大,这一部分工作会交由首席信息官(CIO)来管理,但在早期,大多数工作还是由CTO来完成。

3-3、项目组长

​ 当技术团队开发人员数量增加时,CTO开始转向团队管理,编码工作的份额开始减少,此时的CTO开始承担一个项目组长的所有职责:

  1. 根据以往的最佳实践和新的编码方法定义和实施开发标准
  2. 制定开发流程
  3. 审查软件源代码
  4. 分配开发任务
  5. 监督招聘工作
  6. 培训新职工
  7. 与客户展开沟通
  8. 与决策层沟通
  9. 评估开发人员的KPI(Key Performance Indicatro,即关键绩效指标)
  10. 监督开发人员的工作进度
  11. 评估技术风险

​ 成为一个优秀的技术管理者不仅需要强大的专业技术知识,还需要有良好的沟通能力,需要随时与开发人员沟通,并解决开发中遇到的问题。

3-4、运维工程师

​ 除了推动公司的开发工作向前发展,管理好技术团队之外,CTO在公司的发展期,还需要成为一名不错的运维工程师,在公司进入创业发展期,CTO必须要负责建立安全、可靠、易用的软件部署通道,为开发人员提供良好的软件升级环境,此时的CTO需要建立起高效的运维流程:

  1. 构建易用、安全、稳定的软件部署通道
  2. 规划软件产品的版本
  3. 选择版本管理工具并管理软件产品版本
  4. 制定并实施软件产品版本的发布周期

3-5、企业高管

​ 在本文一开始提到,IT创业公司的CTO通常是公司的创始人之一,是公司软件工程的最高执行官。除了编码、运维等技术类工作,他们还需要参与公司的上层管理:

  1. 参与公司战略规范和重大决策工作
  2. 参与公司的高层管理工作
  3. 跟踪和分析竞争对手,提出技术创新建议
  4. 为公司的业务提供可行性的技术方案
  5. 监督并协调外部劳动力(外包团队)
  6. 跟踪行业内的技术趋势并识别出公司的技术竞争优势
  7. 评估并监督公司在技术领域的资金预算
  8. 优化基础设施配置方案,降低产品运营成本

​ 技术类的企业高管,不仅要具备强大的专业知识,还需要对企业的管理流程有深入的了解。单纯的技术达人,不是一个合格的IT企业CTO。

四、总结

​ 随着IT创业公司的发展,首席技术官所扮演的角色也在发生改变,总的来说,CTO需要处理公司内部所有和技术有关的问题,可能是微观上的,也可能是宏观上的,但其核心使命不变:确保公司的技术战略完全贴合公司的业务战略。一个成功的IT创业公司(什么样子才算成功?所有的IT创业公司都想要成为行业的领头羊或者独角兽,也许这样才算成功!),一定有一个优秀的首席技术官伴随着企业的成长,他们了解企业在不同的发展背景下应该做什么事、需要具备什么样的技术能力和需要思考什么样的问题。

​ 最后,如果需要用一句话来概括一名出色的CTO所具备的能力和特点,那可能是这样的:出色的CTO都是嚼着嘴里的,夹着碗里的,看着锅里的,想着地里长着的

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

总结:实施SaaS模型的11个关键步骤

前言:在SaaS模型中,应用程序是通过网络向用户提供服务。用户只需要通过网络访问应用程序便可完成自身的业务活动,而不需要安装和维护软件。任何的SaaS模型的实施,都应具备以下三个特征:

  1. 多租户架构:SaaS模型设定了多个用户共享一套应用程序源码,此源码被服务提供商统一托管
  2. 自定义可配置:不同用户共享一套应用程序源码,这就要求SaaS模型须具备一定的自定义和可配置功能,以便可以管理和维护用户的自定义业务
  3. 可访问:SaaS模型依赖于网络为用户提供服务,所以必须确保在任何的时间点都能为用户提供数据访问服务。

为了能够打造一款成功的SaaS软件,在实施SaaS模型的过程中,应该遵循以下的步骤:

1. 提前了解业务

​ 在考虑技术和架构的问题之前,清楚的了解业务非常重要。没有了解业务,任何应用程序的设计和开发工作都无从谈起。为了得到更好的SaaS模型实施效果,必须在很早的阶段就开展需求调研工作,明确软件的业务和设计目标。调研的范围应该是充足的、广泛的,以便更好的识别出SaaS模型的业务边界和核心内容。在调研时,应该包括以下的内容:

  1. 采用什么样的架构进行设计?
  2. 用户类型和用户规模分别是什么?
  3. 如何处理应用程序的可扩展性、安全性和稳定性这三个问题?

​ 业务先于设计和开发,在非常早的阶段便开展调研和需求分析工作非常重要,它有助于评估项目的成本、周期和风险。除此之外,还应该重视现有系统在改造工程中所面临的挑战。

2.选择能胜任的团队

​ 物尽其用,人尽其才,无论技术和架构如何,选择适合SaaS模型的技术团队尤为重要。在实施SaaS模型之前,决策者应该选择一支由经验丰富的开发人员组成的团队,他们必须对SaaS模型的概念有深入的了解,且该团队应该拥有多种专业技术的人员,同时对SaaS行业中应该遵循的最佳实践有充分的了解。

3.设计可扩展的基础架构

一旦技术团队了解完业务需求,下一步的重点就是构建可扩展的基础架构组件:

  1. 数据中心
  2. 网络设施:网络安全(如HTTPS加密、认证和鉴权)
  3. 硬件设施:操作系统和存储
  4. 数据备份和系统监控工具

​ 除了上述之外,内部的评审工作也应该同步进行,以便在构建基础架构是评估应用系统的总体成本,在最终决定实施SaaS模型前,还需考虑一下因素(成本驱动):

  1. SaaS软件的成熟度模型
  2. 可伸缩性和可用性
  3. 客户端兼容性
  4. 数据备份和恢复
  5. 网络带宽和存储空间
  6. 安全管理(加密级别和安全证书)

4.网络带宽和应用服务器

​ 这点非常重要,为了达到良好的用户体验效果,用户规模和类型决定了带宽的大小和应用服务的数量。因此需要综合考虑SaaS模型的带宽大小和应用服务器的数量,合适的搭配能够消除数据中心的网络瓶颈问题。如果不自行构建数据中心,选择外包托管(如阿里云,腾讯云,亚马逊)方式,则应该考虑以下因素:

  1. 数据中心是否高可用(7x24x365)?
  2. 是否具备监控和灾备能力(容灾和数据恢复)?
  3. 是否具备数据安全防护能力(防攻击,防破解)?
  4. 是否具备可测试能力(完备的API测试接口,沙盒测试环境,SDK开发包)?

5.构建基础架构组件

​ 基础设施设计完成后,我们须准备可靠的、稳定的、经过实际验证的功能组件,此过程对于确保SaaS模型的高可用性能指标尤为重要。在评估这些硬件指标时,我们还需要确保其能够在项目交付日期范围内交付所有的硬件组件并确保调试正常。

6.向基础设施中持续部署SaaS应用

​ 一旦基础设施可用,开发团队就应该持续构建和部署SaaS应用。此时应对服务器进行统一的管理和配置,根据不同的业务需要安装对应的操作系统,分配指定的IP,设置用户访问策略以及配置防火墙等。

7.容灾和数据恢复

​ 现在,SaaS应用程序已经准备就绪,下一步应该考虑的问题是如何容灾和数据恢复的问题。面对容灾和数据恢复,需要考虑一下几点:

  1. 如何应对灾难的发生(不可抗拒因素不在考虑范围)
  2. 如何缩小受灾范围以及如何减小受灾带来的影响
  3. 如何在灾难发生时,应用程序还可以局部可用
  4. 如何在有限的时间范围内完成数据恢复

8.系统监控

​ 系统监控子系统对于SaaS模型特别重要,好的应用监控子系统有助于更早的发现问题,及时的避免灾难的发生。在SaaS模型中,监控子系统应该对一下的参数进行监控:

  1. 内存和CPU的使用率
  2. 磁盘的I/O读写速率
  3. 操作系统和应用程序日志
  4. 其他应用组件(数据库、Web容器、TCP等)性能参数

9.建立客服中心

​ 一旦SaaS应用成功投放市场,它就需要有一个客户支持服务中心(面向业务或者技术)。客户支持服务中心应该具备适当的票据处理和在线沟通能力,一个优秀的SaaS模型,客户支持服务是必不可少的,它有助于提升SaaS软件的服务质量。客户支持服中心应当适时地提供邮件、短信、电话、票据等服务能力,如果SaaS模型中有需要技术团队继续关注的问题,还应该提供工单处理服务,以便在发生技术故障时,能够以最短的时间将故障信息以邮件或者短信的形式发送给相关的技术人员。

10.SaaS成熟度模型

​ 在实施SaaS模型时,选择何种等级的成熟度模型,直接决定了你需要为此付出的资金成本,同时也影响着软件的交付周期和开发风险。因此,在面对此问题时,可以先考虑第二级或者第三级SaaS软件成熟度模型,然后根据业务量和用户规模,逐步满足高性能、可配置、自定义等更高级别成熟度模型的要求,以降低实施SaaS模型的风险。SaaS软件成熟度模型评估对照表如下图所示:

11.规范化的文档

​ 在完成上述10个关键步骤后,需对整个SaaS模型做出详细的记录。如需求文档,设计文档,组件文档以及接口文档等。这些文档将帮助运维人员在运维过程中快速定位系统故障。如果SaaS模型在后期有任何的修改和升级操作,这些文档也可以帮助工作人员了解SaaS模型。

架构师必备技能指南:SaaS(软件即服务)架构设计

架构师必备技能指南:SaaS(软件即服务)架构设计

架构师必备技能指南:SaaS(软件即服务)架构设计

SaaS(软件即服务)平台架构设计指南

1、介绍

从计算机诞生开始,就伴随着计算机应用程序的演变。简短的回顾历史,我们可以清楚的看到应用程序发生的巨大变化。上世纪70年代中期,随着个人PC机的爆炸式增长以及程序员的崛起,让计算机的计算能力得到了大跨越的提升,个人PC机上可以运行非常复杂的应用程序。

进入上世纪80年代,随着Bulletin Board System(简称:BBS)电子公告板系统的兴起,它可以为广大PC机用户提供基本的在线服务,如在线聊天、电子邮件、消息发送和文件下载。由于受到那个时代计算机网络传输速度的限制,在线服务的响应速度慢,交互体验差是最大的通病。

进入90年代中后期,随着万维网的出现,计算机的计算能开始进入快速提升阶段,加之网络基础设施的持续完善,计算机网络技术也随之发展起来,这让Web网站可以提供功能多元化和更为复杂的在线服务,直到今天,我们所看到的互联网(或云)开发的在线服务应用程序。

在这段计算机技术快速成长的时间里,计算机软件到底发生了哪些变化?从历史的发展中,我们可以看到,应用程序本身没有发生本质的变化(程序=数据结构+算法),变化的是软件的供需方式发生了改变。现在,应用程序消费者不需要再在他们的PC机上下载和安装特定的应用程序,即可获得软件所提供的计算服务。在云计算技术的支持下,消费者(企业或个人)只需要使用Web工具(浏览器)访问并登录软件提供商的Web系统,通过简单的配置,就可以获得自己所需应用程序服务。这种通过网络即可使用软件的服务,即使SaaS(软件即服务)。

图 1-1 2015中国SaaS生态

在本篇文章中,我们将着重介绍SaaS架构设计,并围绕WHAT(是什么?)、WHY(为什么?)、WHERE(在哪里?)和HOW(怎么样?)这四个问题,对以下的几点进行阐述:

图 1-2 文章结构
  • 1、什么是SaaS平台?
  • 2、为什么需要使用SaaS平台架构?
  • 3、SaaS平台主要的特性和优势有哪些?
  • 4、SaaS平台适合在什么领域进行实施?
  • 5、SaaS平台有哪些先天性的缺陷?
  • 6、SaaS平台有哪些核心的组件?
  • 7、实施SaaS架构设计时的注意事项有哪些?

2、什么是SaaS平台?

图 2-1 SaaS组成结构

在你决定实施SaaS品台架构设计前,你有必要先了解SaaS平台是什么。从宏观的角度来看,SaaS是一种软件应用程序交付方式,软件提供商集中化托管一个或多个软件应用程序,并通过互联网向租户体用这些软件应用程序。从分类上看,SaaS(软件即服务)也是云计算重要的一部分。目前国内主流的云服务提供商如阿里云、百度云、腾讯云等,为广大用户提供了不同业务需求的云服务,它们大致可以分为以下几类:

  • 1、基础设施即服务:如CPU、Network、Disk和Memory等
  • 2、平台即服务:如阿里云服务器和云数据库等
  • 3、软件即服务:阿里短信、阿里邮箱等
  • 4、数据即服务:如阿里云对象存储,七牛云存储等
  • 5、其他软件服务:机器学习、人工智能等

SaaS应用程序的任何更新或者修复漏洞操作都是由软件提供商负责实施和处理的,由于租户是通过互联网获取软件服务,所以租户端无需下载任何的升级包或者修复补丁,是一种开箱即获取最新软件产品的服务方式。

通过对什么是SaaS的介绍,接下来,我们了解一下选择SaaS作为软件架构来设计产品的一些理由。

3、为什么选择SaaS?

我们将从不同的角度来阐述几个为什么选择SaaS的理由。透过对这些因素的分析,为你是否需要将自己的软件SaaS化提供一定的参考依据。

3.1、消费者角度

获取软件服务的方式足够简单,SaaS也许是迄今为止使用软件最简单的方式之一,租户只需要动动鼠标和键盘,即可在几小时甚至几分钟内获得一个大型的软件服务。相比于传统使用软件的方式,租户省去了研发、部署、运维等一系列繁复的过程,且获得软件的时间和费用成本都大幅度降低。

3.2、商业角度

SaaS可以体用跨地域、跨平台的软件服务。与此同时,软件服务商可以统一对软件进行版本管理,这将带来以下几点好处(包括但不限于):

  • 1、缩短产品上线时间:多端适配,统一版本,统一更新
  • 2、降低维护成本:不需要同时维护多个版本的软件实例,运维压力减小
  • 3、容易升级:由于版本得到有效控制,一次升级,即可覆盖所有租户端

4、SaaS的特性和优势

我们将SaaS应用程序与传统的桌面应用程序做一个水平的对比,部署一个SaaS产品将可以获得以下的几点优势。

4.1、简单

SaaS化的产品通过互联网向租户提供软件服务,随着Web技术(如jQuery、Node.js)的进步,Web页面的交互体验度大幅度提升,交互更流畅、更人性化。与传统的桌面应用程序的人机交互效果相差无几。

4.2、经济实惠

SaaS化产品可以为租户提供弹性的付费方案,如按日、按月、按年、按使用人数或者按使用量进行计费,它将给租户提供更经济的使用软件的财务预算表。

4.3、安全

使用SaaS产品无需担心数据安全问题,这好比将钱存入银行一样安全。相较于企业内部部署的软件系统而言,SaaS产品具备更高的安全保障能力,因为软件提供商具有更多软件安全防护的技术资源、人力资源和财政资源。

4.4、兼容性

与传统软件相比、SaaS软件的兼容性更好,它没有传统软件的多本版维护问题和操作系统兼容问题。在SaaS软件中,租户用户在使用软件的过程中,几乎上感觉不到软件发生了改变。当租户用户登录到系统上时,就已经获得了最新版本的软件。

5、SaaS软件的适用范围

SaaS产品具有广泛的适应范围,特别是与其他云产品(如IaaS(基础设施即服务)和PaaS(平台即服务))配合使用时这种能力表现尤为突出,例如阿里云之类的云计算技术允许你配置可托管的Web站点、数据库服务器等。你只需要打开浏览器并登录到阿里云控制台,通过操作对应的控制面板,即可获得相关的软件服务。

从理论上讲,SaaS可以将任何的软件SaaS,下面列举一些通用的分类供大家参考:

  • 1、Office在线办公类SaaS产品
  • 2、电子邮件和即时消息类SaaS产品
  • 3、社交媒体类SaaS产品
  • 4、第三方API类SaaS产品
  • 5、安全和访问控制类SaaS产品
  • 6、机器学习类SaaS产品
  • 7、人工智能类SaaS产品
  • 8、地理位置服务类SaaS产品
  • 9、数据流和数据检索类SaaS产品

6、SaaS产品的天生缺陷

图 6-1 SaaS产品的缺点

从上图我们可以直观的看到,SaaS产品与生俱来的几个缺陷,接下来我们将逐一进行描述。

6.1、软件控制权

与企业内部部署的软件不同,由于SaaS软件被击中托管在服务提供商的Web服务器中,所以租户无法控制所有的软件应用程序,SaaS化的软件比企业自行部署的软件获得的控制权更少,租户可操作的自定义控制权极度有限。

6.2、消费者基数小

由于SaaS软件是将一套应用程序共享给一个或者多个租户共同使用,这种共享的消费方式还未被大多数的消费者所接受。同时,受制于市场环境的影响,目前还有大多数的软件还未SaaS化。

6.3、性能瓶颈

共享应用程序必然会带来服务器性能的下降、如计算速度、网络资源、I/O读写等都将面临严峻的考验。在性能方面,企业内部部署的“独享模式”的应用程序比SaaS软件的“共享模式”略胜一筹。

6.4、安全问题

当租户在选择一款SaaS产品时,产品的安全性将会被放置在第一位进行考虑。如数据的隔离、敏感数据的加密、数据访问权限控制、个人隐私等问题。在2018年5月25日,GDPR(General Data Protection Regulation)《通用数据保护条例》出现之后,越来越多的人开始重视数据安全问题。如何最大程度的打消租户的这一顾虑,需要服务提供商加强对自身信誉度的提升,以赢得租户的信赖。

7、SaaS产品的核心组件

不同类型的SaaS产品,由于要面对不同的用户愿景,可能在功能和业务上会有所不同,但任何一个SaaS产品,都具备以下几个共同的核心组件。

图 7-1 SaaS 核心组件

7.1、安全组件

在SaaS产品中,系统安全永远是第一位需要考虑的事情,如何保障租户数据的安全,是你首要的事情。这如同银行首选需要保障储户资金安全一样。安全组件就是统一的对SaaS产品进行安全防护,保障系统数据安全。

7.2、数据隔离组件

安全组件解决了用户数据安全可靠的问题,但数据往往还需要解决隐私问题,各企业之间的数据必须相互不可见,即相互隔离。在SaaS产品中,如何识别、区分、隔离个租户的数据时你在实施SaaS平台架构设计时需要考虑的第二个问题。

7.3、可配置组件

尽管SaaS产品在设计之初就考虑了大多数通用的功能,让租户开箱即用,但任然有为数不少的租户需要定制服务自身业务需求的配置项,如UI布局、主题、标识(Logo)等信息。正因为无法抽象出一个完全通用的应用程序,所以在SaaS产品中,你需要提供一个可用于自定义配置的组件。

7.4、可扩展组件

随着SaaS产品业务和租户数量的增长,原有的服务器配置将无法继续满足新的需求,系统性能将会与业务量和用户量成反比。此时,SaaS产品应该具备水平扩展的能力。如通过网络负载均衡其和容器技术,在多个服务器上部署多个软件运行示例并提供相同的软件服务,以此实现水平扩展SaaS产品的整体服务性能。为了实现可扩展能力,就需要SaaS展示层的代码与业务逻辑部分的代码进行分离,两者独立部署。例如使用VUE+微服务构建前后端分离且可水平进行扩展的分布式SaaS应用产品。对于可扩展,还有另外一种方式,即垂直扩展,其做法比较简单,也比较粗暴:通过增加单台服务器的配置,如购买性能更好的CUP、存储更大的内存条、增大带宽等措施,让服务器能够处理更多的用户请求。但此做法对于提升产品性能没有质的改变,且成本很高。

7.5、0停机时间升级产品

以往的软件在升级或者修复Bug是,都需要将运行的程序脱机一段时间,等待升级或修复工作完成后,再重新启动应用程序。而SaaS产品则需要全天候保障服务的可用性。这就需要你考虑如何实现在不重启原有应用程序的情况下,完成应用程序的升级修复工作。

7.6、多租户组件

要将原有产品SaaS化,就必须提供多租户组件,多租户组件是衡量一个应用程序是否具备SaaS服务能力的重要指标之一。SaaS产品需要同时容纳多个租户的数据,同时还需要保证各租户之间的数据不会相互干扰,保证租户中的用户能够按期望索引到正确的数据,多租户组件是你必须要解决的一个问题。其余的组件都将围绕此组件展开各自的业务。

总结

本文将软件应用程序的发展历程作为切入点,并围绕WHAT(是什么?)、WHY(为什么?)、WHERE(在哪些领域实施?)和HOW(怎么样?)这四个问题对SaaS展开了介绍。文中详细的阐述了基于SaaS架构的软件设计需要注意的问题,并分析了SaaS产品的特性、有点、缺点。最后还介绍了基于SaaS架构的软件产品应该具备的几个核心组件以及他们各自的作用。希望本次能够让你对SaaS平台架构有一个全面的了解,并且在你准备实施SaaS平台架构设计前能够提供一些价值的参考信息。

Spring Boot 构建多租户SaaS平台核心技术指南

Spring Boot 构建多租户SaaS平台核心技术指南

本次教程所涉及到的源码已上传至Github,如果你不需要继续阅读下面的内容,你可以直接点击此链接获取源码内容。https://github.com/ramostear/una-saas-toturial

1. 概述

笔者从2014年开始接触SaaS(Software as a Service),即多租户(或多承租)软件应用平台;并一直从事相关领域的架构设计及研发工作。机缘巧合,在笔者本科毕业设计时完成了一个基于SaaS的高效财务管理平台的课题研究,从中收获颇多。最早接触SaaS时,国内相关资源匮乏,唯一有的参照资料是《互联网时代的软件革命:SaaS架构设计》(叶伟等著)一书。最后课题的实现是基于OSGI(Open Service Gateway Initiative)Java动态模块化系统规范来实现的。

时至今日,五年的时间过去了,软件开发的技术发生了巨大的改变,笔者所实现SaaS平台的技术栈也更新了好几波,真是印证了那就话:“山重水尽疑无路,柳暗花明又一村”。基于之前走过的许多弯路和踩过的坑,以及近段时间有许多网友问我如何使用Spring Boot实现多租户系统,决定写一篇文章聊一聊关于SaaS的硬核技术。

说起SaaS,它只是一种软件架构,并没有多少神秘的东西,也不是什么很难的系统,我个人的感觉,SaaS平台的难度在于商业上的运营,而非技术上的实现。就技术上来说,SaaS是这样一种架构模式:它让多个不同环境的用户使用同一套应用程序,且保证用户之间的数据相互隔离。现在想想看,这也有点共享经济的味道在里面。

笔者在这里就不再深入聊SaaS软件成熟度模型和数据隔离方案对比的事情了。今天要聊的是使用Spring Boot快速构建独立数据库/共享数据库独立Schema的多租户系统。我将提供一个SaaS系统最核心的技术实现,而其他的部分有兴趣的朋友可以在此基础上自行扩展。

2. 尝试了解多租户的应用场景

假设我们需要开发一个应用程序,并且希望将同一个应用程序销售给N家客户使用。在常规情况下,我们需要为此创建N个Web服务器(Tomcat),N个数据库(DB),并为N个客户部署相同的应用程序N次。现在,如果我们的应用程序进行了升级或者做了其他任何的改动,那么我们就需要更新N个应用程序同时还需要维护N台服务器。接下来,如果业务开始增长,客户由原来的N个变成了现在的N+M个,我们将面临N个应用程序和M个应用程序版本维护,设备维护以及成本控制的问题。运维几乎要哭死在机房了…

为了解决上述的问题,我们可以开发多租户应用程序,我们可以根据当前用户是谁,从而选择对应的数据库。例如,当请求来自A公司的用户时,应用程序就连接A公司的数据库,当请求来自B公司的用户时,自动将数据库切换到B公司数据库,以此类推。从理论上将没有什么问题,但我们如果考虑将现有的应用程序改造成SaaS模式,我们将遇到第一个问题:如果识别请求来自哪一个租户?如何自动切换数据源?

3. 维护、识别和路由租户数据源

我们可以提供一个独立的库来存放租户信息,如数据库名称、链接地址、用户名、密码等,这可以统一的解决租户信息维护的问题。租户的识别和路由有很多种方法可以解决,下面列举几个常用的方式:

  • 1.可以通过域名的方式来识别租户:我们可以为每一个租户提供一个唯一的二级域名,通过二级域名就可以达到识别租户的能力,如tenantone.example.com,tenant.example.com;tenantone和tenant就是我们识别租户的关键信息。
  • 2.可以将租户信息作为请求参数传递给服务端,为服务端识别租户提供支持,如saas.example.com?tenantId=tenant1,saas.example.com?tenantId=tenant2。其中的参数tenantId就是应用程序识别租户的关键信息。
  • 3.可以在请求头(Header)中设置租户信息,例如JWT等技术,服务端通过解析Header中相关参数以获得租户信息。
  • 4.在用户成功登录系统后,将租户信息保存在Session中,在需要的时候从Session取出租户信息。

解决了上述问题后,我们再来看看如何获取客户端传入的租户信息,以及在我们的业务代码中如何使用租户信息(最关键的是DataSources的问题)。

我们都知道,在启动Spring Boot应用程序之前,就需要为其提供有关数据源的配置信息(有使用到数据库的情况下),按照一开始的需求,有N个客户需要使用我们的应用程序,我们就需要提前配置好N个数据源(多数据源),如果N<50,我认为我还能忍受,如果更多,这样显然是无法接受的。为了解决这一问题,我们需要借助Hibernate 5提供的动态数据源特性,让我们的应用程序具备动态配置客户端数据源的能力。简单来说,当用户请求系统资源时,我们将用户提供的租户信息(tenantId)存放在ThreadLoacal中,紧接着获取TheadLocal中的租户信息,并根据此信息查询单独的租户库,获取当前租户的数据配置信息,然后借助Hibernate动态配置数据源的能力,为当前请求设置数据源,最后之前用户的请求。这样我们就只需要在应用程序中维护一份数据源配置信息(租户数据库配置库),其余的数据源动态查询配置。接下来,我们将快速的演示这一功能。

4. 项目构建

我们将使用Spring Boot 2.1.5版本来实现这一演示项目,首先你需要在Maven配置文件中加入如下的一些配置:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.47</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-freemarker</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
    </dependencies>

然后提供一个可用的配置文件,并加入如下的内容:

spring:
  freemarker:
    cache: false
    template-loader-path:
    - classpath:/templates/
    prefix:
    suffix: .html
  resources:
    static-locations:
    - classpath:/static/
  devtools:
    restart:
      enabled: true
  jpa:
    database: mysql
    show-sql: true
    generate-ddl: false
    hibernate:
      ddl-auto: none
una:
  master:
    datasource:
      url:  jdbc:mysql://localhost:3306/master_tenant?useSSL=false
      username: root
      password: root
      driverClassName:  com.mysql.jdbc.Driver
      maxPoolSize:  10
      idleTimeout:  300000
      minIdle:  10
      poolName: master-database-connection-pool
logging:
  level:
    root: warn
    org:
      springframework:
        web:  debug
      hibernate: debug

由于采用Freemarker作为视图渲染引擎,所以需要提供Freemarker的相关技术

una:master:datasource配置项就是上面说的统一存放租户信息的数据源配置信息,你可以理解为主库。

接下来,我们需要关闭Spring Boot自动配置数据源的功能,在项目主类上添加如下的设置:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class UnaSaasApplication {

    public static void main(String[] args) {
        SpringApplication.run(UnaSaasApplication.class, args);
    }

}

最后,让我们看看整个项目的结构:

5. 实现租户数据源查询模块

我们将定义一个实体类存放租户数据源信息,它包含了租户名,数据库连接地址,用户名和密码等信息,其代码如下:

@Data
@Entity
@Table(name = "MASTER_TENANT")
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MasterTenant implements Serializable{

    @Id
    @Column(name="ID")
    private String id;

    @Column(name = "TENANT")
    @NotEmpty(message = "Tenant identifier must be provided")
    private String tenant;

    @Column(name = "URL")
    @Size(max = 256)
    @NotEmpty(message = "Tenant jdbc url must be provided")
    private String url;

    @Column(name = "USERNAME")
    @Size(min = 4,max = 30,message = "db username length must between 4 and 30")
    @NotEmpty(message = "Tenant db username must be provided")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 4,max = 30)
    @NotEmpty(message = "Tenant db password must be provided")
    private String password;

    @Version
    private int version = 0;
}

持久层我们将继承JpaRepository接口,快速实现对数据源的CURD操作,同时提供了一个通过租户名查找租户数据源的接口,其代码如下:

package com.ramostear.una.saas.master.repository;

import com.ramostear.una.saas.master.model.MasterTenant;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:22
 * @modify by :
 * @since:
 */
@Repository
public interface MasterTenantRepository extends JpaRepository<MasterTenant,String>{

    @Query("select p from MasterTenant p where p.tenant = :tenant")
    MasterTenant findByTenant(@Param("tenant") String tenant);
}

业务层提供通过租户名获取租户数据源信息的服务(其余的服务各位可自行添加):

package com.ramostear.una.saas.master.service;

import com.ramostear.una.saas.master.model.MasterTenant;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:26
 * @modify by :
 * @since:
 */

public interface MasterTenantService {
    /**
     * Using custom tenant name query
     * @param tenant    tenant name
     * @return          masterTenant
     */
    MasterTenant findByTenant(String tenant);
}

最后,我们需要关注的重点是配置主数据源(Spring Boot需要为其提供一个默认的数据源)。在配置之前,我们需要获取配置项,可以通过@ConfigurationProperties(“una.master.datasource”)获取配置文件中的相关配置信息:

@Getter
@Setter
@Configuration
@ConfigurationProperties("una.master.datasource")
public class MasterDatabaseProperties {

    private String url;

    private String password;

    private String username;

    private String driverClassName;

    private long connectionTimeout;

    private int maxPoolSize;

    private long idleTimeout;

    private int minIdle;

    private String poolName;

    @Override
    public String toString(){
        StringBuilder builder = new StringBuilder();
        builder.append("MasterDatabaseProperties [ url=")
                .append(url)
                .append(", username=")
                .append(username)
                .append(", password=")
                .append(password)
                .append(", driverClassName=")
                .append(driverClassName)
                .append(", connectionTimeout=")
                .append(connectionTimeout)
                .append(", maxPoolSize=")
                .append(maxPoolSize)
                .append(", idleTimeout=")
                .append(idleTimeout)
                .append(", minIdle=")
                .append(minIdle)
                .append(", poolName=")
                .append(poolName)
                .append("]");
        return builder.toString();
    }
}

接下来是配置自定义的数据源,其源码如下:

package com.ramostear.una.saas.master.config;

import com.ramostear.una.saas.master.config.properties.MasterDatabaseProperties;
import com.ramostear.una.saas.master.model.MasterTenant;
import com.ramostear.una.saas.master.repository.MasterTenantRepository;
import com.zaxxer.hikari.HikariDataSource;
import lombok.extern.slf4j.Slf4j;
import org.hibernate.cfg.Environment;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.JpaVendorAdapter;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.persistence.EntityManagerFactory;
import javax.sql.DataSource;
import java.util.Properties;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/25 0025-8:31
 * @modify by :
 * @since:
 */
@Configuration
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = {"com.ramostear.una.saas.master.model","com.ramostear.una.saas.master.repository"},
                       entityManagerFactoryRef = "masterEntityManagerFactory",
                       transactionManagerRef = "masterTransactionManager")
@Slf4j
public class MasterDatabaseConfig {

    @Autowired
    private MasterDatabaseProperties masterDatabaseProperties;

    @Bean(name = "masterDatasource")
    public DataSource masterDatasource(){
        log.info("Setting up masterDatasource with :{}",masterDatabaseProperties.toString());
        HikariDataSource datasource = new HikariDataSource();
        datasource.setUsername(masterDatabaseProperties.getUsername());
        datasource.setPassword(masterDatabaseProperties.getPassword());
        datasource.setJdbcUrl(masterDatabaseProperties.getUrl());
        datasource.setDriverClassName(masterDatabaseProperties.getDriverClassName());
        datasource.setPoolName(masterDatabaseProperties.getPoolName());
        datasource.setMaximumPoolSize(masterDatabaseProperties.getMaxPoolSize());
        datasource.setMinimumIdle(masterDatabaseProperties.getMinIdle());
        datasource.setConnectionTimeout(masterDatabaseProperties.getConnectionTimeout());
        datasource.setIdleTimeout(masterDatabaseProperties.getIdleTimeout());
        log.info("Setup of masterDatasource successfully.");
        return datasource;
    }

    @Primary
    @Bean(name = "masterEntityManagerFactory")
    public LocalContainerEntityManagerFactoryBean masterEntityManagerFactory(){
        LocalContainerEntityManagerFactoryBean lb = new LocalContainerEntityManagerFactoryBean();
        lb.setDataSource(masterDatasource());
        lb.setPackagesToScan(
           new String[]{MasterTenant.class.getPackage().getName(), MasterTenantRepository.class.getPackage().getName()}
        );

        //Setting a name for the persistence unit as Spring sets it as 'default' if not defined.
        lb.setPersistenceUnitName("master-database-persistence-unit");

        //Setting Hibernate as the JPA provider.
        JpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        lb.setJpaVendorAdapter(vendorAdapter);

        //Setting the hibernate properties
        lb.setJpaProperties(hibernateProperties());

        log.info("Setup of masterEntityManagerFactory successfully.");
        return lb;
    }

    @Bean(name = "masterTransactionManager")
    public JpaTransactionManager masterTransactionManager(@Qualifier("masterEntityManagerFactory")EntityManagerFactory emf){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(emf);
        log.info("Setup of masterTransactionManager successfully.");
        return transactionManager;
    }

    @Bean
    public PersistenceExceptionTranslationPostProcessor exceptionTranslationPostProcessor(){
        return new PersistenceExceptionTranslationPostProcessor();
    }

    private Properties hibernateProperties(){
        Properties properties = new Properties();
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        return properties;
    }
}

在改配置类中,我们主要提供包扫描路径,实体管理工程,事务管理器和数据源配置参数的配置。

6. 实现租户业务模块

在此小节中,租户业务模块我们仅提供一个用户登录的场景来演示SaaS的功能。其实体层、业务层和持久化层根普通的Spring Boot Web项目没有什么区别,你甚至感觉不到它是一个SaaS应用程序的代码。

首先,创建一个用户实体User,其源码如下:

@Entity
@Table(name = "USER")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User implements Serializable {
    private static final long serialVersionUID = -156890917814957041L;

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "USERNAME")
    private String username;

    @Column(name = "PASSWORD")
    @Size(min = 6,max = 22,message = "User password must be provided and length between 6 and 22.")
    private String password;

    @Column(name = "TENANT")
    private String tenant;
}

业务层提供了一个根据用户名检索用户信息的服务,它将调用持久层的方法根据用户名对租户的用户表进行检索,如果找到满足条件的用户记录,则返回用户信息,如果没有找到,则返回null;持久层和业务层的源码分别如下:

@Repository
public interface UserRepository extends JpaRepository<User,String>,JpaSpecificationExecutor<User>{

    User findByUsername(String username);
}
@Service("userService")
public class UserServiceImpl implements UserService{

    @Autowired
    private UserRepository userRepository;

    private static TwitterIdentifier identifier = new TwitterIdentifier();



    @Override
    public void save(User user) {
        user.setId(identifier.generalIdentifier());
        user.setTenant(TenantContextHolder.getTenant());
        userRepository.save(user);
    }

    @Override
    public User findById(String userId) {
        Optional<User> optional = userRepository.findById(userId);
        if(optional.isPresent()){
            return optional.get();
        }else{
            return null;
        }
    }

    @Override
    public User findByUsername(String username) {
        System.out.println(TenantContextHolder.getTenant());
        return userRepository.findByUsername(username);
    }

在这里,我们采用了Twitter的雪花算法来实现了一个ID生成器。

7. 配置拦截器

我们需要提供一个租户信息的拦截器,用以获取租户标识符,其源代码和配置拦截器的源代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-23:17
 * @modify by :
 * @since:
 */
@Slf4j
public class TenantInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String tenant = request.getParameter("tenant");
        if(StringUtils.isBlank(tenant)){
            response.sendRedirect("/login.html");
            return false;
        }else{
            TenantContextHolder.setTenant(tenant);
            return true;
        }
    }
}
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new TenantInterceptor()).addPathPatterns("/**").excludePathPatterns("/login.html");
        super.addInterceptors(registry);
    }
}

/login.html是系统的登录路径,我们需要将其排除在拦截器拦截的范围之外,否则我们永远无法进行登录

8. 维护租户标识信息

在这里,我们使用ThreadLocal来存放租户标识信息,为动态设置数据源提供数据支持,该类提供了设置租户标识、获取租户标识以及清除租户标识三个静态方法。其源码如下:

public class TenantContextHolder {

    private static final ThreadLocal<String> CONTEXT = new ThreadLocal<>();

    public static void setTenant(String tenant){
        CONTEXT.set(tenant);
    }

    public static String getTenant(){
        return CONTEXT.get();
    }

    public static void clear(){
        CONTEXT.remove();
    }
}

此类时实现动态数据源设置的关键

9. 动态数据源切换

要实现动态数据源切换,我们需要借助两个类来完成,CurrentTenantIdentifierResolver和AbstractDataSourceBasedMultiTenantConnectionProviderImpl。从它们的命名上就可以看出,一个负责解析租户标识,一个负责提供租户标识对应的租户数据源信息。

首先,我们需要实现CurrentTenantIdentifierResolver接口中的resolveCurrentTenantIdentifier()和validateExistingCurrentSessions()方法,完成租户标识的解析功能。实现类的源码如下:

package com.ramostear.una.saas.tenant.config;

import com.ramostear.una.saas.context.TenantContextHolder;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.context.spi.CurrentTenantIdentifierResolver;

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/26 0026-22:38
 * @modify by :
 * @since:
 */
public class CurrentTenantIdentifierResolverImpl implements CurrentTenantIdentifierResolver {

    /**
     * 默认的租户ID
     */
    private static final String DEFAULT_TENANT = "tenant_1";

    /**
     * 解析当前租户的ID
     * @return
     */
    @Override
    public String resolveCurrentTenantIdentifier() {
        //通过租户上下文获取租户ID,此ID是用户登录时在header中进行设置的
        String tenant = TenantContextHolder.getTenant();
        //如果上下文中没有找到该租户ID,则使用默认的租户ID,或者直接报异常信息
        return StringUtils.isNotBlank(tenant)?tenant:DEFAULT_TENANT;
    }

    @Override
    public boolean validateExistingCurrentSessions() {
        return true;
    }
}

此类的逻辑非常简单,就是从ThreadLocal中获取当前设置的租户标识符

有了租户标识符解析类之后,我们需要扩展租户数据源提供类,实现从数据库动态查询租户数据源信息,其源码如下:

@Slf4j
@Configuration
public class DataSourceBasedMultiTenantConnectionProviderImpl extends AbstractDataSourceBasedMultiTenantConnectionProviderImpl{

    private static final long serialVersionUID = -7522287771874314380L;

    @Autowired
    private MasterTenantRepository masterTenantRepository;

    private Map<String,DataSource> dataSources = new TreeMap<>();

    @Override
    protected DataSource selectAnyDataSource() {
        if(dataSources.isEmpty()){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(), DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.values().iterator().next();
    }

    @Override
    protected DataSource selectDataSource(String tenant) {
        if(!dataSources.containsKey(tenant)){
            List<MasterTenant> tenants = masterTenantRepository.findAll();
            tenants.forEach(masterTenant->{
                dataSources.put(masterTenant.getTenant(),DataSourceUtils.wrapperDataSource(masterTenant));
            });
        }
        return dataSources.get(tenant);
    }
}

在该类中,通过查询租户数据源库,动态获得租户数据源信息,为租户业务模块的数据源配置提供数据数据支持。

最后,我们还需要提供租户业务模块数据源配置,这是整个项目核心的地方,其代码如下:

@Slf4j
@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = {
        "com.ramostear.una.saas.tenant.model",
        "com.ramostear.una.saas.tenant.repository"
})
@EnableJpaRepositories(basePackages = {
        "com.ramostear.una.saas.tenant.repository",
        "com.ramostear.una.saas.tenant.service"
},entityManagerFactoryRef = "tenantEntityManagerFactory"
,transactionManagerRef = "tenantTransactionManager")
public class TenantDataSourceConfig {

    @Bean("jpaVendorAdapter")
    public JpaVendorAdapter jpaVendorAdapter(){
        return new HibernateJpaVendorAdapter();
    }

    @Bean(name = "tenantTransactionManager")
    public JpaTransactionManager transactionManager(EntityManagerFactory entityManagerFactory){
        JpaTransactionManager transactionManager = new JpaTransactionManager();
        transactionManager.setEntityManagerFactory(entityManagerFactory);
        return transactionManager;
    }

    @Bean(name = "datasourceBasedMultiTenantConnectionProvider")
    @ConditionalOnBean(name = "masterEntityManagerFactory")
    public MultiTenantConnectionProvider multiTenantConnectionProvider(){
        return new DataSourceBasedMultiTenantConnectionProviderImpl();
    }

    @Bean(name = "currentTenantIdentifierResolver")
    public CurrentTenantIdentifierResolver currentTenantIdentifierResolver(){
        return new CurrentTenantIdentifierResolverImpl();
    }

    @Bean(name = "tenantEntityManagerFactory")
    @ConditionalOnBean(name = "datasourceBasedMultiTenantConnectionProvider")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            @Qualifier("datasourceBasedMultiTenantConnectionProvider")MultiTenantConnectionProvider connectionProvider,
            @Qualifier("currentTenantIdentifierResolver")CurrentTenantIdentifierResolver tenantIdentifierResolver
    ){
        LocalContainerEntityManagerFactoryBean localBean = new LocalContainerEntityManagerFactoryBean();
        localBean.setPackagesToScan(
                new String[]{
                        User.class.getPackage().getName(),
                        UserRepository.class.getPackage().getName(),
                        UserService.class.getPackage().getName()

                }
        );
        localBean.setJpaVendorAdapter(jpaVendorAdapter());
        localBean.setPersistenceUnitName("tenant-database-persistence-unit");
        Map<String,Object> properties = new HashMap<>();
        properties.put(Environment.MULTI_TENANT, MultiTenancyStrategy.SCHEMA);
        properties.put(Environment.MULTI_TENANT_CONNECTION_PROVIDER,connectionProvider);
        properties.put(Environment.MULTI_TENANT_IDENTIFIER_RESOLVER,tenantIdentifierResolver);
        properties.put(Environment.DIALECT,"org.hibernate.dialect.MySQL5Dialect");
        properties.put(Environment.SHOW_SQL,true);
        properties.put(Environment.FORMAT_SQL,true);
        properties.put(Environment.HBM2DDL_AUTO,"update");
        localBean.setJpaPropertyMap(properties);
        return localBean;
    }
}

在改配置文件中,大部分内容与主数据源的配置相同,唯一的区别是租户标识解析器与租户数据源补给源的设置,它将告诉Hibernate在执行数据库操作命令前,应该设置什么样的数据库连接信息,以及用户名和密码等信息。

10. 应用测试

最后,我们通过一个简单的登录案例来测试本次课程中的SaaS应用程序,为此,需要提供一个Controller用于处理用户登录逻辑。在本案例中,没有严格的对用户密码进行加密,而是使用明文进行比对,也没有提供任何的权限认证框架,知识单纯的验证SaaS的基本特性是否具备。登录控制器代码如下:

/**
 * @author : Created by Tan Chaohong (alias:ramostear)
 * @create-time 2019/5/27 0027-0:18
 * @modify by :
 * @since:
 */
@Controller
public class LoginController {

    @Autowired
    private UserService userService;

    @GetMapping("/login.html")
    public String login(){
        return "/login";
    }

    @PostMapping("/login")
    public String login(@RequestParam(name = "username") String username, @RequestParam(name = "password")String password, ModelMap model){
        System.out.println("tenant:"+TenantContextHolder.getTenant());
        User user = userService.findByUsername(username);
        if(user != null){
            if(user.getPassword().equals(password)){
                model.put("user",user);
                return "/index";
            }else{
                return "/login";
            }
        }else{
            return "/login";
        }
    }
}

在启动项目之前,我们需要为主数据源创建对应的数据库和数据表,用于存放租户数据源信息,同时还需要提供一个租户业务模块数据库和数据表,用来存放租户业务数据。一切准备就绪后,启动项目,在浏览器中输入:http://localhost:8080/login.html

在登录窗口中输入对应的租户名,用户名和密码,测试是否能够正常到达主页。可以多增加几个租户和用户,测试用户是否正常切换到对应的租户下。

总结

在这里,我分享了使用Spring Boot+JPA快速实现多租户应用程序的方法,此方法只涉及了实现SaaS应用平台的最核心技术手段,并不是一个完整可用的项目代码,如用户的认证、授权等并未出现在本文中。额外的业务模块感兴趣的朋友可以在此设计基础上自行扩展,如对其中的代码有任何的疑问,欢迎大家在下方给我留言。