1. 程式人生 > 其它 >ProxySQL(8):SQL語句的重寫規則

ProxySQL(8):SQL語句的重寫規則

文章轉載自: https://www.cnblogs.com/f-ck-need-u/p/9309760.html

為什麼要重寫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的文章中有更多具體的用法。

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。

例如,將下面的語句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        |
+---------+-----------------------+---------------+-----------------+

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正則表示式。