前言:
今天遇到主從表不一致的情況,很奇怪為什么會出現不一致的情況,因為復制狀態一直都是正常的。最后檢查出現不一致的數據都是主鍵,原來是當時初始化數據的時候導致的。現在分析記錄下這個問題,避免以后再遇到這個"坑"。
背景:
主從服務器,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" 的坑) 也在一定程度上說明別用自增主鍵當成有意義的數據。