MySQL主從復制數據不一致問題【自增主鍵】


前言:

       今天遇到主從表不一致的情況,很奇怪為什么會出現不一致的情況,因為復制狀態一直都是正常的。最后檢查出現不一致的數據都是主鍵,原來是當時初始化數據的時候導致的。現在分析記錄下這個問題,避免以后再遇到這個"坑"。

背景:

      主從服務器,MIXED復制模式。

分析:

      表:SPU

       Table: SPU
Create Table: CREATE TABLE `SPU` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `trademark` varchar(255) NOT NULL COMMENT '品牌',
  `item_code` varchar(255) NOT NULL COMMENT '貨號',
  `product_id` int(10) DEFAULT '0',
  PRIMARY KEY (`id`),
  KEY `trademark` (`trademark`),
  KEY `item_code` (`item_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='SPU'

當時的初始化操作的SQL:

INSERT INTO SPU(trademark, item_code) 
SELECT * FROM 
(
SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
FROM 
    product_property a  LEFT JOIN product_property b 
ON 
    a.product_id = b.product_id 
WHERE 
    a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
AND 
    b.value IS NOT NULL AND b.value <> '' 
) as aa 

      上面的SQL執行完之后,主從表的COUNT數量一樣,但SPU表有個自增主鍵,就在這里出現主從插入SPU的順序不一樣,即主從SELECT出來的結果順序不一樣。由於上面的結果集很大,所以就取前10條記錄看看:

主:

現在表中數據的順序:
zjy@192.168.10.23 : tt 04:38:48>select id,trademark col1,item_code col2 from SPU limit 10;
+----+-----------+-------------+
| id | col1      | col2        |
+----+-----------+-------------+
|  1 | 鼓浪嶼    | gd rg       |
|  2 | 山松      | 10020122    |
|  3 | coulter   ||
|  4 | oricell   | mubmd-01101 |
|  5 | oricell   | huxma-01101 |
|  6 | oricell   | huxmf-01001 |
|  7 | oricell   | rawmx-01001 |
|  8 | oricell   | rasmx-01001 |
|  9 | oricell   | rafmx-01101 |
| 10 | oricell   | rbxmx-01001 |
+----+-----------+-------------+
10 rows in set (0.01 sec)

當時初始化的sql讀出數據的順序:
zjy@192.168.10.23 : tt 04:40:25>SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> ''  limit 10;
+-----------+-------------+
| col1      | col2        |
+-----------+-------------+
| 鼓浪嶼    | gd rg       |
| 山松      | 10020122    |
| coulter   ||
| oricell   | mubmd-01101 |
| oricell   | huxma-01101 |
| oricell   | huxmf-01001 |
| oricell   | rawmx-01001 |
| oricell   | rasmx-01001 |
| oricell   | rafmx-01101 |
| oricell   | rbxmx-01001 |
+-----------+-------------+
10 rows in set (0.01 sec)

上面結果col1,col2的順序一模一樣。

從:

現在表中數據的順序:
zjy@192.168.10.8 : tt 04:38:03>select id,trademark col1,item_code col2 from SPU limit 10;
+----+--------------------------------+-----------------+
| id | col1                           | col2            |
+----+--------------------------------+-----------------+
|  1 | bio-rad                        | 125-0140        |
|  2 | oricell(tm)                    | huxma-90011     |
|  3 | oricell(tm)                    | huxmf-90011     |
|  4 | oricell(tm)                    | rawmx-90011     |
|  5 | oricell                        | rasmx-90011     |
|  6 | oricell                        | rafmx-90011     |
|  7 | oricell                        | rbxmx-90011     |
|  8 | 上海科技有限公司                 | bsm03011        |
|  9 | oricell                        | caxmx-90011     |
| 10 | oricell(tm)                    | tedta-10001-100 |
+----+--------------------------------+-----------------+
10 rows in set (0.00 sec)

當時初始化的sql讀出數據的順序:
zjy@192.168.10.8 : tt 04:38:58>SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> ''  limit 10;
+--------------------------------+-----------------+
| col1                           | col2            |
+--------------------------------+-----------------+
| bio-rad                        | 125-0140        |
| oricell(tm)                    | huxma-90011     |
| oricell(tm)                    | huxmf-90011     |
| oricell(tm)                    | rawmx-90011     |
| oricell                        | rasmx-90011     |
| oricell                        | rafmx-90011     |
| oricell                        | rbxmx-90011     |
| 上海科技有限公司                 | bsm03011        |
| oricell                        | caxmx-90011     |
| oricell(tm)                    | tedta-10001-100 |
+--------------------------------+-----------------+
10 rows in set (0.01 sec)

上面結果col1,col2的順序一模一樣。

       到此為止,大家就清楚為什么SPU表的數據不一致了,准確來說是主鍵對應的數據不一致。要是自增主鍵只是提升INNODB的性能,沒有業務上的意義,那么對於產品來說是沒有影響的,可以忽略這個問題。否則,就需要好好的處理這個問題了。從另一個方面來說,也是因為復制的模式是STATEMENT引發這個問題的,因為同一個QUERY在2個地方執行出的結果不一樣;要是ROW的復制模式,主會把所有字段的記錄全部傳送給從,就不會出現這個問題。

進一步分析: 為什么一樣的SQL在主從上跑出來的數據順序不一樣呢?

      通過EXPLAIN 看到主上的QUERY 先讀 b表,再讀a表;而從上的則是先掃描a表,再讀b表。出現這樣的情況,就是數據在塊里面分布不一致,導致索引利用的方式也不一樣,最終影響優化器的選擇。因為INNODB是索引組織表的,一旦走的索引不一樣,就會導致數據以不同的順序被掃描出來。上面的這些結果剛好被驗證。SQL執行計划如下:

主:先b再a

zjy@192.168.10.23 : tt 09:18:04>explain SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2 FROM      product_property a  LEFT JOIN product_property b  ON      a.product_id = b.product_id  WHERE      a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> ''  AND      b.value IS NOT NULL AND b.value <> '';
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
| id | select_type | table | type | possible_keys          | key         | key_len | ref                  | rows    | Extra                        |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
|  1 | SIMPLE      | b     | ref  | property_id,product_id | property_id | 4       | const                | 4145932 | Using where; Using temporary |
|  1 | SIMPLE      | a     | ref  | property_id,product_id | product_id  | 4       | tt.b.product_id |       1 | Using where                  |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
2 rows in set (0.01 sec)

從:先a再b

zjy@192.168.10.8 : tt 09:18:13>explain SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2 FROM      product_property a  LEFT JOIN product_property b  ON      a.product_id = b.product_id  WHERE      a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> ''  AND      b.value IS NOT NULL AND b.value <> '';
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
| id | select_type | table | type | possible_keys          | key         | key_len | ref                  | rows     | Extra                        |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
|  1 | SIMPLE      | a     | ref  | property_id,product_id | property_id | 4       | const                | 17079464 | Using where; Using temporary |
|  1 | SIMPLE      | b     | ref  | property_id,product_id | product_id  | 4       | tt.a.product_id |        5 | Using where                  |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
2 rows in set (0.13 sec)

主從對比發現,他們的執行計划和各表走的索引都不一樣,導致最后出來的順序也不一樣的(結果集是一樣的),這就驗證了分析說的情況。那要是執行計划和索引一致呢?接下來繼續驗證下:

進一步驗證:

因為INNODB是索引組織表的,索引就是數據,要是主從的執行計划一樣,則他們的結果會是?

主的執行計划:
zjy@192.168.10.23 : tt 05:42:15>explain SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> '';
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
| id | select_type | table | type | possible_keys          | key         | key_len | ref                  | rows    | Extra                        |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
|  1 | SIMPLE      | b     | ref  | property_id,product_id | property_id | 4       | const                | 3771874 | Using where; Using temporary |
|  1 | SIMPLE      | a     | ref  | property_id,product_id | product_id  | 4       | tt.b.product_id |       1 | Using where                  |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+---------+------------------------------+
2 rows in set (0.01 sec)
 從的執行計划:
zjy@192.168.10.8 : tt 05:42:07>explain SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> '';
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
| id | select_type | table | type | possible_keys          | key         | key_len | ref                  | rows     | Extra                        |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
|  1 | SIMPLE      | b     | ref  | property_id,product_id | property_id | 4       | const                | 17375012 | Using where; Using temporary |
|  1 | SIMPLE      | a     | ref  | property_id,product_id | product_id  | 4       | tt.b.product_id |        5 | Using where                  |
+----+-------------+-------+------+------------------------+-------------+---------+----------------------+----------+------------------------------+
2 rows in set (0.00 sec)

執行計划一樣,都是先b表再a表,再重新執行初始化的SQL:

主:

zjy@192.168.10.23 : tt 05:42:26>SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> ''  limit 10
    -> ;
+-----------+-------------+
| col1      | col2        |
+-----------+-------------+
| 鼓浪嶼    | gd rg       |
| 山松      | 10020122    |
| coulter   ||
| oricell   | mubmd-01101 |
| oricell   | huxma-01101 |
| oricell   | huxmf-01001 |
| oricell   | rawmx-01001 |
| oricell   | rasmx-01001 |
| oricell   | rafmx-01101 |
| oricell   | rbxmx-01001 |
+-----------+-------------+
10 rows in set (0.00 sec)

從:

zjy@192.168.10.8 : tt 05:42:32>SELECT distinct lower(TRIM(a.`value`)) col1, lower(TRIM(b.`value`)) col2
    -> FROM 
    ->     product_property a  LEFT JOIN product_property b 
    -> ON 
    ->     a.product_id = b.product_id 
    -> WHERE 
    ->     a.property_id = 14 AND b.property_id = 9 AND a.value IS NOT NULL AND a.value <> '' 
    -> AND 
    ->     b.value IS NOT NULL AND b.value <> ''  limit 10
    -> ;
+-----------+-------------+
| col1      | col2        |
+-----------+-------------+
| 鼓浪嶼    | gd rg       |
| 山松      | 10020122    |
| coulter   ||
| oricell   | mubmd-01101 |
| oricell   | huxma-01101 |
| oricell   | huxmf-01001 |
| oricell   | rawmx-01001 |
| oricell   | rasmx-01001 |
| oricell   | rafmx-01101 |
| oricell   | rbxmx-01001 |
+-----------+-------------+
10 rows in set (0.01 sec)

好了,要是執行計划一樣,結果是:SQL在主從上跑出來的結果一致了。

PS:另一個方法就是用ROW模式,有興趣的可以測試下。

總結:

      主從復制在STATEMENT下面確實被忽略了一些問題,可以用ROW模式代替,但也要知道ROW模式有哪些問題,可以參考MySQL Binlog 【ROW】和【STATEMENT】選擇 。也要清楚數據在磁盤塊里面分布不一致,影響優化器的選擇而導致索引利用的方式也不一樣,最終也影響到數據的順序。

      總之,在主從上執行一些比較大的數據量的操作(批量、初始化)的時候,盡可能的先去主從上查看他們的執行計划是否一樣,走的索引是否一致,確保查詢出來的結果一樣。另:這篇文章:blog.xupeng.me/2013/10/11/mysql-replace-into-trap/(MySQL "replace into" 的坑) 也在一定程度上說明別用自增主鍵當成有意義的數據。
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM