1.為什麼要重寫SQL語句 ProxySQL在收到前端發送來的SQL語句後,可以根據已定製的規則去匹配它,匹配到了還可以去重寫這個語句,然後再路由到後端去。 什麼時候需要重寫SQL語句? 對於下麵這種簡單的讀、寫分離,當然用不上重寫SQL語句。 這樣的讀寫分離,實現起來非常簡單。如下: 但是,複雜一 ...
1.為什麼要重寫SQL語句
ProxySQL在收到前端發送來的SQL語句後,可以根據已定製的規則去匹配它,匹配到了還可以去重寫這個語句,然後再路由到後端去。
什麼時候需要重寫SQL語句?
對於下麵這種簡單的讀、寫分離,當然用不上重寫SQL語句。
這樣的讀寫分離,實現起來非常簡單。如下:
mysql_replication_hostgroups:
+------------------+------------------+----------+
| writer_hostgroup | reader_hostgroup | comment |
+------------------+------------------+----------+
| 10 | 20 | cluster1 |
+------------------+------------------+----------+
mysql_servers:
+--------------+----------+------+--------+--------+
| hostgroup_id | hostname | port | status | weight |
+--------------+----------+------+--------+--------+
| 10 | master | 3306 | ONLINE | 1 |
| 20 | slave1 | 3306 | ONLINE | 1 |
| 20 | slave2 | 3306 | ONLINE | 1 |
+--------------+----------+------+--------+--------+
mysql_query_rules:
+---------+-----------------------+----------------------+
| rule_id | destination_hostgroup | match_digest |
+---------+-----------------------+----------------------+
| 1 | 10 | ^SELECT.*FOR UPDATE$ |
| 2 | 20 | ^SELECT |
+---------+-----------------------+----------------------+
但是,複雜一點的,例如ProxySQL實現sharding功能。對db1庫的select_1語句路由給hg=10的組,將db2庫的select_2語句路由給hg=20的組,將db3庫的select_3語句路由給hg=30的組。
在ProxySQL實現sharding時,基本上都需要將SQL語句進行重寫。這裡用一個簡單的例子來說明分庫是如何進行的。
假如,電腦學院it_db占用一個資料庫,裡面有一張學生表stu,stu表中有代表專業的欄位zhuanye(例子只是隨便舉的,請無視合理性)。
it_db庫: stu表
+---------+----------+---------+
| stu_id | stu_name | zhuanye |
+---------+----------+---------+
| 1-99 | ... | Linux |
+---------+----------+---------+
| 100-150 | ... | MySQL |
+---------+----------+---------+
| 151-250 | ... | JAVA |
+---------+----------+---------+
| 251-550 | ... | Python |
+---------+----------+---------+
分庫時,可以為各個專業創建庫。於是,創建4個庫,每個庫中仍保留stu表,但只保留和庫名對應的學生數據:
Linux庫:stu表
+---------+----------+---------+
| stu_id | stu_name | zhuanye |
+---------+----------+---------+
| 1-99 | ... | Linux |
+---------+----------+---------+
MySQL庫:stu表
+---------+----------+---------+
| stu_id | stu_name | zhuanye |
+---------+----------+---------+
| 100-150 | ... | MySQL |
+---------+----------+---------+
JAVA庫:stu表
+---------+----------+---------+
| stu_id | stu_name | zhuanye |
+---------+----------+---------+
| 151-250 | ... | JAVA |
+---------+----------+---------+
Python庫:stu表
+---------+----------+---------+
| stu_id | stu_name | zhuanye |
+---------+----------+---------+
| 251-550 | ... | Python |
+---------+----------+---------+
於是,原來查詢MySQL專業學生的SQL語句:
select * from it_db.stu where zhuanye='MySQL' and xxx;
分庫後,該SQL語句需要重寫為:
select * from MySQL.stu where 1=1 and xxx;
至於如何達到上述目標,本文結尾給出了一個參考規則。
sharding而重寫只是一種情況,在很多使用複雜ProxySQL路由規則時可能都需要重寫SQL語句。下麵將簡單介紹ProxySQL的語句重寫,為後文做個鋪墊,在之後介紹ProxySQL + sharding的文章中有更多具體的用法。
2.SQL語句重寫
在mysql_query_rules表中有match_pattern欄位和replace_pattern欄位,前者是匹配SQL語句的正則表達式,後者是匹配成功後(命中規則),將原SQL語句改寫,改寫後再路由給後端。
需要註意幾點:
- 如果不設置replace_pattern欄位,則不會重寫。
- 要重寫SQL語句,必須使用match_pattern的方式做正則匹配,不能使用match_digest。因為match_digest是對參數化後的語句進行匹配。
- ProxySQL支持兩種正則引擎:RE2和PCRE,預設使用的引擎是PCRE。這兩個引擎預設都設置了caseless修飾符(re_modifiers欄位),表示匹配時忽略大小寫。還可以設置其它修飾符,如global修飾符,global修飾符主要用於SQL語句重寫,表示全局替換,而非首次替換。
- 因為SQL語句千變萬化,在寫正則語句的時候,一定要註意"貪婪匹配"和"非貪婪匹配"的問題。
- stats_mysql_query_digest表中的digest_text欄位顯示了替換後的語句。也就是真正路由出去的語句。
本文的替換規則出於入門的目的,很簡單,只需掌握最基本的正則知識即可。但想要靈活運用,需要掌握PCRE的正則,如果您已有正則的基礎,可參考我的一篇總結性文章:pcre和正則表達式的誤點。
例如,將下麵的語句1重寫為語句2。
select * from test1.t1;
select * from test1.t2;
插入如下規則:
delete from mysql_query_rules;
select * from stats_mysql_query_digest_reset where 1=0;
insert into mysql_query_rules(rule_id,active,match_pattern,replace_pattern,destination_hostgroup,apply)
values (1,1,"^(select.*from )test1.t1(.*)","\1test1.t2\2",20,1);
load mysql query rules to runtime;
save mysql query rules to disk;
select rule_id,destination_hostgroup,match_pattern,replace_pattern from mysql_query_rules;
+---------+-----------------------+------------------------------+-----------------+
| rule_id | destination_hostgroup | match_pattern | replace_pattern |
+---------+-----------------------+------------------------------+-----------------+
| 1 | 20 | ^(select.*from )test1.t1(.*) | \1test1.t2\2 |
+---------+-----------------------+------------------------------+-----------------+
然後執行:
$ proc="mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e"
$ $proc "select * from test1.t1;"
+------------------+
| name |
+------------------+
| test1_t2_malong1 |
| test1_t2_malong2 |
| test1_t2_malong3 |
+------------------+
可見語句成功重寫。
再看看規則的狀態。
Admin> select rule_id,hits from stats_mysql_query_rules;
+---------+------+
| rule_id | hits |
+---------+------+
| 1 | 1 |
| 2 | 0 |
+---------+------+
Admin> select hostgroup,count_star,digest_text from stats_mysql_query_digest;
+-----------+------------+------------------------+
| hostgroup | count_star | digest_text |
+-----------+------------+------------------------+
| 20 | 1 | select * from test1.t2 | <--已替換
+-----------+------------+------------------------+
更簡單的,還可以直接替換單詞。例如:
delete from mysql_query_rules;
select * from stats_mysql_query_digest_reset where 1=0;
insert into mysql_query_rules(rule_id,active,match_pattern,replace_pattern,destination_hostgroup,apply)
values (1,1,"test1.t1","test1.t2",20,1);
load mysql query rules to runtime;
save mysql query rules to disk;
select rule_id,destination_hostgroup,match_pattern,replace_pattern from mysql_query_rules;
+---------+-----------------------+---------------+-----------------+
| rule_id | destination_hostgroup | match_pattern | replace_pattern |
+---------+-----------------------+---------------+-----------------+
| 1 | 20 | test1.t1 | test1.t2 |
+---------+-----------------------+---------------+-----------------+
3.sharding:重寫分庫SQL語句
以本文前面sharding示例中的語句為例,簡單演示下sharding時的分庫語句怎麼改寫。更完整的sharding實現方法,見後面的文章。
#原來查詢MySQL專業學生的SQL語句:
select * from it_db.stu where zhuanye='MySQL' and xxx;
|
|
|
\|/
#改寫為查詢分庫MySQL的SQL語句:
select * from MySQL.stu where 1=1 and xxx;
以下是完整語句:關於這個規則中的正則部分,稍後會解釋。
delete from mysql_query_rules;
select * from stats_mysql_query_digest_reset where 1=0;
insert into mysql_query_rules(rule_id,active,apply,destination_hostgroup,match_pattern,replace_pattern)
values (1,1,1,20,"^(select.*?from) it_db\.(.*?) where zhuanye=['""](.*?)['""] (.*)$","\1 \3.\2 where 1=1 \4");
load mysql query rules to runtime;
save mysql query rules to disk;
select rule_id,destination_hostgroup dest_hg,match_pattern,replace_pattern from mysql_query_rules;
+---------+---------+-----------------------------------------------------------------+-----------------------+
| rule_id | dest_hg | match_pattern | replace_pattern |
+---------+---------+-----------------------------------------------------------------+-----------------------+
| 1 | 20 | ^(select.*?from) it_db\.(.*?) where zhuanye=['"](.*?)['"] (.*)$ | \1 \3.\2 where 1=1 \4 |
+---------+---------+-----------------------------------------------------------------+-----------------------+
然後執行分庫查詢語句:
proc="mysql -uroot -pP@ssword1! -h127.0.0.1 -P6033 -e"
$proc "select * from it_db.stu where zhuanye='MySQL' and 1=1;"
看看是否命中規則,併成功改寫SQL語句:
Admin> select rule_id,hits from stats_mysql_query_rules;
+---------+------+
| rule_id | hits |
+---------+------+
| 1 | 1 |
+---------+------+
Admin> select hostgroup,count_star,digest_text from stats_mysql_query_digest;
+-----------+------------+-------------------------------------------+
| hostgroup | count_star | digest_text |
+-----------+------------+-------------------------------------------+
| 20 | 1 | select * from MySQL.stu where ?=? and ?=? |
| 10 | 1 | select @@version_comment limit ? |
+-----------+------------+-------------------------------------------+
解釋下前面的規則:
match_pattern:
- "^(select.*?from) it_db\.(.*?) where zhuanye=['""](.*?)['""] (.*)$"
replace_pattern:
- "\1 \3.\2 where 1=1 \4"
^(select.*?from)
:表示不貪婪匹配到from字元。之所以不貪婪匹配,是為了避免子查詢或join子句出現多個from的情況。
it_db\.(.*?)
:這裡的it_db是稍後要替換掉為"MySQL"字元的部分,而it_db後面的表稍後要附加在"MySQL"字元後,所以對其分組捕獲。
zhuanye=['""](.*?)['""]
:
- 這裡的zhuanye欄位稍後是要刪除的,但後面的欄位值"MySQL"需要保留作為稍後的分庫,因此對欄位值分組捕獲。同時,欄位值前後的引號可能是單引號、雙引號,所以兩種情況都要考慮到。
- ['""]
:要把引號保留下來,需要對額外的引號進行轉義:雙引號轉義後成單個雙引號。所以,真正插入到表中的結果是['"]
。
- 這裡的語句並不健壯,因為如果是zhuanye='MySQL"
這樣單雙引號混用也能被匹配。如果要避免這種問題,需要使用PCRE的反向引用。例如,改寫為:zhuanye=(['""])(.*?)\g[N]
,這裡的[N]
要替換為(['""])
對應的分組號碼,例如\g3
。
(.*)$
:匹配到結束。因為這裡的測試語句簡單,沒有join和子查詢什麼的,所以直接匹配。
"\1 \3.\2 where 1=1 \4"
:這裡加了1=1
,是為了防止出現and/or等運算符時前面缺少表達式。例如(.*)$
捕獲到的內容為and xxx=1
,不加上1=1的話,將替換為where and xxx=1
,這是錯誤的語句,所以1=1是個占位表達式。
可見,要想實現一些複雜的匹配目標,正則表達式是非常繁瑣的。所以,很有必要去掌握PCRE正則表達式。