旧系统多租户改造
目标
全站改造,实现多租户隔离
项目架构
项目主要是使用Spring Boot + Mybatis Plus + 多数据源
方案
方案一
对所有表添加租户字段,然后基于Mybatis租户插件进行拦截查询
不要使用
Mybatis Plus的拦截器实现,因为可能存在其他项目使用Mybatis、Mybatis Flux等框架
整体改造工作量太大,需要修改所有用到的表结构
方案二
- 添加一张路由表,将所有用户划分到不同的租户下
- 所有查询基于
Mybatis拦截器实现SQL改写
比如原先查询订单信息
select uid,order_id,create_time from order
改写后的SQL为
select o.uid,o.order_id,o.create_time from order o inner join user_rote ur on o.uid = ur.uid
where
ur.user_region in()
这里需要注意要给原表的字段添加别名,因为原表的字段可能会和
user_rote重名导致SQL异常
主要是关联region进行逻辑隔离
-
优点:
- 改动不需要改动表结构,改造量小
- 整体风险可控,通过注解仅拦截可控SQL,不进行全局拦截
-
缺点:
- 复杂SQL需要手动改造
- 对于没有路由健的表无法进行租户隔离
但是SQL改写实现复杂,对复杂的SQL改写不一定能成功
基于项目现状SQL有如下特征:
- 大部分SQL都是单表查询
- 少量关联查询 也是比较标准的关联查询
- 极少复杂统计SQL
- 部分查询是RPC调用
- 异步查询
难点
PageHelper分页拦截问题
普通SQL可以直接添加注解拦截
PageHelper生成的自动分页不行
Mybatis Plus自动分页
<P extends IPage<T>> P selectPage(P page, @Param(Constants.WRAPPER) Wrapper<T> queryWrapper);
异步查询
一般拦截是使用一个ThreadLocal注入一个标志位,进行拦截。
但是异步线程使用ThreadLocal上下文会丢失,可以考虑使用TransmittableThreadLocal
RPC查询
这种查询仅支持本地查询改造,如果查询为RPC查询,则无法处理
统计查询无法隔离
部分业务的统计数据是直接统计好的,统计表中不存在比如uid,无法通过join进行隔离
技术方案
由于时间紧急,最终考虑选用一种尽量快速,但相对稳定的技术方案
所以选用了方案二
表结构设计
站点表
新增一张站点表,用来记录站点信息
drop table if exists csa_region
create table csa_region
(
id bigint auto_increment
primary key,
region_code varchar(50) not null comment '站点代码,如:global, uk, us, de',
region_name varchar(100) null comment '站点名称',
region_type tinyint not null comment '站点类型(1:全球站;2:严格独立站;3:普通独立站)',
domain_name varchar(255) null comment '主域名,如: example.com',
port int null comment '端口(默认80/443可不存)',
status tinyint default 0 not null comment '状态: 0=启用, 1=禁用',
create_time datetime default CURRENT_TIMESTAMP not null,
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,
constraint uk_site_code
unique (region_code)
)
comment '站点信息表';
路由表
这张表主要用来关联用户路由到哪个站点
drop table if exists csa_user_route
create table csa_user_route
(
id bigint auto_increment
primary key,
uid varchar(50) not null comment '用户uid',
csa_region_id bigint not null comment '站点id',
create_time datetime default CURRENT_TIMESTAMP not null,
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,
remark varchar(100) null comment '备注',
constraint csa_user_route_pk
unique (uid)
)
comment '用户/请求路由规则表';
create index idx_cur_uid_region
on csa_user_route (uid);
运营人员关联表
主要用来记录运营人员的站点信息,可以查询哪些站点的信息
用来实现逻辑隔离
drop table if exists csa_admin_route
create table csa_admin_route
(
id bigint auto_increment
primary key,
sys_admin_id bigint null comment 'admin id',
csa_region_id bigint not null comment '站点id',
create_time datetime default CURRENT_TIMESTAMP not null,
update_time datetime default CURRENT_TIMESTAMP not null on update CURRENT_TIMESTAMP,
constraint csa_admin_route_pk
unique (sys_admin_id, csa_region_id)
)
comment '用户/请求路由规则表';
业务改造
单表查询
单表查询分两种
一种是查询已经封装成方法的
@Select("SELECT * FROM t_merchant")
List<OrderDO> selectAllMerchants();
这种直接添加一个注解即可
提供一个注解比如@AutoRoute
@AutoRoute
@Select("SELECT * FROM t_merchant")
List<OrderDO> selectAllMerchants();
然后就将查询SQL自动改写为join路由表
比如原始SQL
select * from order;
自动改写
select t.* from order t inner join csa_user_route cur on t.uid = cur.uid
where cur.csa_region_id in (select id from csa_admin_route where sys_id = 5);
对于查询没有单独封装,在service中的
比如
@Override
public <OrderVO> selectList(OrderDTO dto) {
// 业务逻辑代码
// 查询代码 需要改造
List<OrderDO> orders = this.baseMapper.selectList(wrapper);
// 查询代码 无需改造
}
由于调用的是Mybatis Plus中封装好的方法,无法添加@AutoRoute注解
有两种改法
- 将原始方法进行封装,再添加
@AutoRoute注解 - 提供一个工具类
比如提供一个RegionRouteTemplate
改造
List<OrderDO> orders = regionRouteTemplate.execute(() -> this.baseMapper.selectList(wrapper))
这种改造侵入性会小一点
简单的多表查询
对于简单的多表查询,也可以直接使用@AutoRoute注解
具体多复杂的SQL不支持改写,需要看拦截改写插件实现到什么程序,需要自己测试
复杂查询
由于不多直接手动改造
RPC调用
RPC调用去RPC调用的对应系统使用相同的改造方式即可
异步查询
SDK 改写标志位使用TransmittableThreadLocal进行传递
应用挂载TransmittableThreadLocal agent自动包装异步线程池和异步任务
如果觉得挂载TTL agent太重,可以考虑将异步查询改成同步(在性能可接受情况下)