头图

动口

老东:A系统定位是一个SaaS系统,需要支持多租户多数据源,每个租户的数据库完全隔离,并且系统通过区分不同租户的请求进行动态数据源的切换,能搞定不?
小西:有点难度,不过我们组内刚才也头脑风暴了一下,可以考虑通过不同的二级域名来区分不同租户,租户信息通过软负载传递给后端系统。

知识点

多租户(Multi-Tenancy)是一种软件架构技术,主要应用于SaaS(Software as a Service)领域,其核心目的是在多用户环境下共用相同的系统或程序组件,同时确保各个用户间的数据隔离性。多租户架构允许在一台服务器上运行单个应用实例,并为多个租户(客户)提供服务。每个租户都有自己的数据、配置和用户界面,可以根据自身需求进行定制和扩展。
1、租户识别
租户信息的识别通过Nginx代理来实现,核心思路就是域名中包含租户信息,然后通过Nginx代理时,在请求头和响应头中添加租户的识别信息。

server {
        listen       8081;
        server_name  ~^(?<sub>.+)\.gzb\.com$;

        location /api {
            proxy_pass http://127.0.0.1:8082;
            proxy_set_header tenant_id $sub;
            add_header tenant_id $sub;
        }
}

通过nginx代理后,请求都会带上租户信息,比如通过aaa.gzb.com这个域名访问系统时会自动识别出租户为aaa。

2、多租户的数据库存储方案
1)独立数据库,一个租户一个数据库,隔离性最好,安全性最高,但成本也越高;
2)共享数据库,多个租户共享一个数据库,但每一个租户一个schema,隔离级别较高,成本较低,但如果需要跨租户统计数据,存在一定困难;
3)共享数据库,多个租户共享一个数据库、schema,在表中通过tenantId字段区分租户的数据,成本最低,安全性最低,但数据恢复与备份困难,需要逐表逐条备份和还原。
我们还是选择独立数据库,Spring-jdbc提供了AbstractRoutingDataSource根据用户定义的规则选择当前的数据源,这样我们可以在执行查询之前,设置使用的数据源。
我们导入依赖:

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>5.2.15.RELEASE</version>
</dependency>

动手

1、定义一个接口,动态获取数据源,查询user表的数据:

public class HelloHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange httpExchange) throws IOException {
        Headers headers = httpExchange.getRequestHeaders();
        DynamicDataSource.setDataSource(headers.get("tenant_id").get(0));
        try {
            Connection connection = DynamicDataSource.getInstance().getConnection();
            String sql = "select user_id, user_name, cellno from user";
            PreparedStatement preparedStatement = connection.prepareStatement(sql);
            ResultSet resultSet = preparedStatement.executeQuery();
            while (resultSet.next()) {
                System.out.println("user_id: " + resultSet.getInt("user_id"));
                System.out.println("user_name: " + resultSet.getString("user_name"));
                System.out.println("cellno: " + resultSet.getString("cellno"));
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
        String response = "Hello Handler " + headers.get("tenant_id").get(0);
        httpExchange.sendResponseHeaders(200, 0);
        OutputStream outputStream = httpExchange.getResponseBody();
        outputStream.write(response.getBytes());
        outputStream.close();
    }
}

2、继承AbstractRoutingDataSource类,实现数据源切换;

public class DynamicDataSource extends AbstractRoutingDataSource {
    private static final ThreadLocal<String> contextHolder = new ThreadLocal<>();

    private static final DynamicDataSource dynamicDataSource;

    static {
        Map<Object, Object> targetDataSources = new HashMap<>();
        BuildDataSources buildDataSources1 = new BuildDataSources("aaa");
        targetDataSources.put("aaa", buildDataSources1.createDataSource());
        BuildDataSources buildDataSources2 = new BuildDataSources("bbb");
        targetDataSources.put("bbb", buildDataSources2.createDataSource());
        dynamicDataSource = new DynamicDataSource(targetDataSources);
    }

    private DynamicDataSource() {
    }

    private DynamicDataSource(Map<Object, Object> targetDataSources) {
        super.setTargetDataSources(targetDataSources);
        super.afterPropertiesSet();
    }

    @Override
    protected Object determineCurrentLookupKey() {
        return getDataSource();
    }

    public static DynamicDataSource getInstance() {
        return dynamicDataSource;
    }

    public static void setDataSource(String dataSource) {
        contextHolder.set(dataSource);
    }

    public static String getDataSource() {
        return contextHolder.get();
    }

    public static void clearDataSource() {
        contextHolder.remove();
    }
}

3、打开浏览器,输入http://aaa.gzb.com:8081/api/HelloHandler,页面返回结果看得出是识别出了aaa租户。

返回数据也是aaa库的数据:

user_id: 1
user_name: coco
cellno: 13726200003
user_id: 2
user_name: lee
cellno: 13726200004
user_id: 3
user_name: lee
cellno: 13726200004

4、打开浏览器,输入http://bbb.gzb.com:8081/api/HelloHandler,页面返回结果看得出是识别出了bbb租户。

返回数据也是bbb库的数据:

user_id: 1
user_name: Jeccy
cellno: 13724154789

背风
1 声望0 粉丝