动口
老东: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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。