找回密码
 立即注册

QQ登录

只需一步,快速开始

搜索

微服务系统SaaS化改造实战

2023-4-21 09:15| 发布者: admin| 查看: 4290| 评论: 0

摘要: 随着教务系统功能的不断完善,发现行业内很多公司都有类似教务系统场景的需求,但内部所使用的系统并不成熟。我们就在思考,提供给到用户使用的教务系统是否可市场化?即,SaaS系统的软件和服务市场化。 ... ...

随着教务系统功能的不断完善,发现行业内很多公司都有类似教务系统场景的需求,但内部所使用的系统并不成熟。我们就在思考,提供给到用户使用的教务系统是否可市场化?即,SaaS系统的软件和服务市场化。

 于是,我们进阶提出了这样一个需求:把一个只能服务于一个B端用户(企业)的教务中台系统,改造成一个可服务于N个B端用户的SaaS系统。目标是基于现有系统,以最小代价实现SaaS化改造,以渐进式方式进行,采取小步快跑模式。该系统使用Java语言开发,采用SpringCloud相关组件搭建的微服务系统。

 接下来就给大家介绍一下此次改造的过程,以及遇到的问题的解决方案。

image.png

1、多租户数据隔离方案

SaaS的核心概念就是多租户,而多租户的核心及难点就是数据隔离,数据又分为数据库数据,程序运行时数据。

image.png

首先关于数据库数据隔离方案选型,目前主流有3种隔离方案对比:


image.png

如果使用的是MySQL数据库,方案1和方案2相当于数据库的物理隔离和逻辑隔离的区别。通过分析业务特点,大部分租户短期内规模应该差距不会特别大,创建新租户成本要尽可能地低且不能增加维护成本。方案3多租户只需要维护一套程序和一套数据库,成本最低,最适合当前业务场景。

关于程序运行时数据隔离,对于Java程序来说也只有线程隔离这一条路可选。需要注意的是在线程复用时的脏数据问题,线程使用完后要及时清理缓存数据,以避免数据污染。

2、租户标识传递方案

image.png

B端用户和租户是多对多关系,所以B端用户登录系统后要选择进入哪个租户系统,此时前端把租户标识保存到LocalStorage,后续请求服务器端接口都会在请求头带上租户标识。C端用户通过带有店铺标识的URL进入,前端从URL中取得租户标识保存到LocalStorage,后续请求服务器端接口都会在请求头带上租户标识。

3、系统应用层面的租户隔离

主要涉及两方面:与数据库交互的ORM框架统一支持多租户,目前主流的ORM框架大多都已支持,如果不支持可以通过插件扩展;应用层面采用线程级别的租户隔离,租户信息存储在ThreadLocal中。 

image.png

选定了方案之后,我们开始着手于系统改造。

第一步:数据库改造,表增加tenant_id字段,初始化数据

梳理所有数据库表,除了一些系统共用的基础表以外,其他所有租户相关的表都增加tenant_id字段,并对tenant_id建索引,更新所有组合索引把tenant_id加到第一位

关于tenant_id的初始化,需要根据实际情况而定。如果改造前所有数据都属于一个租户,那就比较简单,在租户表新增一条记录获得租户ID,然后其他所有带tenant_id的表里tenant_id都更新成这个租户ID,即可完成初始化。如果要根据组织架构划分成多个租户,那么就要把原表里的组织ID映射到相应的租户ID,对于原表里没有组织ID的还得通过关联关系查出来。

数据库改造的所有DDL和DML都要记录好,在开发和测试环境充分验证,才能保证上线时顺利执行。

第二步:SQL改造,增加tenantId

经过上一步改造,数据库表里已经有了tenant_id字段,接下来要检查程序中所有SQL。

针对insert要在insert字段增加tenant_id;

针对update,delete,select要在where子句增加tenant_id。

如果针对每一个SQL处理,我们要不厌其烦的在每个SQL上增加 and tenant_id = ? 查询条件,如果稍不留意就会漏加造成数据越界,而且代码侵入严重,工作量巨大。那有没有办法统一增加呢?

我们要改造的这个系统使用的ORM框架是Ebean。于是翻阅Ebean官方文档,发现Ebean果然支持多租户模式,参考:GitHub
-
ebean-orm-examples/example-multi-tenancy-partition 对Ebean进行多租户配置,接着我们只需要提供一个有tenantId的entity基类,让所有需要增加tenantId的entity继承这个基类即可。

如果使用的是MyBatis框架,也有相应的插件可实现类似效果。这样大大降低了代码浸入,而且后面新增SQL也不会漏掉。如果是一些系统层面跨租户的SQL需求该如何处理呢?其实Ebean多租户模式也是通过ThreadLocal传递tenantId,如果ThreadLocal没有tenantId它就不会增加tenantId,我们可以利用这个特性,在执行SQL之前临时把ThreadLocal里的tenantId清除,执行完后再加回去。

第三步:在ThreadLocal中增加tenantId

提供给前端的接口,除了一些系统及的接口,都要在请求头带上tenantId,请求到达后端服务首先会进入Spring拦截器,拦截器从请求头获取tenantId后设置到ThreadLocal中,后续流程有用到tenantId都从ThreadLocal中取得。这里会遇到两个问题:

·   线程池里的线程获取不到tenantId。

线程池里的线程存在复用的情况,我们不能通过创建线程时把tenantId传递过去,应该是提交任务时把ThreadLocal拷贝过去,不过已经有现成的解决方案:transmittable-thread-local 。我们是使用Java
Agent来修饰JDK线程池实现类,对应用代码无侵入,实现线程池线程的ThreadLocal值传递。JVM参数增加:-javaagent:path/to/transmittable-thread-local-2.x.y.jar,存储tenantId的ThreadLocal 改成 com.alibaba.ttl.TransmittableThreadLocal。

·   跨线程池MDC日志追踪时有用到tenantId获取不到的问题。

通过引入logback-mdc-ttl 解决。

第四步:Feign改造,请求头增加tenantId

这个系统是基于SpringCloud的微服务架构,服务间调用是通过Feign实现,我们约定通过请求头传递tenantId。和SQL的改造一样,我们需要针对每一个接口调用,在其请求头增加tenantId,这样就要改动大量代码,显然是后端开发不愿接受的。所以我们要考虑有没有少量代码侵入,甚至无代码侵入的方案。使用过Feign的同学应该都知道Feign有提供拦截器功能,自定义拦截器实现feign.RequestInterceptor接口即可操作feign.RequestTemplate,其中就提供了自定义request

header的方法。这样我们只要增加一个Feign全局拦截器,从ThreadLocal取得tenantId,通过RequestTemplate添加到请求头,即可实现所有Feign调用向下游服务传递tenantId。

经过以上改造,整个系统已具备多租户能力,形成SaaS雏形。我们以少量代码改造完成了整个微服务系统的SaaS化改造,只有entity继承基类需要修改大部分entity类,其他环节基本没有代码侵入。工作量较大的环节其实是数据库改造,以及测试环节。


路过

雷人

握手

鲜花

鸡蛋

相关分类

返回顶部