跳到主要内容

旧系统多租户改造

目标

全站改造,实现多租户隔离

项目架构

项目主要是使用Spring Boot + Mybatis Plus + 多数据源

方案

方案一

对所有表添加租户字段,然后基于Mybatis租户插件进行拦截查询

不要使用Mybatis Plus的拦截器实现,因为可能存在其他项目使用MybatisMybatis Flux等框架

整体改造工作量太大,需要修改所有用到的表结构

方案二

  1. 添加一张路由表,将所有用户划分到不同的租户下
  2. 所有查询基于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有如下特征:

  1. 大部分SQL都是单表查询
  2. 少量关联查询 也是比较标准的关联查询
  3. 极少复杂统计SQL
  4. 部分查询是RPC调用
  5. 异步查询

难点

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注解

有两种改法

  1. 将原始方法进行封装,再添加@AutoRoute注解
  2. 提供一个工具类

比如提供一个RegionRouteTemplate

改造

    List<OrderDO> orders = regionRouteTemplate.execute(() -> this.baseMapper.selectList(wrapper))

这种改造侵入性会小一点

简单的多表查询

对于简单的多表查询,也可以直接使用@AutoRoute注解

具体多复杂的SQL不支持改写,需要看拦截改写插件实现到什么程序,需要自己测试

复杂查询

由于不多直接手动改造

RPC调用

RPC调用去RPC调用的对应系统使用相同的改造方式即可

异步查询

SDK 改写标志位使用TransmittableThreadLocal进行传递

应用挂载TransmittableThreadLocal agent自动包装异步线程池和异步任务

如果觉得挂载TTL agent太重,可以考虑将异步查询改成同步(在性能可接受情况下)