SpringBoot單資料庫例項多Schema多租戶實現

介紹

springboot mybatis-plus 實現的單資料庫例項多schema的多租戶系統, 也就是一個租戶使用一個數據庫schema

網上的教程大部分都是基於mybatis-plus的

TenantLineInnerInterceptor

實現所有的租戶透過tenant_id來處理多租戶之間打資料隔離

但是這個並不符合我打需求, 我需要每個租戶使用一個數據庫schema, 和其他的租戶資料完全隔離

本例項只在本地測試透過,請勿用於生產環境!

資料初始化

這裡使用資料庫表 tenant儲存所有的租戶資訊 結構如下

CREATE TABLE `tenant`。`tenant` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(255) DEFAULT NULL, `db_name` varchar(255) DEFAULT ‘’, `alias` varchar(255) DEFAULT NULL COMMENT ‘唯一標識’, PRIMARY KEY (`id`)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;INSERT INTO tenant。tenant (id, name, db_name, alias) VALUES (1, ‘陽光’, ‘db_1’, ‘a’);INSERT INTO tenant。tenant (id, name, db_name, alias) VALUES (2, ‘錘子門’, ‘db_2’, ‘b’);INSERT INTO tenant。tenant (id, name, db_name, alias) VALUES (3, ‘海賊王’, ‘db_3’, ‘c’);

有3條資料,陽光,錘子門,海賊王 分別對應他們自己打資料庫 db_1, db_2, db_3 這3個數據庫裡面有一個user表

CREATE TABLE if not exists `user` ( `id` bigint(22) NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `password` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

租戶識別

租戶的資訊識別是透過域名和nginx的代理來實現 ,思路是給每個租戶分配一個域名,然後透過nginx的代理轉發 本地除錯在/etc/hosts裡面增加

127。0。0。1 a。cdn。system。me127。0。0。1 b。cdn。system。me127。0。0。1 c。cdn。system。me127。0。0。1 d。cdn。system。me

域名前面打abc 分別對應tenant 表中的

alias

欄位 如果使用正式域名,需要在dns解析 那裡增加一個

*。cdn

的泛域名cname到api的域名上面 nginx 就方便來之間使用 serverName *。cdn。xxxx。com;

比如

a。cdn。system。me

透過域名訪問系統時會識別出租戶

a

資料來源的切換

這個是該方案的核心思路, 透過實現mybatis的攔截器

Interceptor

改寫原來的sql, 把sql語句裡面的資料庫表都加上對應的schema

配置spring的攔截器, 識別租戶的標識

@Slf4jpublic class RequestDomainInterceptor implements HandlerInterceptor { @Value(“${domainSuffix}”) private String domainSuffix; @Resource private TenantMapper tenantMapper; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String serverName = request。getServerName(); //擷取域名 比如 test。cdn。system。me domainSuffix=。cdn。system。me 那麼擷取打字串test就是分配個商戶的名字 String tenantName = StringUtils。delete(serverName, domainSuffix); Tenant tenant = TenantMap。get(tenantName); if(null == tenant){ QueryWrapper query = new QueryWrapper<>(); query。eq(“name”, tenantName); tenant = tenantMapper。selectOne(query); if(null == tenant){ throw new RuntimeException(“找不到該商戶名 => ”+ tenantName); } TenantMap。put(tenant。getName(), tenant); return false; } TenantHolder。set(tenant); log。info(“get tenant => {}”, tenant); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { TenantHolder。remove(); }}

透過獲取request。getServerName()擷取字串來獲取租戶標識, 然後把當前的租戶放到ThreadLocal中

實現mybatis的攔截器, 在sql中的表名前增加schema

@Slf4j@Intercepts({ @Signature(type = Executor。class, method = “query”, args = {MappedStatement。class, Object。class, RowBounds。class, ResultHandler。class}), @Signature(type = Executor。class, method = “update”, args = {MappedStatement。class, Object。class})})public class TenantInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { MappedStatement ms = (MappedStatement)invocation。getArgs()[0]; Object parameterObject = null; if(invocation。getArgs()。length > 1){ parameterObject = invocation。getArgs()[1]; } BoundSql boundSql = ms。getBoundSql(parameterObject); BoundSql newBoundSql = new BoundSql( ms。getConfiguration(), replace(boundSql。getSql()), //sql替換 boundSql。getParameterMappings(), boundSql。getParameterObject() ); MappedStatement。Builder build = new MappedStatement。Builder( ms。getConfiguration(), ms。getId(), new CustomSqlSource(newBoundSql), ms。getSqlCommandType() ); build。resource(ms。getResource()); build。fetchSize(ms。getFetchSize()); build。statementType(ms。getStatementType()); build。keyGenerator(ms。getKeyGenerator()); build。timeout(ms。getTimeout()); build。parameterMap(ms。getParameterMap()); build。resultMaps(ms。getResultMaps()); build。cache(ms。getCache()); MappedStatement newStmt = build。build(); //替換原來的MappedStatement invocation。getArgs()[0] = newStmt; return invocation。proceed(); } private String replace(String sql) throws JSQLParserException { Statement stmt = CCJSqlParserUtil。parse(sql); Tenant tenant = TenantHolder。get(); if(null == tenant){ return sql; } String schemeName = String。format(“`%s`”, tenant。getDbName()); if(stmt instanceof Insert){ Insert insert = (Insert)stmt; return SQLParser。doInsert(insert, schemeName); }else if(stmt instanceof Update){ Update update = (Update) stmt; return SQLParser。doUpdate(update, schemeName); }else if(stmt instanceof Delete){ Delete delete = (Delete) stmt; return SQLParser。doDelete(delete, schemeName); }else if(stmt instanceof Select){ Select select = (Select)stmt; return SQLParser。doSelect(select, schemeName); } throw new RuntimeException(“非法SQL語句 不可能執行到這裡”); } public static class CustomSqlSource implements SqlSource{ private BoundSql boundSql; protected CustomSqlSource(BoundSql boundSql){ this。boundSql = boundSql; } @Override public BoundSql getBoundSql(Object o) { return boundSql; } }}

執行

執行 db。sql

執行

TenantApplication

透過訪問 http://a。cdn。system。me/users 就可以看到效果

SpringBoot單資料庫例項多Schema多租戶實現

程式碼倉庫

https://gitee。com/dengmin/mp_tenant