項目開發到一半,用戶忽然提出須要多個分公司共同使用,這種須要將系統設計成SaaS架構,將各個分公司的數據進行隔離。mysql
獨立數據庫git
每一個企業 獨立的物理數據庫,隔離性好,成本高。sql
共享數據庫、獨立schemashell
就是一臺物理機,多個邏輯數據庫,oracle叫作schema,mysql叫作database,每一個企業獨立的schema。數據庫
共享數據庫、數據庫表(本次採用):express
在表中添加「企業」或者「租戶」字段區分是哪一個企業的數據。操做的時候根據「租戶」字段去查詢相應的數據。緩存
優勢: 全部租戶使用同一數據庫,因此成本低廉。安全
缺點:隔離級別低,安全性低,須要在開發時加大對安全的開發量,數據備份和恢復最困難。mybatis
共享數據庫、數據庫表
的SaaS方案。改造時須要作如下工做:tenant_id
。用於關聯租戶信息表。tenant_id
和原始表id建立聯合主鍵。注意主鍵的順序,原表主鍵必須在左邊。tenant_id
字段的值,在刪改查中,都須要在where條件中以tenant_id
爲條件來操做某個租戶的數據。測試庫中有5張表,我下文使用sys_log
表進行測試。架構
sys_log
的建表語句爲:
CREATE TABLE `sys_log` (
`log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`type` TINYINT(1) DEFAULT NULL COMMENT '類型',
`content` VARCHAR(255) DEFAULT NULL COMMENT '內容',
`create_id` BIGINT(18) DEFAULT NULL COMMENT '建立人ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
`tenant_id` INT NOT NULL,
PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系統日誌'
複製代碼
租戶id(tenant_id)字段
的表。SELECT
table_name
FROM
INFORMATION_SCHEMA.TABLES
WHERE table_schema = 'my' -- my 是個人測試數據庫名稱
AND table_name NOT IN
(SELECT
t.table_name
FROM
(SELECT
table_name,
column_name
FROM
information_schema.columns
WHERE table_name IN
(SELECT
table_name
FROM
INFORMATION_SCHEMA.TABLES
WHERE table_schema = 'my')) t
WHERE t.column_name = 'tenant_id') ;
複製代碼
執行,找到兩個符合條件的表,在數據庫進行確認,確實表中沒tenant_id
字段。
僅供參考,用於保存租戶信息
CREATE TABLE `t_tenant` (
`tenant_id` varchar(40) NOT NULL DEFAULT 'c12dee54f652452b88142a0267ec74b7' COMMENT '租戶id',
`tenant_code` varchar(100) DEFAULT NULL COMMENT '租戶編碼',
`name` varchar(50) DEFAULT NULL COMMENT '租戶名稱',
`desc` varchar(500) DEFAULT NULL COMMENT '租戶描述',
`logo` varchar(255) DEFAULT NULL COMMENT '公司logo地址',
`status` smallint(6) DEFAULT NULL COMMENT '狀態1有效0無效',
`create_by` varchar(100) DEFAULT NULL COMMENT '建立者',
`create_time` datetime DEFAULT NULL COMMENT '建立時間',
`last_update_by` varchar(100) DEFAULT NULL COMMENT '最後修改人',
`last_update_time` datetime DEFAULT NULL COMMENT '最後修改時間',
`street_address` varchar(200) DEFAULT NULL COMMENT '街道樓號地址',
`province` varchar(20) DEFAULT NULL COMMENT '一級行政單位,如廣東省,上海市等',
`city` varchar(20) DEFAULT NULL COMMENT '城市, 如廣州市,佛山市等',
`district` varchar(20) DEFAULT NULL COMMENT '行政區,如番禺區,天河區等',
`link_man` varchar(50) DEFAULT NULL COMMENT '聯繫人',
`link_phone` varchar(50) DEFAULT NULL COMMENT '聯繫電話',
`longitude` decimal(10,6) DEFAULT NULL COMMENT '經度',
`latitude` decimal(10,6) DEFAULT NULL COMMENT '緯度',
`adcode` varchar(8) DEFAULT NULL COMMENT '區域編碼,用於經過區域id快速匹配後展現, 如廣州是440100',
PRIMARY KEY (`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='租戶的基本信息表';
複製代碼
tenant_id
字段DROP PROCEDURE IF EXISTS addColumn ;
DELIMITER $$
CREATE PROCEDURE addColumn ()
BEGIN
-- 定義表名變量
DECLARE s_tablename VARCHAR (100) ;
/*顯示錶的數據庫中的全部表
SELECT table_name FROM information_schema.tables WHERE table_schema='databasename' Order by table_name ;
*/
#顯示全部
DECLARE cur_table_structure CURSOR FOR
SELECT
table_name
FROM
INFORMATION_SCHEMA.TABLES
WHERE table_schema = 'my' -- my = 個人測試數據庫名稱
AND table_name NOT IN
(SELECT
t.table_name
FROM
(SELECT
table_name,
column_name
FROM
information_schema.columns
WHERE table_name IN
(SELECT
table_name
FROM
INFORMATION_SCHEMA.TABLES
WHERE table_schema = 'my')) t
WHERE t.column_name = 'tenant_id') ;
DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET s_tablename = NULL ;
OPEN cur_table_structure ;
FETCH cur_table_structure INTO s_tablename ;
WHILE
(s_tablename IS NOT NULL) DO SET @MyQuery = CONCAT(
"alter table `",
s_tablename,
"` add COLUMN `tenant_id` INT not null COMMENT '租戶id'"
) ;
PREPARE msql FROM @MyQuery ;
EXECUTE msql ;
#USING @c;
FETCH cur_table_structure INTO s_tablename ;
END WHILE ;
CLOSE cur_table_structure ;
END $$
DELIMITER ;
#執行存儲過程
CALL addColumn () ;
複製代碼
實現的目標:在添加租戶的時候實現對全部表添加分區
須要的條件:
tenant_id
必須和原表log_id
主鍵組成聯合主鍵。表中添加分區有三種方式:
sys_log_copy
,copy數據過來後,刪除舊的sys_log
,再將sys_log_copy
修改成sys_log
(本次採用,詳見下文)-- 若是表中沒數據,能夠直接將表進行分區
ALTER TABLE sys_log PARTITION BY LIST COLUMNS (tenant_id)
(
PARTITION a1 VALUES IN (1) ENGINE = INNODB,
PARTITION a2 VALUES IN (2) ENGINE = INNODB,
PARTITION a3 VALUES IN (3) ENGINE = INNODB
);
複製代碼
-- 已是分區表中添加分區
ALTER TABLE sys_log_copy ADD PARTITION
(
PARTITION a4 VALUES IN (4) ENGINE = INNODB,
PARTITION a5 VALUES IN (5) ENGINE = INNODB,
PARTITION a6 VALUES IN (6) ENGINE = INNODB
);
複製代碼
建立臨時分區表
的方式將原錶轉換成分區表SHOW CREATE TABLE `sys_log`;
複製代碼
CREATE TABLE `sys_log_copy` (
`log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`type` TINYINT(1) DEFAULT NULL COMMENT '類型',
`content` VARCHAR(255) DEFAULT NULL COMMENT '內容',
`create_id` BIGINT(18) DEFAULT NULL COMMENT '建立人ID',
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
`tenant_id` INT NOT NULL,
PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系統日誌'
PARTITION BY LIST COLUMNS (tenant_id)
(
PARTITION a1 VALUES IN (1) ENGINE = INNODB,
PARTITION a2 VALUES IN (2) ENGINE = INNODB,
PARTITION a3 VALUES IN (3) ENGINE = INNODB
);
複製代碼
注意上文中的
DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC
CHARSET=utf8mb4
是由於utf8在mysql中是不健全的編碼。ROW_FORMAT=DYNAMIC
是爲了不因此長度過大後致使以下報錯:ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes. 複製代碼
也能夠在my.ini配置文件中設置爲true解決這個問題,可是要重啓數據庫,會比較麻煩。
[mysqld] innodb_large_prefix=true 複製代碼
SELECT
partition_name part,
partition_expression expr,
partition_description descr,
table_rows
FROM
information_schema.partitions
WHERE TABLE_SCHEMA = SCHEMA()
AND TABLE_NAME = 'sys_log_copy' ;
複製代碼
能夠查看到添加的3個分區
INSERT INTO `sys_log_copy` SELECT * FROM `sys_log`
複製代碼
sys_log
,再修改sys_log_copy
表中的名字爲sys_log
。經過存儲過程實現,在分區表中添加分區
DELIMITER $$
USE `my`$$
DROP PROCEDURE IF EXISTS `add_table_partition`$$
CREATE DEFINER=`root`@`%` PROCEDURE `add_table_partition`(IN _tenantId INT)
BEGIN
DECLARE IS_FOUND INT DEFAULT 1 ;
-- 用於記錄遊標中存在分區的表名
DECLARE v_tablename VARCHAR (200) ;
-- 用於緩存添加分區時候的sql
DECLARE v_sql VARCHAR (5000) ;
-- 分區名稱定義
DECLARE V_P_VALUE VARCHAR (100) DEFAULT CONCAT('P', REPLACE(_tenantId, '-', '')) ;
DECLARE V_COUNT INT ;
DECLARE V_LOONUM INT DEFAULT 0 ;
DECLARE V_NUM INT DEFAULT 0 ;
-- 定義遊標,值是全部分區表的表名
DECLARE curr CURSOR FOR
(SELECT
t.TABLE_NAME
FROM
INFORMATION_SCHEMA.partitions t
WHERE TABLE_SCHEMA = SCHEMA()
AND t.partition_name IS NOT NULL
GROUP BY t.TABLE_NAME) ;
-- 若是沒影響的記錄,程序也繼續執行
DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0;
-- 獲取上一步中的遊標中獲取到的表名的個數
SELECT
COUNT(1) INTO V_LOONUM
FROM
(SELECT
t.TABLE_NAME
FROM
INFORMATION_SCHEMA.partitions t
WHERE TABLE_SCHEMA = SCHEMA()
AND t.partition_name IS NOT NULL
GROUP BY t.TABLE_NAME) A ;
-- 只有在存在分區表的時候纔打開遊標
IF V_LOONUM > 0
THEN -- 打開遊標
OPEN curr ;
-- 循環
read_loop :
LOOP
-- 聲明結束的時候
IF V_NUM >= V_LOONUM
THEN LEAVE read_loop ;
END IF ;
-- 取遊標的值給變量
FETCH curr INTO v_tablename ;
-- 依次判斷分區表是否存在改分區,若是不存在則添加分區
SET V_NUM = V_NUM + 1 ;
SELECT
COUNT(1) INTO V_COUNT
FROM
INFORMATION_SCHEMA.partitions t
WHERE LOWER(T.TABLE_NAME) = LOWER(v_tablename)
AND T.PARTITION_NAME = V_P_VALUE
AND T.TABLE_SCHEMA = SCHEMA() ;
IF V_COUNT <= 0
THEN SET v_sql = CONCAT(
' ALTER TABLE ',
v_tablename,
' ADD PARTITION (PARTITION ',
V_P_VALUE,
' VALUES IN(',
_tenantId,
') ENGINE = INNODB) '
) ;
SET @v_sql = v_sql ;
-- 預處理須要執行的動態SQL,其中stmt是一個變量
PREPARE stmt FROM @v_sql ;
-- 執行SQL語句
EXECUTE stmt ;
-- 釋放掉預處理段
DEALLOCATE PREPARE stmt ;
END IF ;
-- 結束循環
END LOOP read_loop;
-- 關閉遊標
CLOSE curr ;
END IF ;
END$$
DELIMITER ;
複製代碼
調用存儲過程測試
CALL add_table_partition (8) ;
複製代碼
- 若是表還不是分區表,那麼調用存儲過程會有以下報錯:
錯誤代碼: 1505 Partition management on a not partitioned table is not possible 複製代碼
翻譯出來的意思是:「在未分區的表上進行分區管理是不可能的」。
- 可能會報錯以下:
錯誤代碼: 1329 No data - zero rows fetched, selected, or processed 複製代碼
但若是經過查詢下面的
information_schema.partitions
無誤,那就是添加分區成功。能夠經過在定義遊標後,打開遊標以前,添加以下方式解決:
DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0; 複製代碼
SELECT
partition_name part,
partition_expression expr,
partition_description descr,
table_rows
FROM
information_schema.partitions
WHERE TABLE_SCHEMA = SCHEMA()
AND TABLE_NAME = 'sys_log' ;
複製代碼
mybatis
調用存儲過程<select id="testProcedure" statementType="CALLABLE" useCache="false" parameterType="string">
<![CDATA[
call add_table_partition (
#{_tenantId,mode=IN,jdbcType=VARCHAR});
]]>
</select>
複製代碼