MyCat權威指南閱讀筆記(基礎篇)

 1.1何爲數據切分?


簡單來說,就是指通過某種特定的條件,將我們存放在同一個數據庫中的數據分散存放到多個數據庫(主
機)上面,以達到分散單臺設備負載的效果。
數據的切分(Sharding)根據其切分規則的類型,可以分爲兩種切分模式。一種是按照不同的表(或者
Schema)來切分到不同的數據庫(主機)之上,這種切可以稱之爲數據的垂直(縱向)切分;另外一種則是根據
表中的數據的邏輯關係,將同一個表中的數據按照某種條件拆分到多臺數據庫(主機)上面,這種切分稱之爲數
據的水平(橫向)切分。
垂直切分的最大特點就是規則簡單,實施也更爲方便,尤其適合各業務之間的耦合度非常低,相互影響很
小,業務邏輯非常清晰的系統。在這種系統中,可以很容易做到將不同業務模塊所使用的表分拆到不同的數據庫
中。根據不同的表來進行拆分,對應用程序的影響也更小,拆分規則也會比較簡單清晰。
水平切分於垂直切分相比,相對來說稍微複雜一些。因爲要將同一個表中的不同數據拆分到不同的數據庫
中,對於應用程序來說,拆分規則本身就較根據表名來拆分更爲複雜,後期的數據維護也會更爲複雜一些。

1.2垂直切分


一個數據庫由很多表的構成,每個表對應着不同的業務,垂直切分是指按照業務將表進行分類,分佈到不同
的數據庫上面,這樣也就將數據或者說壓力分擔到不同的庫上面,如下圖:


系統被切分成了,用戶,訂單交易,支付幾個模塊。
一個架構設計較好的應用系統,其總體功能肯定是由很多個功能模塊所組成的,而每一個功能模塊所需要的
數據對應到數據庫中就是一個或者多個表。而在架構設計中,各個功能模塊相互之間的交互點越統一越少,系統
的耦合度就越低,系統各個模塊的維護性以及擴展性也就越好。這樣的系統,實現數據的垂直切分也就越容易。
但是往往系統之有些表難以做到完全的獨立,存在這擴庫 join 的情況,對於這類的表,就需要去做平
衡,是數據庫讓步業務,共用一個數據源,還是分成多個庫,業務之間通過接口來做調用。在系統初期,數據量
比較少,或者資源有限的情況下,會選擇共用數據源,但是當數據發展到了一定的規模,負載很大的情況,就需
要必須去做分割。
一般來講業務存在着複雜 join 的場景是難以切分的,往往業務獨立的易於切分。如何切分,切分到何種
程度是考驗技術架構的一個難題。
下面來分析下垂直切分的優缺點:
優點:
 拆分後業務清晰,拆分規則明確;
 系統之間整合或擴展容易;
 數據維護簡單。
缺點:
 部分業務表無法 join,只能通過接口方式解決,提高了系統複雜度;
 受每種業務不同的限制存在單庫性能瓶頸,不易數據擴展跟性能提高;
 事務處理複雜。
由於垂直切分是按照業務的分類將表分散到不同的庫,所以有些業務表會過於龐大,存在單庫讀寫與存儲瓶
頸,所以就需要水平拆分來做解決。 

1.3水平切分


相對於垂直拆分,水平拆分不是將表做分類,而是按照某個字段的某種規則來分散到多個庫之中,每個表中
包含一部分數據。簡單來說,我們可以將數據的水平切分理解爲是按照數據行的切分,就是將表中的某些行切分
到一個數據庫,而另外的某些行又切分到其他的數據庫中,如圖:

拆分數據就需要定義分片規則。關係型數據庫是行列的二維模型,拆分的第一原則是找到拆分維度。比如:
從會員的角度來分析,商戶訂單交易類系統中查詢會員某天某月某個訂單,那麼就需要按照會員結合日期來拆
分,不同的數據按照會員 ID 做分組,這樣所有的數據查詢 join 都會在單庫內解決;如果從商戶的角度來講,要查
詢某個商家某天所有的訂單數,就需要按照商戶 ID 做拆分;但是如果系統既想按會員拆分,又想按商家數據,則
會有一定的困難。如何找到合適的分片規則需要綜合考慮衡量。
幾種典型的分片規則包括:
 按照用戶 ID 求模,將數據分散到不同的數據庫,具有相同數據用戶的數據都被分散到一個庫中;
 按照日期,將不同月甚至日的數據分散到不同的庫中;
 按照某個特定的字段求摸,或者根據特定範圍段分散到不同的庫中。
如圖,切分原則都是根據業務找到適合的切分規則分散到不同的庫,下面用用戶 ID 求模舉例:

既然數據做了拆分有優點也就優缺點。
優點:
 拆分規則抽象好,join 操作基本可以數據庫做;
 不存在單庫大數據,高併發的性能瓶頸;
 應用端改造較少;
 提高了系統的穩定性跟負載能力。
缺點:
20
 拆分規則難以抽象;
 分片事務一致性難以解決;
 數據多次擴展難度跟維護量極大;
 跨庫 join 性能較差。
前面講了垂直切分跟水平切分的不同跟優缺點,會發現每種切分方式都有缺點,但共同的特點缺點有:
 引入分佈式事務的問題;
 跨節點 Join 的問題;
 跨節點合併排序分頁問題;
 多數據源管理問題。
針對數據源管理,目前主要有兩種思路:
A. 客戶端模式,在每個應用程序模塊中配置管理自己需要的一個(或者多個)數據源,直接訪問各個數據
庫,在模塊內完成數據的整合;
B. 通過中間代理層來統一管理所有的數據源,後端數據庫集羣對前端應用程序透明;
可能 90%以上的人在面對上面這兩種解決思路的時候都會傾向於選擇第二種,尤其是系統不斷變得龐大複雜
的時候。確實,這是一個非常正確的選擇,雖然短期內需要付出的成本可能會相對更大一些,但是對整個系統的
擴展性來說,是非常有幫助的。
Mycat 通過數據切分解決傳統數據庫的缺陷,又有了 NoSQL 易於擴展的優點。通過中間代理層規避了多數
據源的處理問題,對應用完全透明,同時對數據切分後存在的問題,也做了解決方案。下面章節就分析,mycat
的由來及如何進行數據切分問題。
由於數據切分後數據 Join 的難度在此也分享一下數據切分的經驗:
第一原則:能不切分儘量不要切分。
第二原則:如果要切分一定要選擇合適的切分規則,提前規劃好。
第三原則:數據切分儘量通過數據冗餘或表分組(Table Group)來降低跨庫 Join 的可能。
第四原則:由於數據庫中間件對數據 Join 實現的優劣難以把握,而且實現高性能難度極大,業務讀取儘量
少使用多表 Join。

 

2.1Mycat 原理


Mycat 的原理並不複雜,複雜的是代碼,如果代碼也不復雜,那麼早就成爲一個傳說了。
Mycat 的原理中最重要的一個動詞是“攔截”,它攔截了用戶發送過來的 SQL 語句,首先對 SQL 語句做了
一些特定的分析:如分片分析、路由分析、讀寫分離分析、緩存分析等,然後將此 SQL 發往後端的真實數據庫,
並將返回的結果做適當的處理,最終再返回給用戶。

上述圖片裏,Orders 表被分爲三個分片 datanode(簡稱 dn),這三個分片是分佈在兩臺 MySQL Server 上
(DataHost),即 datanode=database@datahost 方式,因此你可以用一臺到 N 臺服務器來分片,分片規則爲
(sharding rule)典型的字符串枚舉分片規則,一個規則的定義是分片字段(sharding column)+分片函數(rule
function),這裏的分片字段爲 prov 而分片函數爲字符串枚舉方式。
當 Mycat 收到一個 SQL 時,會先解析這個 SQL,查找涉及到的表,然後看此表的定義,如果有分片規則,
則獲取到 SQL 裏分片字段的值,並匹配分片函數,得到該 SQL 對應的分片列表,然後將 SQL 發往這些分片去執
行,最後收集和處理所有分片返回的結果數據,並輸出到客戶端。以 select * from Orders where prov=?語句爲
例,查到 prov=wuhan,按照分片函數,wuhan 返回 dn1,於是 SQL 就發給了 MySQL1,去取 DB1 上的查詢
結果,並返回給用戶。
如果上述 SQL 改爲 select * from Orders where prov in (‘wuhan’,‘beijing’),那麼,SQL 就會發給
MySQL1 與 MySQL2 去執行,然後結果集合並後輸出給用戶。但通常業務中我們的 SQL 會有 Order By 以及
Limit 翻頁語法,此時就涉及到結果集在 Mycat 端的二次處理,這部分的代碼也比較複雜,而最複雜的則屬兩個
表的 Jion 問題,爲此,Mycat 提出了創新性的 ER 分片、全局表、HBT(Human Brain Tech)人工智能的
Catlet、以及結合 Storm/Spark 引擎等十八般武藝的解決辦法,從而成爲目前業界最強大的方案,這就是開源的
力量! 

2.2應用場景


Mycat 發展到現在,適用的場景已經很豐富,而且不斷有新用戶給出新的創新性的方案,以下是幾個典型的
應用場景:
 單純的讀寫分離,此時配置最爲簡單,支持讀寫分離,主從切換;
 分表分庫,對於超過 1000 萬的表進行分片,最大支持 1000 億的單表分片;
 多租戶應用,每個應用一個庫,但應用程序只連接 Mycat,從而不改造程序本身,實現多租戶化;
 報表系統,藉助於 Mycat 的分表能力,處理大規模報表的統計;
 替代 Hbase,分析大數據;
 作爲海量數據實時查詢的一種簡單有效方案,比如 100 億條頻繁查詢的記錄需要在 3 秒內查詢出來結果,
除了基於主鍵的查詢,還可能存在範圍查詢或其他屬性查詢,此時 Mycat 可能是最簡單有效的選擇。

 

3.1 linux 服務安裝與配置


MyCAT 有提供編譯好的安裝包,支持 windows、Linux、Mac、Solaris 等系統上安裝與運行。

linux 下可以下載 Mycat-server-xxxxx.linux.tar.gz 解壓在某個目錄下,注意目錄不能有空格,在
Linux(Unix)下,建議放在 usr/local/Mycat 目錄下,如下:


下面是修改 MyCAT 用戶密碼的方式(僅供參考):


目錄解釋如下:
bin 程序目錄,存放了 window 版本和 linux 版本,除了提供封裝成服務的版本之外,也提供了 nowrap 的
shell 腳本命令,方便大家選擇和修改,進入到 bin 目錄:
Linux 下運行:./mycat console,首先要 chmod +x *
注:mycat 支持的命令{ console | start | stop | restart | status | dump }
conf 目錄下存放配置文件,server.xml 是 Mycat 服務器參數調整和用戶授權的配置文件,schema.xml 是邏
輯庫定義和表以及分片定義的配置文件,rule.xml 是分片規則的配置文件,分片規則的具體一些參數信息單獨存
放爲文件,也在這個目錄下,配置文件修改,需要重啓 Mycat 或者通過 9066 端口 reload.
lib 目錄下主要存放 mycat 依賴的一些 jar 文件.
日誌存放在 logs/mycat.log 中,每天一個文件,日誌的配置是在 conf/log4j.xml 中,根據自己的需要,可
以調整輸出級別爲 debug,debug 級別下,會輸出更多的信息,方便排查問題.

注意:Linux 下部署安裝 MySQL,默認不忽略表名大小寫,需要手動到/etc/my.cnf 下配置
lower_case_table_names=1 使 Linux 環境下 MySQL 忽略表名大小寫,否則使用 MyCAT 的時候會提示找不到
表的錯誤!

3.2 windows 下 服務安裝與配置

MyCAT 有提供編譯好的安裝包,支持 windows、Linux、Mac、Solaris 等系統上安裝與運行。
windows 下可以下載 Mycat-server-xxxxx-win.tar.gz 解壓在某個目錄下,建議解壓到本地某個盤符根目錄
下,如下:


目錄解釋如下:
bin 程序目錄,存放了 window 版本和 linux 版本,除了提供封裝成服務的版本之外,也提供了 nowrap 的
shell 腳本命令,方便大家選擇和修改,進入到 bin 目錄:
Windows 下運行:運行: mycat.bat 在控制檯啓動程序,也可以裝載成服務,若此程序運行有問題,也可以
運行 startup_nowrap.bat,確保 java 命令可以在命令執行。
Windows 下將 MyCAT 做成系統服務:MyCAT 提供 warp 方式的命令,可以將 MyCAT 安裝成系統服務並
可啓動和停止。
1) 進入 bin 目錄下, 輸入 ./mycat start 啓動 mycat 服務。
conf 目錄下存放配置文件,server.xml 是 Mycat 服務器參數調整和用戶授權的配置文件,schema.xml 是邏
輯庫定義和表以及分片定義的配置文件,rule.xml 是分片規則的配置文件,分片規則的具體一些參數信息單獨存
放爲文件,也在這個目錄下,配置文件修改,需要重啓 Mycat 或者通過 9066 端口 reload。
lib 目錄下主要存放 mycat 依賴的一些 jar 文件。

日誌存放在 logs/mycat.log 中,每天一個文件,日誌的配置是在 conf/log4j.xml 中,根據自己的需要,可
以調整輸出級別爲 debug,debug 級別下,會輸出更多的信息,方便排查問題。

 3.3服務啓動與啓動設置

3.3.1 linux 下啓動


MyCAT 在 Linux 中部署啓動時,首先需要在 Linux 系統的環境變量中配置 MYCAT_HOME,操作方式如下:
1) vi /etc/profile,在系統環境變量文件中增加 MYCAT_HOME=/usr/local/Mycat。
2) 執行 source /etc/profile 命令,使環境變量生效。
如果是在多臺 Linux 系統中組建的 MyCAT 集羣,那需要在 MyCAT Server 所在的服務器上配置對其他 ip 和
主機名的映射,配置方式如下:
vi /etc/hosts
例如:我有 4 臺機器,配置如下:
IP 主機名:
192.168.100.2 sam_server_1
192.168.100.3 sam_server_2
192.168.100.4 sam_server_3
192.168.100.5 sam_server_4
編輯完後,保存文件。
經過以上兩個步驟的配置,就可以到/usr/local/Mycat/bin 目錄下執行:
./mycat start
即可啓動 mycat 服務!


3.3.2 windows下啓動


MyCAT 在 windows 中部署時,建議放在某個盤符的根目錄下,如果不是在根目錄下,請儘量不要放在包含
中文的目錄下
如:D:\Mycat-server-1.4-win\
命令行方式啓動:
從 cmd 中執行命令到達 D:\Mycat-server-1.4-win\bin 目錄下,執行 startup_nowrap.bat 即可啓動
MyCAT 服務。
45
注:執行此命令時,需要確保 windows 系統中已經配置好了 JAVA 的環境變量,並可執行 java 命令。jdk 版
本必須是 1.7 及以上版本。

3.3.3基於 zk 的啓動


1.5 開始會支持本地 xml 啓動,以及從 zk 加載配置轉爲本地 xml 的兩種方式,conf 下的 zk.conf 文件裏設置
loadfromzk 參數默認爲 false
如果沒有這個文件,或者沒有 loadfromzk 爲 true 的參數,即從本地加載。下面介紹從 ZK 啓動相關配置。
Zk-create.yaml 說明
1.5 正式引入 zookeeper(以下簡稱 zk)管理 Mycat-Server,啓動 server 第一步是初始化 zk 數據,下面介紹初
始化 zk 數據步驟,信息在 zk-create.yaml。Mycat ZK 配置文件詳解:
https://github.com/MyCATApache/Mycat-
doc/blob/master/%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3/2.0/Mycat%20ZK%E9%85%
8D%E7%BD%AE%E6%96%87%E4%BB%B6%E8%AF%A6%E8%A7%A3.docx
1、zk-create 總體結構

2、參數說明
2.1、zkURL,zk 連接地址
2.2、mycat-cluster

 

 

 

 

 

 

 

 

 

2.7 mysql-reps 

 

Zk 初始化
1、進入 MYCAT/bin 目錄
cd /data/test1/mycat/bin

 

2、修改 MYCAT/conf/zk-create.yaml 內容
修改方法見“Zk-create.yaml 說明”。
3、啓動 ZK

啓動 ZK: bin/zkServer.sh start 

 登陸 ZK: bin/zkCli.sh

4、初始化 ZK 數據
sh create_zookeeper_data.sh
等待執行結束後,檢查 ZK 數據
5、檢查 ZK 數據

OK,數據初始化成功。

 

4.1 Mycat 的分片 join 

8.1 join 概述
Join 絕對是關係型數據庫中最常用一個特性,然而在分佈式環境中,跨分片的 join 確是最複雜的,最難解決一
個問題。
下面我們簡單介紹下各種 Join 操作。
INNER JOIN
內連接,也叫等值連接,inner join 產生同時符合 A 表和 B 表的一組數據。
如圖:

LEFT JOIN
左連接從 A 表(左)產生一套完整的記錄,與匹配的 B 表記錄(右表) .如果沒有匹配,右側將包含 null,在 Mysql 中
等同於 left outer join。
如圖:

RIGHT JOIN
同 Left join,AB 表互換即可。
Cross join
交叉連接,得到的結果是兩個表的乘積,即笛卡爾積。笛卡爾(Descartes)乘積又叫直積。假設集合
A={a,b},集合 B={0,1,2},則兩個集合的笛卡爾積爲{(a,0),(a,1),(a,2),(b,0),(b,1), (b,2)}。可以擴展到多個集合的情
況。類似的例子有,如果 A 表示某學校學生的集合,B 表示該學校所有課程的集合,則 A 與 B 的笛卡爾積表示所
有可能的選課情況。
Full join
全連接產生的所有記錄(雙方匹配記錄)在表 A 和表 B。如果沒有匹配,則對面將包含 null。

性能建議
儘量避免使用 Left join 或 Right join,而用 Inner join
在使用 Left join 或 Right join 時,ON 會優先執行,where 條件在最後執行,所以在使用過程中,條件盡
可能的在 ON 語句中判斷,減少 where 的執行
少用子查詢,而用 join。
Mycat 目前版本支持跨分片的 join,主要實現的方式有四種。
全局表,ER 分片,catletT(人工智能)和 ShareJoin,ShareJoin 在開發版中支持,前面三種方式 1.3.0.1 支
持 。

4.1.1 全局表


一個真實的業務系統中,往往存在大量的類似字典表的表格,它們與業務表之間可能有關係,這種關係,可
以理解爲“標籤”,而不應理解爲通常的“主從關係”,這些表基本上很少變動,可以根據主鍵 ID 進行緩存,下
面這張圖說明了一個典型的“標籤關係”圖:


在分片的情況下,當業務表因爲規模而進行分片以後,業務表與這些附屬的字典表之間的關聯,就成了比較
棘手的問題,考慮到字典表具有以下幾個特性:
• 變動不頻繁
• 數據量總體變化不大
• 數據規模不大,很少有超過數十萬條記錄。
鑑於此,MyCAT 定義了一種特殊的表,稱之爲“全局表”,全局表具有以下特性:
• 全局表的插入、更新操作會實時在所有節點上執行,保持各個分片的數據一致性
• 全局表的查詢操作,只從一個節點獲取
• 全局表可以跟任何一個表進行 JOIN 操作
將字典表或者符合字典表特性的一些表定義爲全局表,則從另外一個方面,很好的解決了數據 JOIN 的難題。
通過全局表+基於 E-R 關係的分片策略,MyCAT 可以滿足 80%以上的企業應用開發。

配置
全局表配置比較簡單,不用寫 Rule 規則,如下配置即可:
<table name="company" primaryKey="ID" type="global" dataNode="dn1,dn2,dn3" />

需要注意的是,全局表每個分片節點上都要有運行創建表的 DDL 語句。 

 

4.1.2 ER Join

MyCAT 借鑑了 NewSQL 領域的新秀 Foundation DB 的設計思路,Foundation DB 創新性的提出了 Table
Group 的概念,其將子表的存儲位置依賴於主表,並且物理上緊鄰存放,因此徹底解決了 JION 的效率和性能問
題,根據這一思路,提出了基於 E-R 關係的數據分片策略,子表的記錄與所關聯的父表記錄存放在同一個數據分
片上。
customer 採用 sharding-by-intfile 這個分片策略,分片在 dn1,dn2 上,orders 依賴父表進行分片,兩個表
的關聯關係爲 orders.customer_id=customer.id。於是數據分片和存儲的示意圖如下:

 

這樣一來,分片 Dn1 上的的 customer 與 Dn1 上的 orders 就可以進行局部的 JOIN 聯合,Dn2 上也如此,再合
並兩個節點的數據即可完成整體的 JOIN,試想一下,每個分片上 orders 表有 100 萬條,則 10 個分片就有 1 個億,基
於 E-R 映射的數據分片模式,基本上解決了 80%以上的企業應用所面臨的問題。

配置
以上述例子爲例,schema.xml 中定義如下的分片配置:
<table name="customer" dataNode="dn1,dn2" rule="sharding-by-intfile">
<childTable name="orders" joinKey="customer_id" parentKey="id"/>
</table> 

 

4.1.3 Share join

ShareJoin 是一個簡單的跨分片 Join,基於 HBT 的方式實現。
目前支持 2 個表的 join,原理就是解析 SQL 語句,拆分成單表的 SQL 語句執行,然後把各個節點的數據匯
集。
配置
支持任意配置的 A,B 表如:
A,B 的 dataNode 相同
<table name="A" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
<table name="B" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
A,B 的 dataNode 不同
<table name="A" dataNode="dn1,dn2 " rule="auto-sharding-long" />
<table name="B" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />

<table name="A" dataNode="dn1 " rule="auto-sharding-long" />
<table name="B" dataNode=" dn2,dn3" rule="auto-sharding-long" />
代碼測試
先把表 company 從全局表修改下配置
<table name="company" primaryKey="ID" dataNode="dn1,dn2,dn3" rule="mod-long" />
重新插入數據
mysql> delete from company;
Query OK, 9 rows affected (0.19 sec)
mysql> insert company (id,name) values(1,'mycat');
Query OK, 1 row affected (0.08 sec)
mysql> insert company (id,name) values(2,'ibm');
Query OK, 1 row affected (0.03 sec)
101
mysql> insert company (id,name) values(3,'hp');
Query OK, 1 row affected (0.03 sec)
下面可以看下普通的 join 和 sharejoin 的區別
mysql> select a.*,b.id, b.name as tit from customer a,company b where a.company_id=b.id;
+----+------+------------+-------------+----+------+
| id | name | company_id | sharding_id | id | tit |
+----+------+------------+-------------+----+------+
| 3 | feng | 3 | 10000 | 3 | hp |
+----+------+------------+-------------+----+------+
1 row in set (0.03 sec)


mysql> /*!mycat:catlet=demo.catlets.ShareJoin */ select a.*,b.id, b.name as tit from customer a,company b
on a.company_id=b.id;
+----+------+------------+-------------+----+-------+
| id | name | company_id | sharding_id | id | tit |
+----+------+------------+-------------+----+-------+
| 3 | feng | 3 | 10000 | 3 | hp |
| 1 | wang | 1 | 10000 | 1 | mycat |
| 2 | xue | 2 | 10010 | 2 | ibm |
+----+------+------------+-------------+----+-------+
3 rows in set (0.05 sec)


其他兩種寫法
/*!mycat:catlet=demo.catlets.ShareJoin */ select a.*,b.id, b.name as tit from customer a join company b on
a.company_id=b.id;
+----+------+------------+-------------+----+-------+
| id | name | company_id | sharding_id | id | tit |
+----+------+------------+-------------+----+-------+
| 3 | feng | 3 | 10000 | 3 | hp |
| 1 | wang | 1 | 10000 | 1 | mycat |
102
| 2 | xue | 2 | 10010 | 2 | ibm |
+----+------+------------+-------------+----+-------+
3 rows in set (0.01 sec)


/*!mycat:catlet=demo.catlets.ShareJoin */ select a.*,b.id, b.name as tit from customer a join company b where
a.company_id=b.id;
+----+------+------------+-------------+----+-------+
| id | name | company_id | sharding_id | id | tit |
+----+------+------------+-------------+----+-------+
| 3 | feng | 3 | 10000 | 3 | hp |
| 1 | wang | 1 | 10000 | 1 | mycat |
| 2 | xue | 2 | 10010 | 2 | ibm |
+----+------+------------+-------------+----+-------+
3 rows in set (0.01 sec)


對*的支持,還可以這樣寫 SQL
mysql> /*!mycat:catlet=demo.catlets.ShareJoin */ select a.*,b.* from customer a join company b on
a.company_id=b.id;
+----+------+------------+-------------+-------+
| id | name | company_id | sharding_id | name |
+----+------+------------+-------------+-------+
| 1 | wang | 1 | 10000 | mycat |
| 2 | xue | 2 | 10010 | ibm |
| 3 | feng | 3 | 10000 | hp |
+----+------+------------+-------------+-------+
3 rows in set (0.02 sec)


mysql> /*!mycat:catlet=demo.catlets.ShareJoin */ select * from customer a join company b on
a.company_id=b.id;
+----+------+------------+-------------+-------+
103
| id | name | company_id | sharding_id | name |
+----+------+------------+-------------+-------+
| 1 | wang | 1 | 10000 | mycat |
| 2 | xue | 2 | 10010 | ibm |
| 3 | feng | 3 | 10000 | hp |
+----+------+------------+-------------+-------+
3 rows in set (0.02 sec)


/*!mycat:catlet=demo.catlets.ShareJoin */ select a.id,a.user_id,a.traveldate,a.fee,a.days,b.id as nnid, b.title as
tit from travelrecord a join hotnews b on b.id=a.days order by a.id

4.1.4 catlet(人工智能)

解決跨分片的 SQL JOIN 的問題,遠比想象的複雜,而且往往無法實現高效的處理,既然如此,就依靠人工
的智力,去編程解決業務系統中特定幾個必須跨分片的 SQL 的 JOIN 邏輯,MyCAT 提供特定的 API 供程序員調
用,這就是 MyCAT 創新性的思路——人工智能。
以一個跨節點的 SQL 爲例。
Select a.id,a.name,b.title from a,b where a.id=b.id
其中 a 在分片 1,2,3 上,b 在 4,5,6 上,需要把數據全部拉到本地(MyCAT 服務器),執行 JOIN 邏
輯,具體過程如下(只是一種可能的執行邏輯):
EngineCtx ctx=new EngineCtx();//包含 MyCat.SQLEngine
String sql=,“select a.id ,a.name from a ”;
//在 a 表所在的所有分片上順序執行下面的本地 SQL
ctx.executeNativeSQLSequnceJob(allAnodes,new DirectDBJoinHandler());
DirectDBJoinHandler 類是一個回調類,負責處理 SQL 執行過程中返回的數據包,這裏的這個類,主要目的
是用 a 表返回的 ID 信息,去 b 表上查詢對於的記錄,做實時的關聯:
DirectDBJoinHandler{
Private HashMap<byte[],byte[]> rows;//Key 爲 id,value 爲一行記錄的 Column 原始 Byte 數組,這裏是
a.id,a.name,b.title 這三個要輸出的字段
104
Public Boolean onHeader(byte[] header)
{
//保存 Header 信息,用於從 Row 中獲取 Field 字段值
}
Public Boolean onRowData(byte[] rowData)
{
String id=getColumnAsString(“id”);
//放入結果集,b.title 字段未知,所以先空着
rows.put(getColumnRawBytes(“id”),rowData);
//滿 1000 條,發送一個查詢請求
String sql=”select b.id, b.name from b where id in (………….)”;
//此 SQL 在 B 的所有節點上併發執行,返回的結果直接輸出到客戶端
ctx.executeNativeSQLParallJob(allBNodes,sql ,new MyRowOutPutDataHandler(rows));
}
Public Boolean onRowFinished()
{
}
Public void onJobFinished()
{
If(ctx.allJobFinished())
{///used total time ….
}
}
}
最後,增加一個 Job 事件監聽器,這裏是所有 Job 完成後,往客戶端發送 RowEnd 包,結束整個流程。
ctx.setJobEventListener(new JobEventHandler(){public void onJobFinished(){ client.writeRowEndPackage()}});
以上提供一個 SQL 執行框架,完全是異步的模式執行,並且以後會提供更多高質量的 API,簡化分佈式數據
處理,比如內存結合文件的數據 JOIN 算法,分組算法,排序算法等等,期待更多的牛人一起來完善。

 

4.1.5 Spark/Storm 對 join 擴展 

看到這個標題,可能會感到很奇怪,Spark 和 Storm 和 Join 有關係嗎? 有必要用 Spark,storm 嗎?
mycat 後續的功能會引入 spark 和 storm 來做跨分片的 join,大致流程是這樣的在 mycat 調用 spark,storm
的 api,把數據傳送到 spark,storm,在 spark,storm 進行 join,在把數據傳回 mycat,mycat 在返回給客戶端。

 

4.1.6 兩個表標準 Join 的支持 

兩個表標準 Join 主要包括八方面內容:
 兩個分片規則,分片都相同的表之間 JOIN
 一個分片表和一個全局表之間 JOIN
 兩表 JOIN 作爲子表
待完善
 兩個分片規則相同,分片不同的表之間 JOIN
 兩個分片規則不同的表之間 JOIN
 一個分片表和一個本地表之間 JOIN
 一個本地表和一個全局表之間 JOIN
 分佈式 JOIN 算法的設計開發
1. 測試計劃

1) 在 DB 服務器上新建四個庫,庫名爲 htdb,htdb1,htdb2,htdb3。
2) Htdb 中存放非分片表數據。
3) Htdb1,htdb2,htdb3 三個庫用於存放分片表數據。
2. 測試實施
兩個分片規則,分片都相同的表之間 JOIN
 配置分片規則
<table name="company" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" rule="rang-long" />
<table name="address" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" rule="rang-long" />


 準備測試數據
create table company(id int primary key, companyname varchar(20), addressid int);
create table address(id int primary key, addressname varchar(20));


insert into company(id,companyname,addressid) values(1, 'Intel', 1),(2, 'IBM', 2),(3, 'Dell',
3),(5000001, 'Sony', 5000001),(5000002, 'Apple', 5000002),(10000001, 'Microsoft',
10000001),(10000002, 'Oracle', 10000002);


insert into address(id,addressname) values(1, 'USA_1'),(2, 'USA_2'),(3, 'USA_3'),(5000001,
'USA_4'),(5000002, 'USA_5'),(10000001, 'USA_6'),(10000002, 'USA_7');
 測試結果:

 符合規則

一個分片表和一個全局表之間 JOIN
 配置分片規則
<table name="company" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" rule="auto-sharding-long" />
<table name="address" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" type="global" />
 準備測試數據
create table company(id int primary key, companyname varchar(20), addressid int);
create table address(id int primary key, addressname varchar(20));
insert into company(id,companyname,addressid) values(1, 'Intel', 1),(2, 'IBM', 2),(3, 'Dell', 3),(5000001,
'Sony', 5000001),(5000002, 'Apple', 5000002),(10000001, 'Microsoft', 10000001),(10000002, 'Oracle',
10000002);
insert into address(id,addressname) values(1, 'USA_1'),(2, 'USA_2'),(3, 'USA_3'),(5000001,
'USA_4'),(5000002, 'USA_5'),(10000001, 'USA_6'),(10000002, 'USA_7'),(15000001,
'USA_8'),(15000002, 'USA_9');
 測試結果

 

 

結果相同,符合規則

兩表 JOIN 作爲子表
 配置分片規則
<table name="company" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" rule="auto-sharding-long" />
<table name="address" primaryKey="ID" dataNode="htdb1,htdb2,htdb3" rule="auto-sharding-long" />
 準備測試數據
create table company(id int primary key, companyname varchar(20), addressid int);
create table address(id int primary key, addressname varchar(20));
insert into company(id,companyname,addressid) values(1, 'Intel', 1),(2, 'IBM', 2),(3, 'Dell', 3),(5000001,
'Sony', 5000001),(5000002, 'Apple', 5000002),(10000001, 'Microsoft', 10000001),(10000002, 'Oracle',
10000002);
insert into address(id,addressname) values(1, 'USA_1'),(2, 'USA_2'),(3, 'USA_3'),(5000001,
'USA_4'),(5000002, 'USA_5'),(10000001, 'USA_6'),(10000002, 'USA_7'); 

 測試結果

 

符合規則

5.1.1 全局序列號介紹

在實現分庫分表的情況下,數據庫自增主鍵已無法保證自增主鍵的全局唯一。爲此,MyCat 提供了全局
sequence,並且提供了包含本地配置和數據庫配置等多種實現方式。

5.1.2 本地文件方式

原理:此方式 MyCAT 將 sequence 配置到文件中,當使用到 sequence 中的配置後,MyCAT 會更下
classpath 中的 sequence_conf.properties 文件中 sequence 當前的值。
配置方式:
在 sequence_conf.properties 文件中做如下配置:
GLOBAL_SEQ.HISIDS=
GLOBAL_SEQ.MINID=1001
GLOBAL_SEQ.MAXID=1000000000
GLOBAL_SEQ.CURID=1000
其中 HISIDS 表示使用過的歷史分段(一般無特殊需要可不配置),MINID 表示最小 ID 值,MAXID 表示最大
ID 值,CURID 表示當前 ID 值。

server.xml 中配置:
<system><property name="sequnceHandlerType">0</property></system>
注:sequnceHandlerType 需要配置爲 0,表示使用本地文件方式。
使用示例:
insert into table1(id,name) values(next value for MYCATSEQ_GLOBAL,‘test’);
缺點:當 MyCAT 重新發布後,配置文件中的 sequence 會恢復到初始值。
優點:本地加載,讀取速度較快。

5.1.3 數據庫方式

原理
在數據庫中建立一張表,存放 sequence 名稱(name),sequence 當前值(current_value),步長(increment
int 類型每次讀取多少個 sequence,假設爲 K)等信息;
Sequence 獲取步驟:
1).當初次使用該 sequence 時,根據傳入的 sequence 名稱,從數據庫這張表中讀取 current_value,和
increment 到 MyCat 中,並將數據庫中的 current_value 設置爲原 current_value 值+increment 值。
111
.MyCat 將讀取到 current_value+increment 作爲本次要使用的 sequence 值,下次使用時,自動加 1,當
使用 increment 次後,執行步驟 1)相同的操作。
MyCat 負責維護這張表,用到哪些 sequence,只需要在這張表中插入一條記錄即可。若某次讀取的
sequence 沒有用完,系統就停掉了,則這次讀取的 sequence 剩餘值不會再使用。
配置方式:
server.xml 配置:
<system><property name="sequnceHandlerType">1</property></system>
注:sequnceHandlerType 需要配置爲 1,表示使用數據庫方式生成 sequence。
數據庫配置:
1) 創建 MYCAT_SEQUENCE 表
– 創建存放 sequence 的表
DROP TABLE IF EXISTS MYCAT_SEQUENCE;
– name sequence 名稱
– current_value 當前 value
– increment 增長步長! 可理解爲 mycat 在數據庫中一次讀取多少個 sequence. 當這些用完後, 下次再從數
據庫中讀取。
CREATE TABLE MYCAT_SEQUENCE (name VARCHAR(50) NOT NULL,current_value INT NOT
NULL,increment INT NOT NULL DEFAULT 100, PRIMARY KEY(name)) ENGINE=InnoDB;
– 插入一條 sequence
INSERT INTO MYCAT_SEQUENCE(name,current_value,increment) VALUES (‘GLOBAL’, 100000,
100);
2) 創建相關 function
– 獲取當前 sequence 的值 (返回當前值,增量)
DROP FUNCTION IF EXISTS mycat_seq_currval;
DELIMITER
CREATE FUNCTION mycat_seq_currval(seq_name VARCHAR(50)) RETURNS varchar(64) CHARSET
utf-8
112
DETERMINISTIC
BEGIN
DECLARE retval VARCHAR(64);
SET retval=“-999999999,null”;
SELECT concat(CAST(current_value AS CHAR),“,”,CAST(increment AS CHAR)) INTO retval FROM
MYCAT_SEQUENCE WHERE name = seq_name;
RETURN retval;
END
DELIMITER;
– 設置 sequence 值
DROP FUNCTION IF EXISTS mycat_seq_setval;
DELIMITER
CREATE FUNCTION mycat_seq_setval(seq_name VARCHAR(50),value INTEGER) RETURNS varchar(64)
CHARSET utf-8
DETERMINISTIC
BEGIN
UPDATE MYCAT_SEQUENCE
SET current_value = value
WHERE name = seq_name;
RETURN mycat_seq_currval(seq_name);
END
DELIMITER;
– 獲取下一個 sequence 值
DROP FUNCTION IF EXISTS mycat_seq_nextval;
DELIMITER
CREATE FUNCTION mycat_seq_nextval(seq_name VARCHAR(50)) RETURNS varchar(64) CHARSET
utf-8
113
DETERMINISTIC
BEGIN
UPDATE MYCAT_SEQUENCE
SET current_value = current_value + increment WHERE name = seq_name;
RETURN mycat_seq_currval(seq_name);
END
DELIMITER;
4) sequence_db_conf.properties 相關配置,指定 sequence 相關配置在哪個節點上:
例如:
USER_SEQ=test_dn1
注意:MYCAT_SEQUENCE 表和以上的 3 個 function,需要放在同一個節點上。function 請直接在具體節
點的數據庫上執行,如果執行的時候報:
you might want to use the less safe log_bin_trust_function_creators variable
需要對數據庫做如下設置:
windows 下 my.ini[mysqld]加上 log_bin_trust_function_creators=1
linux 下/etc/my.cnf 下 my.ini[mysqld]加上 log_bin_trust_function_creators=1
修改完後,即可在 mysql 數據庫中執行上面的函數。
使用示例:
insert into table1(id,name) values(next value for MYCATSEQ_GLOBAL,‘test’);

5.1.4本地時間戳方式

ID= 64 位二進制 (42(毫秒)+5(機器 ID)+5(業務編碼)+12(重複累加)
換算成十進制爲 18 位數的 long 類型,每毫秒可以併發 12 位二進制的累加。
使用方式:
a. 配置 server.xml
<property name="sequnceHandlerType">2</property>
b. 在 mycat 下配置:sequence_time_conf.properties
WORKID=0-31 任意整數
114
DATAACENTERID=0-31 任意整數
多個個 mycat 節點下每個 mycat 配置的 WORKID,DATAACENTERID 不同,組成唯一標識,總共支持
32*32=1024 種組合。
ID 示例:56763083475511

5.1.5 分佈式 ZK ID 生成器

<property name="sequnceHandlerType">3</property>
Zk 的連接信息統一在 myid.properties 的 zkURL 屬性中配置。
基於 ZK 與本地配置的分佈式 ID 生成器(可以通過 ZK 獲取集羣(機房)唯一 InstanceID,也可以通過配置文
件配置 InstanceID)
ID 結構:long 64 位,ID 最大可佔 63 位
* |current time millis(微秒時間戳 38 位,可以使用 17 年)|clusterId(機房或者 ZKid,通過配置文件配置 5
位)|instanceId(實例 ID,可以通過 ZK 或者配置文件獲取,5 位)|threadId(線程 ID,9 位)
|increment(自增,6 位)
* 一共 63 位,可以承受單機房單機器單線程 1000*(2^6)=640000 的併發。
* 一共 63 位,可以承受單機房單機器單線程 1000*(2^7)=1280000 的併發。
* 無悲觀鎖,無強競爭,吞吐量更高
配置文件:sequence_distributed_conf.properties,只要配置裏面:INSTANCEID=ZK 就是從 ZK 上獲取
InstanceID

5.1.6  Zk 遞增方式

<property name="sequnceHandlerType">4</property>
Zk 的連接信息統一在 myid.properties 的 zkURL 屬性中配置。
4 是 zookeeper 實現遞增序列號
* 配置文件:sequence_conf.properties
* 只要配置好 ZK 地址和表名的如下屬性
* TABLE.MINID 某線程當前區間內最小值
* TABLE.MAXID 某線程當前區間內最大值
* TABLE.CURID 某線程當前區間內當前值
* 文件配置的 MAXID 以及 MINID 決定每次取得區間,這個對於每個線程或者進程都有效
* 文件中的這三個屬性配置只對第一個進程的第一個線程有效,其他線程和進程會動態讀取 ZK

5.1.7 其他方式 

1) 使用 catelet 註解方式
/*!mycat:catlet=demo.catlets.BatchGetSequence */SELECT mycat_get_seq(‘GLOBAL’,100);
注:此方法表示獲取 GLOBAL 的 100 個 sequence 值,例如當前 GLOBAL 的最大 sequence 值爲 5000,
則通過此方式返回的是 5001,同時更新數據庫中的 BLOBAL 的最大 sequence 值爲 5100。
2) 利用 zookeeper 方式實現

5.1.8 自增長主鍵

MyCAT 自增長主鍵和返回生成主鍵 ID 的實現
說明:
1) mysql 本身對非自增長主鍵,使用 last_insert_id()是不會返回結果的,只會返回 0;
2) mysql 只會對定義自增長主鍵,可以用 last_insert_id()返回主鍵值;
MyCAT 目前提供了自增長主鍵功能,但是如果對應的 mysql 節點上數據表,沒有定義 auto_increment,
那麼在 MyCAT 層調用 last_insert_id()也是不會返回結果的。
正確配置方式如下:
1) mysql 定義自增主鍵
CREATE TABLE table1(
‘id_’ INT(10) UNSIGNED NOT NULL AUTO_INCREMENT,
‘name_’ INT(10) UNSIGNED NOT NULL,
PRIMARY KEY (‘id_’)
) ENGINE=MYISAM AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;
2) mycat 定義主鍵自增

3) mycat 對應 sequence_db_conf.properties 增加相應設置
TABLE1=dn1
4) 在數據庫中 mycat_sequence 表中增加 TABLE1 表的 sequence 記錄
測試使用:
127.0.0.1/root:[TESTDB> insert into tt2(name_) values(‘t1’);
Query OK, 1 row affected (0.14 sec)
127.0.0.1/root:[TESTDB> select last_insert_id();
+——————+
| LAST_INSERT_ID() |
+——————+
| 100 |
+——————+
1 row in set (0.01 sec)
127.0.0.1/root:[TESTDB> insert into tt2(name_) values(‘t2’);
Query OK, 1 row affected (0.00 sec)
117
127.0.0.1/root:[TESTDB> select last_insert_id();
+——————+
| LAST_INSERT_ID() |
+——————+
| 101 |
+——————+
1 row in set (0.00 sec)
127.0.0.1/root:[TESTDB> insert into tt2(name_) values(‘t3’);
Query OK, 1 row affected (0.00 sec)
127.0.0.1/root:[TESTDB> select last_insert_id();
+——————+
| LAST_INSERT_ID() |
+——————+
| 102 |
+——————+
1 row in set (0.00 sec)
Myibatis 中新增記錄後獲取 last_insert_id 的示例:

6.1  分片規則


在數據切分處理中,特別是水平切分中,中間件最終要的兩個處理過程就是數據的切分、數據的聚合。選擇
合適的切分規則,至關重要,因爲它決定了後續數據聚合的難易程度,甚至可以避免跨庫的數據聚合處理。
前面講了數據切分中重要的幾條原則,其中有幾條是數據冗餘,表分組(Table Group),這都是業務上規
避跨庫 join 的很好的方式,但不是所有的業務場景都適合這樣的規則,因此本章將講述如何選擇合適的切分規
則。 

6.1.1 Mycat 全局表

如果你的業務中有些數據類似於數據字典,比如配置文件的配置,常用業務的配置或者數據量不大很少變動
的表,這些表往往不是特別大,而且大部分的業務場景都會用到,那麼這種表適合於 Mycat 全局表,無須對數據
進行切分,只要在所有的分片上保存一份數據即可,Mycat 在 Join 操作中,業務表與全局表進行 Join 聚合會優
先選擇相同分片內的全局表 join,避免跨庫 Join,在進行數據插入操作時,mycat 將把數據分發到全局表對應的
所有分片執行,在進行數據讀取時候將會隨機獲取一個節點讀取數據。
目前 Mycat 沒有做全局表的數據一致性檢查,後續版本 1.4 之後可能會提供全局表一致性檢查,檢查每個分
片的數據一致性。
全局表的配置如下
<table name="t_area" primaryKey="id" type="global" dataNode="dn1,dn2" />

6.1.2 ER 分片表

有一類業務,例如訂單(order)跟訂單明細(order_detail),明細表會依賴於訂單,也就是說會存在表的主
從關係,這類似業務的切分可以抽象出合適的切分規則,比如根據用戶 ID 切分,其他相關的表都依賴於用戶 ID,
再或者根據訂單 ID 切分,總之部分業務總會可以抽象出父子關係的表。這類表適用於 ER 分片表,子表的記錄與
所關聯的父表記錄存放在同一個數據分片上,避免數據 Join 跨庫操作。
以 order 與 order_detail 例子爲例,schema.xml 中定義如下的分片配置,order,order_detail 根據 order_id
進行數據切分,保證相同 order_id 的數據分到同一個分片上,在進行數據插入操作時,Mycat 會獲取 order 所在
的分片,然後將 order_detail 也插入到 order 所在的分片。

<table name="order" dataNode="dn$1-32" rule="mod-long">
<childTable name="order_detail" primaryKey="id" joinKey="order_id" parentKey="order_id" />
</table>

6.1.3  多對多關聯

有一類業務場景是 “主表 A+關係表+主表 B”,舉例來說就是商戶會員+訂單+商戶,對應這類業務,如何
切分?
從會員的角度,如果需要查詢會員購買的訂單,那按照會員進行切分即可,但是如果要查詢商戶當天售出的
訂單,那又需要按照商戶做切分,可是如果既要按照會員又要按照商戶切分,幾乎是無法實現,這類業務如何選
擇切分規則非常難。目前還暫時無法很好支持這種模式下的 3 個表之間的關聯。目前總的原則是需要從業務角度
來看,關係表更偏向哪個表,即“A 的關係”還是“B 的關係”,來決定關係表跟從那個方向存儲,未來 Mycat
版本中將考慮將中間表進行雙向複製,以實現從 A-關係表 以及 B-關係表的雙向關聯查詢如下圖所示:

6.1.4 主鍵分片 vs 非主鍵分片 

當你沒人任何字段可以作爲分片字段的時候,主鍵分片就是唯一選擇,其優點是按照主鍵的查詢最快,當採
用自動增長的序列號作爲主鍵時,還能比較均勻的將數據分片在不同的節點上。
若有某個合適的業務字段比較合適作爲分片字段,則建議採用此業務字段分片,選擇分片字段的條件如下:
120
 儘可能的比較均勻分佈數據到各個節點上;
 該業務字段是最頻繁的或者最重要的查詢條件。
常見的除了主鍵之外的其他可能分片字段有“訂單創建時間”、“店鋪類別”或“所在省”等。當你找到某
個合適的業務字段作爲分片字段以後,不必糾結於“犧牲了按主鍵查詢記錄的性能”,因爲在這種情況下,
MyCAT 提供了“主鍵到分片”的內存緩存機制,熱點數據按照主鍵查詢,絲毫不損失性能。
<table name="t_user" primaryKey="user_id" dataNode="dn$1-32" rule="mod-long">
<childTable name="t_user_detail" primaryKey="id" joinKey="user_id" parentKey="user_id" />
</table>
對於非主鍵分片的 table,填寫屬性 primaryKey,此時 MyCAT 會將你根據主鍵查詢的 SQL 語句的第一次執
行結果進行分析,確定該 Table 的某個主鍵在什麼分片上,並進行主鍵到分片 ID 的緩存。第二次或後續查詢
mycat 會優先從緩存中查詢是否有 id–>node 即主鍵到分片的映射,如果有直接查詢,通過此種方法提高了非主
鍵分片的查詢性能。
本節主要講了如何去分片,如何選擇合適分片的規則,總之儘量規避跨庫 Join 是一條最重要的原則,下一節
將介紹 Mycat 目前已有的分片規則,每種規則都有特定的場景,分析每種規則去選擇合適的應用到項目中。

 6.1.5 Mycat 常用的分片規則

1.分片枚舉
通過在配置文件中配置可能的枚舉 id,自己配置分片,本規則適用於特定的場景,比如有些業務需要按照省
份或區縣來做保存,而全國省份區縣固定的,這類業務使用本條規則,配置如下:

 

上面 columns 標識將要分片的表字段,algorithm 分片函數,
其中分片函數配置中,mapFile 標識配置文件名稱,type 默認值爲 0,0 表示 Integer,非零表示 String,
所有的節點配置都是從 0 開始,及 0 代表節點 1
/**
* defaultNode 默認節點:小於 0 表示不設置默認節點,大於等於 0 表示設置默認節點
* 默認節點的作用:枚舉分片時,如果碰到不識別的枚舉值,就讓它路由到默認節點
* 如果不配置默認節點(defaultNode 值小於 0 表示不配置默認節點),碰到
* 不識別的枚舉值就會報錯,
* like this:can’t find datanode for sharding column:column_name val:ffffffff
*/

2. 固定分片 hash 算法

本條規則類似於十進制的求模運算,區別在於是二進制的操作,是取 id 的二進制低 10 位,即 id 二進制
&1111111111。
此算法的優點在於如果按照 10 進製取模運算,在連續插入 1-10 時候 1-10 會被分到 1-10 個分片,增
大了插入的事務控制難度,而此算法根據二進制則可能會分到連續的分片,減少插入事務事務控制難度。

<tableRule name="rule1">
<rule>
<columns>user_id</columns>
<algorithm>func1</algorithm>
</rule>
</tableRule>
<function name="func1" class="io.mycat.route.function.PartitionByLong">
<property name="partitionCount">2,1</property>
<property name="partitionLength">256,512</property>
</function>

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,
partitionCount 分片個數列表,partitionLength 分片範圍列表
分區長度:默認爲最大 2^n=1024 ,即最大支持 1024 分區
約束 :
count,length 兩個數組的長度必須是一致的。
1024 = sum((count[i]*length[i])). count 和 length 兩個向量的點積恆等於 1024
用法例子:
本例的分區策略:希望將數據水平分成 3 份,前兩份各佔 25%,第三份佔 50%。(故本例非均勻分區)
// |<———————1024———————————>|
122
// |<—-256—>|<—-256—>|<———-512————->|
// | partition0 | partition1 | partition2 |
// | 共 2 份,故 count[0]=2 | 共 1 份,故 count[1]=1 |
int[] count = new int[] { 2, 1 };
int[] length = new int[] { 256, 512 };
PartitionUtil pu = new PartitionUtil(count, length);

// 下面代碼演示分別以 offerId 字段或 memberId 字段根據上述分區策略拆分的分配結果
int DEFAULT_STR_HEAD_LEN = 8; // cobar 默認會配置爲此值
long offerId = 12345;
String memberId = "qiushuo";
// 若根據 offerId 分配,partNo1 將等於 0,即按照上述分區策略,offerId 爲 12345 時將會被分配
到 partition0 中
int partNo1 = pu.partition(offerId);
// 若根據 memberId 分配,partNo2 將等於 2,即按照上述分區策略,memberId 爲 qiushuo 時將會被
分到 partition2 中
int partNo2 = pu.partition(memberId, 0, DEFAULT_STR_HEAD_LEN);
如果需要平均分配設置:平均分爲 4 分片,partitionCount*partitionLength=1024
<function name="func1" class="io.mycat.route.function.PartitionByLong">
<property name="partitionCount">4</property>
<property name="partitionLength">256</property>
</function>

3.範圍約定

此分片適用於,提前規劃好分片字段某個範圍屬於哪個分片,
start <= range <= end.
range start-end ,data node index
K=1000,M=10000.

<tableRule name="auto-sharding-long">
<rule>
<columns>user_id</columns>
<algorithm>rang-long</algorithm>
</rule>
</tableRule>
<function name="rang-long" class="io.mycat.route.function.AutoPartitionByLong">
<property name="mapFile">autopartition-long.txt</property>
<property name="defaultNode">0</property>
</function>

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,
rang-long 函數中 mapFile 代表配置文件路徑
defaultNode 超過範圍後的默認節點。
所有的節點配置都是從 0 開始,及 0 代表節點 1,此配置非常簡單,即預先制定可能的 id 範圍到某個分片
0-500M=0
500M-1000M=1
1000M-1500M=2

0-10000000=0
10000001-20000000=1

4.取模

此規則爲對分片字段求摸運算。

<tableRule name="mod-long">
<rule>
<columns>user_id</columns>
<algorithm>mod-long</algorithm>
</rule>
</tableRule>
<function name="mod-long" class="io.mycat.route.function.PartitionByMod">
<!-- how many data nodes -->
<property name="count">3</property>
</function>

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,
此種配置非常明確即根據 id 進行十進制求模預算,相比固定分片 hash,此種在批量插入時可能存在批量插入單
事務插入多數據分片,增大事務一致性難度。

5.按日期(天)分片

此規則爲按天分片

<tableRule name="sharding-by-date">
<rule>
<columns>create_time</columns>
<algorithm>sharding-by-date</algorithm>
</rule>
</tableRule>
<function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sBeginDate">2014-01-01</property>
124
<property name="sEndDate">2014-01-02</property>
<property name="sPartionDay">10</property>
</function>

配置說明:
columns :標識將要分片的表字段
algorithm :分片函數
dateFormat :日期格式
sBeginDate :開始日期
sEndDate:結束日期
sPartionDay :分區天數,即默認從開始日期算起,分隔 10 天一個分區
如果配置了 sEndDate 則代表數據達到了這個日期的分片後後循環從開始分片插入。
Assert.assertEquals(true, 0 == partition.calculate(“2014-01-01”));
Assert.assertEquals(true, 0 == partition.calculate(“2014-01-10”));
Assert.assertEquals(true, 1 == partition.calculate(“2014-01-11”));
Assert.assertEquals(true, 12 == partition.calculate(“2014-05-01”)); 

6. 取模範圍約束

此種規則是取模運算與範圍約束的結合,主要爲了後續數據遷移做準備,即可以自主決定取模後數據的節點
分佈。

<tableRule name="sharding-by-pattern">
<rule>
<columns>user_id</columns>
<algorithm>sharding-by-pattern</algorithm>
</rule>
</tableRule>
<function name="sharding-by-pattern"
class="io.mycat.route.function.PartitionByPattern">
<property name="patternValue">256</property>
<property name="defaultNode">2</property>
<property name="mapFile">partition-pattern.txt</property>
</function> 

partition-pattern.txt

# id partition range start-end ,data node index
###### first host configuration
1-32=0
33-64=1
65-96=2
125
97-128=3
######## second host configuration
129-160=4
161-192=5
193-224=6
225-256=7
0-0=7 

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,patternValue 即求模基數,defaoultNode
默認節點,如果配置了默認,則不會按照求模運算
mapFile 配置文件路徑
配置文件中,1-32 即代表 id%256 後分布的範圍,如果在 1-32 則在分區 1,其他類推,如果 id 非數據,則
會分配在 defaoultNode 默認節點
String idVal = “0”;
Assert.assertEquals(true, 7 == autoPartition.calculate(idVal));
idVal = “45a”;
Assert.assertEquals(true, 2 == autoPartition.calculate(idVal));

7.截取數字做 hash 求模範圍約束

此種規則類似於取模範圍約束,此規則支持數據符號字母取模。

<tableRule name="sharding-by-prefixpattern">
<rule>
<columns>user_id</columns>
<algorithm>sharding-by-prefixpattern</algorithm>
</rule>
</tableRule>
<function name="sharding-by-pattern"
class="io.mycat.route.function.PartitionByPrefixPattern">
<property name="patternValue">256</property>
<property name="prefixLength">5</property>
<property name="mapFile">partition-pattern.txt</property>
</function> 

partition-pattern.txt

# range start-end ,data node index
# ASCII
# 8-57=0-9 阿拉伯數字
# 64、65-90=@、A-Z
# 97-122=a-z
###### first host configuration
1-4=0
5-8=1
9-12=2
13-16=3
###### second host configuration
17-20=4
21-24=5
25-28=6
29-32=7
0-0=7

 配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,patternValue 即求模基數,prefixLength
ASCII 截取的位數
mapFile 配置文件路徑
配置文件中,1-32 即代表 id%256 後分布的範圍,如果在 1-32 則在分區 1,其他類推
此種方式類似方式 6 只不過採取的是將列種獲取前 prefixLength 位列所有 ASCII 碼的和進行求模
sum%patternValue ,獲取的值,在範圍內的分片數,
String idVal=“gf89f9a”;
Assert.assertEquals(true, 0==autoPartition.calculate(idVal));
idVal=“8df99a”;
Assert.assertEquals(true, 4==autoPartition.calculate(idVal));
idVal=“8dhdf99a”;
Assert.assertEquals(true, 3==autoPartition.calculate(idVal));

8.應用指定

此規則是在運行階段有應用自主決定路由到那個分片

<tableRule name="sharding-by-substring">
<rule>
<columns>user_id</columns>
<algorithm>sharding-by-substring</algorithm>
</rule>
</tableRule>
<function name="sharding-by-substring"
class="io.mycat.route.function.PartitionDirectBySubString">
<property name="startIndex">0</property><!-- zero-based -->
<property name="size">2</property>

<property name="partitionCount">8</property>
<property name="defaultPartition">0</property>
</function>

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數
此方法爲直接根據字符子串(必須是數字)計算分區號(由應用傳遞參數,顯式指定分區號)。
例如 id=05-100000002
在此配置中代表根據 id 中從 startIndex=0,開始,截取 siz=2 位數字即 05,05 就是獲取的分區,如果沒傳
默認分配到 defaultPartition

9.截取數字 hash 解析

此規則是截取字符串中的 int 數值 hash 分片。

<tableRule name="sharding-by-stringhash">
<rule>
<columns>user_id</columns>
<algorithm>sharding-by-stringhash</algorithm>
</rule>
</tableRule>
<function name="sharding-by-stringhash"
class="io.mycat.route.function.PartitionByString">
<property name="partitionLength">512</property><!-- zero-based -->
<property name="partitionCount">2</property>
<property name="hashSlice">0:2</property>
</function> 

配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數
函數中 partitionLength 代表字符串 hash 求模基數,
partitionCount 分區數,
hashSlice hash 預算位,即根據子字符串中 int 值 hash 運算
hashSlice : 0 means str.length(), -1 means str.length()-1
/**
* “2” -> (0,2)
* “1:2” -> (1,2)
* “1:” -> (1,0)
* “-1:” -> (-1,0)
128
* “:-1” -> (0,-1)
* “:” -> (0,0)
*/

例子:
String idVal=null;
rule.setPartitionLength("512");
rule.setPartitionCount("2");
rule.init();
rule.setHashSlice("0:2");
// idVal = "0";
// Assert.assertEquals(true, 0 == rule.calculate(idVal));
// idVal = "45a";
// Assert.assertEquals(true, 1 == rule.calculate(idVal));
//last 4
rule = new PartitionByString();
rule.setPartitionLength("512");
rule.setPartitionCount("2");
rule.init();
//last 4 characters
rule.setHashSlice("-4:0");
idVal = "aaaabbb0000";
Assert.assertEquals(true, 0 == rule.calculate(idVal));
idVal = "aaaabbb2359";
Assert.assertEquals(true, 0 == rule.calculate(idVal));

 

10 一致性 hash

一致性 hash 預算有效解決了分佈式數據的擴容問題。

<tableRule name="sharding-by-murmur">
<rule>
<columns>user_id</columns>
<algorithm>murmur</algorithm>
</rule>
</tableRule>
<function name="murmur" class="io.mycat.route.function.PartitionByMurmurHash">
<property name="seed">0</property><!-- 默認是 0-->
<property name="count">2</property><!-- 要分片的數據庫節點數量,必須指定,否則沒法分片-->
<property name="virtualBucketTimes">160</property><!-- 一個實際的數據庫節點被映射爲這麼多虛擬

節點,默認是 160 倍,也就是虛擬節點數是物理節點數的 160 倍-->
<!--
<property name="weightMapFile">weightMapFile</property>
節點的權重,沒有指定權重的節點默認是 1。以 properties 文件的格式填寫,以從 0 開始到 count-1 的整數值也就
是節點索引爲 key,以節點權重值爲值。所有權重值必須是正整數,否則以 1 代替 -->
<!--
<property name="bucketMapPath">/etc/mycat/bucketMapPath</property>
用於測試時觀察各物理節點與虛擬節點的分佈情況,如果指定了這個屬性,會把虛擬節點的 murmur hash 值與物理節
點的映射按行輸出到這個文件,沒有默認值,如果不指定,就不會輸出任何東西 -->
</function>

11 按單月小時拆分

此規則是單月內按照小時拆分,最小粒度是小時,可以一天最多 24 個分片,最少 1 個分片,一個月完後下月
從頭開始循環。
每個月月尾,需要手工清理數據


<tableRule name="sharding-by-hour">
<rule>
<columns>create_time</columns>
<algorithm>sharding-by-hour</algorithm>
</rule>
</tableRule>
<function name="sharding-by-hour" class="io.mycat.route.function.LatestMonthPartion">
<property name="splitOneDay">24</property>
</function>

配置說明:
columns: 拆分字段,字符串類型(yyyymmddHH)
splitOneDay : 一天切分的分片數

LatestMonthPartion partion = new LatestMonthPartion();
partion.setSplitOneDay(24);
Integer val = partion.calculate("2015020100");
assertTrue(val == 0);
val = partion.calculate("2015020216");
assertTrue(val == 40);
val = partion.calculate("2015022823");
assertTrue(val == 27 * 24 + 23);
Integer[] span = partion.calculateRange("2015020100", "2015022823");
assertTrue(span.length == 27 * 24 + 23 + 1);
assertTrue(span[0] == 0 && span[span.length - 1] == 27 * 24 + 23);

span = partion.calculateRange("2015020100", "2015020123");
assertTrue(span.length == 24);
assertTrue(span[0] == 0 && span[span.length - 1] == 23); 

12.範圍求模分片 

先進行範圍分片計算出分片組,組內再求模
優點可以避免擴容時的數據遷移,又可以一定程度上避免範圍分片的熱點問題
綜合了範圍分片和求模分片的優點,分片組內使用求模可以保證組內數據比較均勻,分片組之間是範圍分片可以
兼顧範圍查詢。
最好事先規劃好分片的數量,數據擴容時按分片組擴容,則原有分片組的數據不需要遷移。由於分片組內數據比
較均勻,所以分片組內可以避免熱點數據問題。

<tableRule name="auto-sharding-rang-mod">
<rule>
<columns>id</columns>
<algorithm>rang-mod</algorithm>
</rule>
</tableRule>
<function name="rang-mod"
class="io.mycat.route.function.PartitionByRangeMod">
<property name="mapFile">partition-range-mod.txt</property>
<property name="defaultNode">21</property>
</function>

 配置說明:
上面 columns 標識將要分片的表字段,algorithm 分片函數,
rang-mod 函數中 mapFile 代表配置文件路徑
defaultNode 超過範圍後的默認節點順序號,節點從 0 開始。
partition-range-mod.txt
range start-end ,data node group size
以下配置一個範圍代表一個分片組,=號後面的數字代表該分片組所擁有的分片的數量。
0-200M=5 //代表有 5 個分片節點
200M1-400M=1
400M1-600M=4
600M1-800M=4
800M1-1000M=6

13. 日期範圍 hash 分片

思想與範圍求模一致,當由於日期在取模會有數據集中問題,所以改成 hash 方法。
先根據日期分組,再根據時間 hash 使得短期內數據分佈的更均勻
優點可以避免擴容時的數據遷移,又可以一定程度上避免範圍分片的熱點問題
要求日期格式儘量精確些,不然達不到局部均勻的目的

< tableRule name="rangeDateHash">
<rule>
<columns>col_date</columns>
<algorithm>range-date-hash</algorithm>
</rule>
</tableRule>
<function name="range-date-hash"
class="io.mycat.route.function.PartitionByRangeDateHash">
<property name="sBeginDate">2014-01-01 00:00:00</property>
<property name="sPartionDay">3</property>
<property name="dateFormat">yyyy-MM-dd HH:mm:ss</property>
<property name="groupPartionSize">6</property>
</function>
sPartionDay 代表多少天分一個分片
groupPartionSize 代表分片組的大小
14 .冷熱數據分片
根據日期查詢日誌數據 冷熱數據分佈 ,最近 n 個月的到實時交易庫查詢,超過 n 個月的按照 m 天分片。
<tableRule name="sharding-by-date">
<rule>
<columns>create_time</columns>
<algorithm>sharding-by-hotdate</algorithm>
</rule>
</tableRule>
<function name="sharding-by-hotdate" class="io.mycat.route.function.PartitionByHotDate">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sLastDay">10</property>
<property name="sPartionDay">30</property>
</function>
15. 自然月分片
按月份列分區 ,每個自然月一個分片,格式 between 操作解析的範例。
<tableRule name="sharding-by-month">
<rule>
<columns>create_time</columns>
<algorithm>sharding-by-month</algorithm>
</rule>
</tableRule>
<function name="sharding-by-month" class="io.mycat.route.function.PartitionByMonth">
<property name="dateFormat">yyyy-MM-dd</property>
<property name="sBeginDate">2014-01-01</property>
</function>
配置說明:
columns: 分片字段,字符串類型
dateFormat : 日期字符串格式,默認爲 yyyy-MM-dd
sBeginDate : 開始日期,無默認值
sEndDate:結束日期,無默認值
節點從 0 開始分片
使用場景:
場景 1:
默認設置;節點數量必須是 12 個,從 1 月~12 月
 "2014-01-01" = 節點 0
 "2013-01-01" = 節點 0
 "2018-05-01" = 節點 4
 "2019-12-01" = 節點 11
場景 2:
sBeginDate = "2017-01-01"
該配置表示"2017-01 月"是第 0 個節點,從該時間按月遞增,無最大節點
 "2014-01-01" = 未找到節點
 "2017-01-01" = 節點 0
 "2017-12-01" = 節點 11
 "2018-01-01" = 節點 12
 "2018-12-01" = 節點 23
場景 3:
sBeginDate = "2015-01-01"sEndDate = "2015-12-01"
該配置可看成與場景 1 一致;場景 1 的配置效率更高
 "2014-01-01" = 節點 0
 "2014-02-01" = 節點 1
 "2015-02-01" = 節點 1
 "2017-01-01" = 節點 0
 "2017-12-01" = 節點 11
 "2018-12-01" = 節點 11
該配置可看成是與場景 1 一致
場景 4:
sBeginDate = "2015-01-01"sEndDate = "2015-03-01"
該配置標識只有 3 個節點;很難與月份對應上;平均分散到 3 個節點上
自然月分片算法功能測試用例:
PartitionByMonth partition = new PartitionByMonth();
partition.setDateFormat("yyyy-MM-dd");
partition.setsBeginDate("2014-01-01");
partition.init();
Assert.assertEquals(true, 0 == partition.calculate("2014-01-01"));
Assert.assertEquals(true, 0 == partition.calculate("2014-01-10"));
Assert.assertEquals(true, 0 == partition.calculate("2014-01-31"));
Assert.assertEquals(true, 1 == partition.calculate("2014-02-01"));
Assert.assertEquals(true, 1 == partition.calculate("2014-02-28"));
Assert.assertEquals(true, 2 == partition.calculate("2014-03-1"));
Assert.assertEquals(true, 11 == partition.calculate("2014-12-31"));
Assert.assertEquals(true, 12 == partition.calculate("2015-01-31"));
Assert.assertEquals(true, 23 == partition.calculate("2015-12-31"));
16 .有狀態分片算法
有狀態分片算法與之前的分片算法不同,它是爲(在線)數據自動遷移而設計的.
數據自動遷移分片算法需要滿足一致性哈希的要求,尤其是單調性。
直至 2018 年 7 月 24 日爲止,現支持有狀態算法的分片策略只有 crc32slot 歡迎大家提供更多有狀態分片算法.
一個有狀態分片算法在使用過程中暫時存在兩個操作
一種是初始化,使用 mycat 創建配置帶有有狀態分片算法的 table 時(推介)或者第一次配置有狀態分片算法的
table 並啓動 mycat 時,有狀態分片算法會根據表的 dataNode 的數量劃分分片範圍並生成 ruledata 下的文件,
這個分片範圍規則就是’狀態’,一個表對應一個狀態,對應一個有狀態分片算法實例,以及對應一個滿足以下命
名規則的文件:
算法名字_schema 名字_table 名字.properties
文件裏內容一般具有以下特徵
8 8= 91016-102399
7 7= 79639-91015
6 6= 68262-79638
5 5= 56885-68261
4 4= 45508-56884
3 3= 34131-45507
2 2= 22754-34130
1 1= 11377-22753
0 0= 0-11376
行數就是 table 的分片節點數量,每行的’數字-數字’就是分片算法生成的範圍,這個範圍與具體算法實現有關,一
個分片節點可能存在多個範圍,這些範圍以逗號,分隔.一般來說,不要手動更改這個文件,應該使用算法生成範圍,而
且需要注意的是,物理庫上的數據的分片字段的值一定要落在對應範圍裏.
一種是添加操作,即數據擴容,具體參考第六章的 6.8 與 6.9
添加節點,有狀態分片算法根據節點的變化,重新分配範圍規則,之後執行數據自動遷移任務.

17 .crc32slot 分片算法
crc32solt 是有狀態分片算法的實現之一,是一致性哈希,具體參考第六章 數據自動遷移方案設計
crc32(key)%102400=slot
slot 按照範圍均勻分佈在 dataNode 上,針對每張表進行實例化,通過一個文件記錄 slot 和節點
映射關係,遷移過程中通過 zk 協調
其中需要在分片表中增加 slot 字段,用以避免遷移時重新計算,只需要遷移對應 slot 數據即可
分片最大個數爲 102400 個,短期內應該夠用,每分片一千萬,總共可以支持一萬億數據
值得注意的是 crc32 算法對字段計算的結果與字符集有關
crc32 會根據用戶指定的分片字段,即圖中的 id,算出 slot 的值
<e tableRule name ="crc32slot">
< rule>
< columns>id</ columns>
< algorithm>crc32slot</ algorithm>
</ rule>
</ tableRule>
然後根據 slot 找到對應的節點
c public Integer calculate(String columnValue) {
f if (e ruleName == null)
w throw w new RuntimeException();
PureJavaCrc32 crc32 = w new PureJavaCrc32();
byte[] bytes = columnValue.getBytes( DEFAULT_CHARSET );
crc32.update(bytes, 0, bytes. length);
g long x = crc32.getValue();
t int slot = ( int) (x % DEFAULT_SLOTS_NUM );
this.t slot = slot;
n return rangeMap2[slot];
}
因爲算法中的_slot 字段字段被算法佔用,所以使用 crc32slot 的 tableRule 中的 rule 的 columns 分片字段
不能爲_slot.。_slot 是爲了數據自動遷移過程中不需要重複根據分片字段計算_slot 而在數據庫存儲層面做的數
據冗餘。考慮數據冗餘帶來的數據存儲空間與傳輸層面的開銷與重複計算_slot 的時間開銷,冗餘 crc32 計算
結果是值得的。如果有特殊原因可以提供一個選項給用戶選擇是否創建_slot 字段.此爲後續 mycat 開發的一個
任務。

配置說明:
<e table name" ="travelrecord" dataNode" ="dn1,dn2" rule" ="crc32slot" />
使用 mycat 配置完表後使用 mycat 創建表。
需要注意的是,在 rule.xml 中 crc32slot 的信息請保持如下配置,不需要配置 count
<n function name ="crc32slot"
class ="io.mycat.route.function.PartitionByCRC32PreSlot">
</ function>
USE TESTDB;
CREATE TABLE `travelrecord` (
id xxxx
xxxxxxx
) ENGINE=INNODB DEFAULT CHARSET=utf8;

7.1權限控制

7.1.1 遠程連接配置(讀、寫權限)

目前 Mycat 對於中間件的連接控制並沒有做太複雜的控制,目前只做了中間件邏輯庫級別的讀寫權限控制

<user name="mycat">
<property name="password">mycat</property>
<property name="schemas">order</property>
<property name="readOnly">true</property>
</user>
<user name="mycat2">
<property name="password">mycat</property>
<property name="schemas">order</property>
</user>

配置說明:
配置中 name 是應用連接中間件邏輯庫的用戶名。
mycat 中 password 是應用連接中間件邏輯庫的密碼。
order 中是應用當前連接的邏輯庫中所對應的邏輯表。schemas 中可以配置一個或多個。
true 中 readOnly 是應用連接中間件邏輯庫所具有的權限。true 爲只讀,false 爲讀寫都有,默認爲 false。

7.1.2 多租戶支持

單租戶就是傳統的給每個租戶獨立部署一套 web + db 。由於租戶越來越多,整個 web 部分的機器和運維成
本都非常高,因此需要改進到所有租戶共享一套 web 的模式(db 部分暫不改變)。
基於此需求,我們對單租戶的程序做了簡單的改造實現 web 多租戶共享。具體改造如下:
1.web 部分修改:
a.在用戶登錄時,在線程變量(ThreadLocal)中記錄租戶的 id
b.修改 jdbc 的實現:在提交 sql 時,從 ThreadLocal 中獲取租戶 id, 添加 sql 註釋,把租戶的 schema
放到註釋中。例如:/*!mycat : schema = test_01 */ sql ;
2.在 db 前面建立 proxy 層,代理所有 web 過來的數據庫請求。proxy 層是用 mycat 實現的,web 提交的 sql 過
來時在註釋中指定 schema, proxy 層根據指定的 schema 轉發 sql 請求。
3.Mycat 配置:


<user name="mycat">
<property name="password">mycat</property>
<property name="schemas">order</property>
<property name="readOnly">true</property>
</user>
<user name="mycat2">
<property name="password">mycat</property>
<property name="schemas">order</property>
</user>

 8.1常見問題與解決方案

8.1.1 Mycat 目前有哪些功能與特性

• 支持 SQL 92 標準;
• 支持 Mysql 集羣,可以作爲 Proxy 使用;
• 支持 JDBC 連接多數據庫;
• 支持 NoSQL 數據庫;
• 支持 galera for mysql 集羣,percona-cluster 或者 mariadb cluster,提供高可用性數據分片集羣;
• 自動故障切換,高可用性;
• 支持讀寫分離,支持 Mysql 雙主多從,以及一主多從的模式;
• 支持全局表,數據自動分片到多個節點,用於高效表關聯查詢;
• 支持獨有的基於 E-R 關係的分片策略,實現了高效的表關聯查詢;
• 支持一致性 Hash 分片,有效解決分片擴容難題;
• 多平臺支持,部署和實施簡單;
• 支持 Catelet 開發,類似數據庫存儲過程,用於跨分片複雜 SQL 的人工智能編碼實現,143 行 Demo 完成
跨分片的兩個表的 JION 查詢;
• 支持 NIO 與 AIO 兩種網絡通信機制,Windows 下建議 AIO,Linux 下目前建議 NIO;
• 支持 Mysql 存儲過程調用;
• 以插件方式支持 SQL 攔截和改寫;
• 支持自增長主鍵、支持 Oracle 的 Sequence 機制。

Mycat 除了 Mysql 還支持哪些數據庫?
答:mongodb、oracle、sqlserver 、hive 、db2 、 postgresql。
8.1.2 Mycat 目前有生產案例了麼?
答:目前 Mycat 初步統計大概 600 家公司使用。
8.1.3 Mycat 穩定性與 Cobar 如何?
答:目前 Mycat 穩定性優於 Cobar,而且一直在更新,Cobar 已經停止維護,可以放心使用。
8.1.4 Mycat 支持集羣麼?
答:目前 Mycat 沒有實現對多 Mycat 集羣的支持,可以暫時使用 haproxy 來做負載,或者統計硬件負載。
8.1.5 Mycat 多主切換需要人工處理麼?
答:Mycat 通過心跳檢測,自主切換數據庫,保證高可用性,無須手動切換。
8.1.6 Mycat 目前有多少人開發?
答:Mycat 目前開發全部是志願者無償支持,主要有以 leaderus 爲首的 Mycat-Server 開始、以 rainbow
爲首的 Mycat-web 開發、以海王星爲首的產品發佈及代碼管理,還有以 Marshy 爲首的推廣。
8.1.7 Mycat 目前有哪些項目?

答:Mycat-Server :Mycat 核心服務;
Mycat-spider : Mycat 爬蟲技術;
Mycat-ConfigCenter :Mycat 配置中心 ;
Mycat-BigSQL : Mycat 大數據處理(暫未更細);
Mycat-Web : Mycat 監控及 web(新版開發中) ;
Mycat-Balance :Mycat 集羣負載(暫未更細)。
8.1.8 Mycat 最新的穩定版本是哪個到哪裏下載?
答:打包代碼:Mycat 最新穩定版是 1.5.1 ,1.6 爲 aphla,下載地址是:
https://github.com/MyCATApache/Mycat-download。
文檔:https://github.com/MyCATApache/Mycat-doc。
源碼:https://github.com/MyCATApache/Mycat-Server。
8.1.9 Mycat 如何配置字符集?
答:在配置文件 server.xml 配置,默認配置爲 utf8。
<system>
<property name="charset">utf8</property>
</system>
 8.1.10Mycat 後臺管理監控如何使用?
答:9066 端口可以用 JDBC 方式執行命令,在界面上進行管理維護,也可以通過命令行查看命令行操作。
命令行操作是:mysql -h127.0.0.1 -utest -ptest -P9066 登陸,然後執行相應命令。
8.1.11 Mycat 主鍵插入後應用如何獲取?
答:獲得自增主鍵,插入記錄後執行 select last_insert_id()獲取。
8.1.12Mycat 如何啓動與加入服務?
答:目前 Mycat 暫未封裝加入服務,需要自己封裝。
linux 環境爲:
./mycat start 啓動

./mycat stop 停止
./mycat console 前臺運行
./mycat restart 重啓服務
./mycat pause 暫停
./mycat status 查看啓動狀態
window 啓動爲:
直接雙擊運行 startup_nowrap.bat ,如果閃退用 cmd 模式運行查看日誌。
8.1.13Mycat 運行 sql 時經常阻塞或卡死是什麼原因?
答: 如果出現執行 sql 語句長時間未返回,或卡死,請檢查是否是虛機下運行或 cpu 爲單核,具體解決方式
請參 考:https://github.com/MyCATApache/Mycat-Server/issues/73,如果仍舊無法解決,可以
暫時跳過,目前有些環境阻塞卡死原因未知。
8.1.14Mycat 中,舊系統數據如何遷移到 Mycat 中?
答:舊數據遷移目前可以手工導入,在 mycat 中提取配置好分配規則及後端分片數據庫,然後通過 dump
或 loaddata 方式導入,後續 Mycat 就做舊數據自動數據遷移工具。
8.1.15Mycat 如何對舊分片數據遷移或擴容,支持自動擴容麼?
答:目前除了一致性 hash 規則分片外其他數據遷移比較困難,目前暫時可以手工遷移,未提供自動遷移方
案,具體遷移方案情況 Mycat 權威指南對應章節。
8.1.16Mycat 支持批量插入嗎?
答:目前 Mycat1.3.0.3 以後支持多 values 的批量插入,如 insert into(xxx) values(xxx),(xxx) 。
8.1.17Mycat 支持多表 Join 嗎?
答:Mycat 目前支持 2 個表 Join,後續會支持多表 Join,具體 Join 請看 Mycat 權威指南對應章節。
8.1.18Mycat 啓動報主機不存在的問題?
答:需要添加 ip 跟主機的映射。
8.1.19Mycat 連接會報無效數據源(Invalid datasource)?

答:例如報錯:mysql> select * from company;
ERROR 3009 (HY000): java.lang.IllegalArgumentException: Invalid DataSource:0
這類錯誤最常見是一些配置問題例如 schema.xml 中的 dataNode 的配置和實際不符合,請先仔細檢查配置
項,確保配置沒有問題。如果不是配置問題,分析具體日誌看出錯原因,常見的有:
如果是應用連:在某些版本的 Mysql 驅動下連接 Mycat 會報錯,可升級最新的驅動包試下。
如果是服務端控制檯連,確認 mysql 是否開啓遠程連接權限,或防火牆是否設置正確,或者數據庫
database 是否配置,或用戶名密碼是否正確。
8.1.20Mycat 使用中如何提需求或 bug?
答:bug 或新需求可以到羣裏提問,同時最好到 github 發起以 isuues:
https://github.com/MyCATApache/Mycat-Server/issues
8.1.21 Mycat 如何建表與創建存儲過程?


答:注意註解中語句是節點的表請替換成自己表如 select 1 from 表 ,查出來的數據在那個節點往哪個節點

存儲過程
/*!mycat: sql=select 1 from 表 */ CREATE DEFINER=`root`@`%` PROCEDURE `proc_test`() BEGIN
END ;
表:
/*!mycat: sql=select 1 from 表 */create table ttt(id int);
8.1.21 Mycat 目前有多少人維護?
答:目前初步統計有 10 人以上核心人員維護。
8.1.22 Mycat 支持的或者不支持的語句有哪些?
答:insert into,複雜子查詢,3 表及其以上跨庫 join 等不支持。
8.1.23 MycatJDBC 連接報 PacketTooBigException 異常
答:檢查 mysqljdbc 驅動的版本,在使用 mycat1.3 和 mycat1.4 版本情況下,不要使用 jdbc5.1.37 和 38
版本的驅動,會出現如下異常報錯:com.mysql.jdbc.PacketTooBigException: Packet for query is too large

(60 > -1). You can change this value on the server by setting the max_allowed_packet' variable。建議使
用 jdbc5.1.35 或者 36 的版本。
8.1.24 Mycat 中文亂碼的問題
答:如果在使用 mycat 出現中文插入或者查詢出現亂碼,請檢查三個環節的字符集設置:1)客戶端環節
(應用程序、mysql 命令或圖形終端工具)連接 mycat 字符集 2)mycat 連接數據庫的字符集 3)數據庫
(mysql,oracle)字符集。這三個環節的字符集如果配置一致,則不會出現中文亂碼,其中尤其需要注意的是客
戶端連接 mycat 時使用的連接字符集,通常的中文亂碼問題一般都由此處設置不當引出。其中 mycat 內部默認使
用 utf8 字符集,在最初啓動連接數據庫時,mycat 會默認使用 utf8 去連接數據庫,當客戶端真正連接 mycat 訪
問數據庫時,mycat 會使用客戶端連接使用的字符集修改它連接數據庫的字符集,在 mycat 環境的管理 9066 端
口,可以通過 show @@backend 命令查看後端數據庫的連接字符集,通過 show @@connection 命令查看前
端客戶端的連接字符集。客戶端的連接可以通過指定字符集編碼或者發送 SET 命令指定連接 mycat 時
connection 使用的字符集,常見客戶端連接指定字符集寫法如下:
1)jdbcUrl=jdbc:mysql://localhost:8066/databaseName? characterEncoding=iso_1
2)SET character_set_client = utf8;用來指定解析客戶端傳遞數據的編碼
SET character_set_results = utf8;用來指定數據庫內部處理時使用的編碼
SET character_set_connection = utf8;用來指定數據返回給客戶端的編碼方式
3) mysql –utest –ptest –P8066 --default-character-set=gbk
8.1.25 Mycat 無法登陸 Access denied
答:Mycat 正常安裝配置完成,登陸 mycat 出現以下錯誤:
[mysql@master ~]$ mysql -utest -ptest -P8066
ERROR 1045 (28000): Access denied for user 'test'@'localhost' (using password: YES)
請檢查在 schema.xml 中的相關 dataHost 的 mysql 主機的登陸權限,一般都是因爲配置的 mysql 的用戶登
陸權限不符合,mysql 用戶權限管理不熟悉的請自己度娘。只有一種情況例外,mycat 和 mysql 主機都部署在同
一臺設備,其中主機 localhost 的權限配置正確,使用-hlocalhost 能正確登陸 mysql 但是無法登陸 mycat 的情
況,請使用-h127.0.0.1 登陸,或者本地網絡實際地址,不要使用-hlocalhost,很多使用者反饋此問題,原因未
明。

8.1.26 Mycat 的分片數據插入報異常 IndexOutofBoundException
答:在一些配置了分片策略的表進行數據插入時報錯,常見的報錯信息如下:
java.lang.IndexOutOfBoundsException:Index:4,size:3 這類報錯通常由於分片策略配置不對引起,請仔細檢查
並理解分片策略的配置,例如:使用固定分片 hash 算法,PartitionByLong 策略,如果 schema.xml 裏面設置的
分片數量 dataNode 和 rule.xml 配置的 partitionCount 分片個數不一致,尤其是出現分片數量 dataNode 小於
partitionCount 數量的情況,插入數據就可能會報錯。很多使用者都沒有仔細理解文檔中對分片策略的說明,用
默認 rule.xml 配置的值,沒有和自己實際使用環境進行參數覈實就進行分片策略使用造成這類問題居多。
8.1.27 Mycat ER 分片子表數據插入報錯
答:一般都是插入子表時出現不能找到父節點的報錯。報錯信息如: [Err] 1064 - can't find (root) parent
sharding node for sql:。此類 ER 表的插入操作不能做爲一個事務進行數據提交,如果父子表在一個事務中進行
提交,顯然在事務沒有提交前子表是無法查到父表的數據的,因此就無法確定 sharding node。如果是 ER 關係
的表在插入數據時不能在同一個事務中提交數據,只能分開提交。
8.1.28 Mycat 最大內存無法調整至 4G 以上
答:mycat1.4 的 JVM 使用最大內存調整如果超過 4G 大小,不能使用 wrapper.java.maxmemory 參數,需
要使用 wrapper.java.additional 的寫法,注意將 wrapper.java.maxmemory 參數註釋,例如增加最大內存至
8G:wrapper.java.additional.10=-Xmx8G。
8.1.29 Mycat 使用過程中報錯怎麼辦
答:記住無論什麼時候遇到報錯,如果不能第一時間理解報錯的原因,首先就去看日誌,無論是啓動
(wrapper.log)還是運行過程中(mycat.log),請相信良好的日誌是編程查錯的終極必殺技。日誌如果記錄信
息不夠,可以調整 conf/log4j.xml 中的 level 級別至 debug,所有的詳細信息均會記錄。另外如果在羣裏面提
問,儘量將環境配置信息和報錯日誌提供清楚,這樣別人才能快速幫你定位問題。

 

9.1 Mycat 性能測試指南

 

Mycat 自身提供了一套基準性能測試工具,這套工具可以用於性能測試、疲勞測試等,包括分片表插入性能
測試、分片表查詢性能測試、更新性能測試、全局表插入性能測試等基準測試工具。
這裏需要說明的一點是,分片表的性能測試不同於普通單表,因爲它的數據是分佈在幾個 Datahost 上的,因
此插入和查詢,都必需要特定的工具,才能做到多個節點同時負載請求,通過觀察每個主機的負載,能夠確定是
否你的測試是合理和正確的。
大量測試表明,當帶寬不是問題而且帶寬沒有佔滿,比如千兆網網絡連接的 Mycat 和 MySQL 服務器,以及
測試客戶端,(通常個人電腦到服務器的連接爲 100M),分片表的性能取決於後端部署 MySQL 的物理機的個
數,比如每個 MySQL 的性能是 5 萬 Tps,則 3 臺理論上是 15 萬,而 Mycat 能達到 80-95%之間,即 12 萬以
上。
關於帶寬問題,是一個比較棘手的問題,通常需要監控交換機、MySQL 服務器、Mycat 服務器、以獲取測試
過程中的端口流量信息,才能確定是否帶寬存在問題,另外,很多企業裏,千兆交換機採用了百兆的普通網線的
情況時有發生,防不勝防,所以,在不能控制的網絡環境裏,測試最大性能的目標通常無法實現。
另外,很多人測試的時候,並不知道 MySQL 直連的性能,因此無法正確比較 Mycat 的性能,所以,建議性
能測試過程裏,首先直連 MySQL 進行性能測試,可以同時直連多個 MYSQL 服務器,然後把測試結果累計,作
爲直連的性能指標,然後改爲連接 Mycat 進行測試,這樣的對比才是有價值的,當插件過大的時候,需要先排除
是否存在 MySQL 冷熱不均的現象,然後考慮 Mycat 性能調優。
測試工具在單獨的包中,解壓到任意機器中執行使用,跟 MyCAT Server 沒有關聯關係,此測試工具很強
大,可以測試任意表,和任意數據庫,測試工具下載:
https://github.com/MyCATApache/Mycat-download 目錄下的 testtool.tar.gz 中。
解壓後,在 bin 目錄裏運行文中的測試腳本。
標準插入性能測試腳本 test_stand_insert_perf.sh 支持任意表的定製化業務數據的隨機生成功能了,在 sql 模板文件中
用${int(1-100)}這種變量,測試程序會隨機生成符合要求的值並插入數據庫。
./test_stand_insert_perf.sh jdbc:mysql://localhost:8066/TESTDB test test 10 file=mydata-create.sql
其中 mydata-create.sql 的內容如下:
total=10000000
sql=insert into my_table1 (….) values ('${date(yyyyMMddHHmmssSSS-[2014-2015]y)}-${int(0-
9999)}ok${int(1111-9999)}xxx ','${char([0-9]2:2)} OPP_${enum(BJ,SH,WU,GZ)}_1',10,${int(10-999)},${int(10-
99)},100,3,15,'${date(yyyyMMddHHmmssSSS-[2014-2015]y}${char([a-f,0-9]8:8)} ',${phone(139-
189)},2,${date(yyyyMMddHH-[2014-2015]y},${date(HHmmssSSS)},${int(100-1000)},'${enum(0000,0001,0002)}')
目前支持的有以下類型變量:
Int:${int(..)} 可以是,${int(10-999)}或者,${int(10,999)}前者表示從 10 到 999 的值,後者表示 10 或者 999
Date:日期如${date(yyyyMMddHHmmssSSS-[2014-2015]y)}表示從 2014 到 2015 年的時間,前面是輸出格式,符
合 Java 標準
Char:字符串${char([0-9]2:2)}表示從 0 到 9 的字符,長度爲 2 位(2:2),}${char([a-f,0-9]8:8)}表示從 a 到 f 以及 0 到
9 的字符串隨機組成,定常爲 8 位。
Enmu:枚舉,表示從指定範圍內獲取一個值,${enum(0000,0001,0002)},裏面可以是任意字符串或數字等內容。
標準查詢性能測試腳本 test_stand_select_perf 也支持 sqlTemplate 的變量方式,查詢任意指定的 sql
./test_stand_select_perf.sh jdbc:mysql://localhost:8066/TESTDB test test 10 100000 file=mysql-select.sql
其中 oppcall-select.sql 的內容類似下面:
sql=select * from mytravelrecord where id = ${int(1-1000000)}
表明查詢 id 爲 1 到 1000000 之間的隨機 SQL。
注意:Windows 下 file=xxx.slq 需要加引號:
test_stand_insert_perf.bat jdbc:mysql://localhost:8066/TESTDB test test 50 "file=oppcall.sql"
首先參考 MyCAT 性能調優,確保整個系統達到最優配置。
性能測試,建議先小規模壓力預熱 10-20 分鐘,這是衆所周知的 Java 的特性,越跑越快。
測試的硬件和網絡條件:
• 建議至少 3 臺服務器;
• MyCAT Server 一臺;
• Mysql 一臺;
• 帶寬應該是至少 100M,建議千兆;
• 壓力程序在另一臺,壓力程序的機器也可以由性能差的機器來代替。
有條件的話,分片庫在不同的 MYSQL 實例上,如 20 個分片,每個 MYSQL 實例 7 個分片,而且最好有多臺
MYSQL 物理機。
分片表的錄入性能測試-T01
測試案例:分片表的併發錄入性能測試,測試 DEMO 中的 travelrecord 表,此表的基準 DDL 語句:create
travelrecord: create table travelrecord (id bigint not null primary key,user_id varchar(100),traveldate
DATE, fee decimal,days int);
此表的標準分片方式爲基於 ID 範圍的自動分片策略。Schema.xml 中配置如下:
<table name="travelrecord" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" />
默認是 3 個分片,分片 ID 範圍定義在 autopartition-long.txt 中,建議修改爲以下或更大的數值範圍分片,
每個分片 500 萬數據
# range start-end ,data node index
0-2000000=0
2000001-4000000=1
4000001-6000000=2
根據自己的情況,可以每個分片放更多的數據,進行對比性能測試,當分片 index 增加時,注意 dataNode
也增加(dataNode=“dn1,dn2,dn3”)。
測試的輸入參數如下[jdbcurl] [user] [password] [threadpoolsize] [recordrange]:
Jdbcurl:連接 mycat 的地址,格式爲 jdbc:mysql://localhost:8066/TESTDB
User 連接 Mycat 的用戶名
Password:密碼
Threadpoolsize:併發線程請求,可以在 50-2000 左右調整,看看哪種情況下的性能最好
Recordrang:插入的分片系列以及對應的 ID 範圍,minId-maxId 然後逗號分開,對應多組分片的 ID 範圍,如 0-
200000,200001-400000,400001-600000,跟分片配置保持一致。
測試過程:
每次測試,建議先執行重建表的操作,以保證測試環境的一致性:
連接 mycat 8066 端口,在命令行執行下面的操作:
drop table travelrecord;
create table travelrecord (id bigint not null primary key,user_id varchar(100),traveldate DATE, fee
decimal,days int);
先預測試:
執行命令:
test_stand_insert_perf jdbc:mysql://localhost:8066/TESTDB test test 100 “0-100M,100M1-200M,200M1-
400”
MyCAT 溫馨提示:併發線程數表明同時至少有多少個 Mysql 連接會被打開,當 SQL 不跨分片的時候,併發線程數
=MYSQL 連接數,在 Mycat conf/schema.xml 中,將 minCon 設置爲>=併發連接數,這種情況下重啓 MYCAT,會
初始建立 minCon 個連接,併發測試結果更好,另外,也可以驗證是否當前內存設置,以及 MYSQL 是否支持開啓這麼
多連接,若無法支持,則 logs/mycat.log 日誌中會有告警錯誤信息,建議測試過程中 tail –f logs/mycat.log 觀察有無
錯誤信息。另外,開啓單獨的 Mycat 管理窗口,mysql –utest –ptest –P9066 然後運行 show @@datasource 可以
看到後端連接的使用情況。Show @@threadpool 可以看線程和 SQL 任務積壓的情況。
也可以同時啓動多個測試程序,在不同的機器上,併發進行測試,每個測試程序寫入一個分片的數據範圍,對於 1 個億的
數據插入測試來說,可能效果更好,畢竟單機併發線程 50 個左右已經差不多極限:
test_stand_insert_perf jdbc:mysql://localhost:8066/TESTDB test test 100 “0-100M”
est_stand_insert_perf jdbc:mysql://localhost:8066/TESTDB test test 100 100M1-200M”
全局表的查詢性能測試 T02:
全局表自動在多個節點上同步插入,因此其插入性能有所降低,這裏的插入表爲 goods 表,執行的命令類似
T01 的測試。溫馨提示:全局表是同時往多個分片上寫數據,因此所需併發 MYSQL 數連接爲普通表的 3 倍,最
好的模式是全局表分別在多個 mysql 實例上。
建表語句:
drop table goods;
create table goods(id int not null primary key,name varchar(200),good_type tinyint,good_img_url
varchar(200),good_created date,good_desc varchar(500), price double);
test_globaltable_insert_perf.bat jdbc:mysql://localhost:8066/TESTDB test test 100 1000000
本機筆記本,4G 內存,數據庫與 Mycat 以及測試程序都在一起,跑出來每秒 1000 多的插入速度:
分片表的查詢性能測試 T03:
此測試可以在 T01 的集成上運行,先生成大量 travelrecord 記錄,然後進行併發隨機查詢,此測試是在分片
庫上,基於分片的主鍵 ID 進行隨機查詢,返回單條記錄,多線程併發隨機執行 N 此記錄查詢,每次查詢的記錄主
鍵 ID 是隨機選擇,在 maxID(參數)範圍之內。
測試工具 test_stand_select_perf 的參數如下
[jdbcurl] [user] [password] [threadpoolsize] [executetimes] [maxId]
Executetimes:每個線程總共執行多少次隨機查詢,建議 1000 次以上
maxId:travelrecord 表的最大 ID,可以執行 select max(id) from travelrecord 來獲取。
Example:
test_stand_select_perf.bat jdbc:mysql://localhost:8066/TESTDB test test 100 10000 50000
分片表的匯聚性能測試 T04:
此測試可以在 T01 的集成上運行,先生成大量 travelrecord 記錄,然後進行併發隨機查詢,此測試執行分片
庫上的聚合、排序、分頁的性能,SQL 如下:
select sum(fee) total_fee, days,count(id),max(fee),min(fee) from travelrecord group by days order by
days desc limit ?
測試工具 test_stand_merge_sel_perf 的參數如下
[
jdbcurl] [user] [password] [threadpoolsize] [executetimes] [limit]
Executetimes:每個線程總共執行多少次隨機查詢,建議 1000 次以上
limit:分頁返回的記錄個數,必須大於 30
Example:
test_stand_merge_sel_perf.bat jdbc:mysql://localhost:8066/TESTDB test test 10 100 100
分片表的更新性能測試 T05:
此測試可以在 T01 的集成上運行,先生成大量 travelrecord 記錄,然後進行併發更新操作,
update travelrecord set user =? ,traveldate=?,fee=?,days=? where id=?
測試工具 test_stand_update_perf 的參數如下
[jdbcurl] [user] [password] [threadpoolsize] [record]
record:總共修改多少條記錄,>5000
Example:
test_stand_update_perf.bat jdbc:mysql://localhost:8066/TESTDB test test 10 10000

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章