前些天公司論壇的一個從數據庫報了一次
Duplicate entry(1062)
錯誤,現在的論壇都搭建了雙從庫,而登錄另一臺從庫看到是同步正常的,這就有些詭異了。起初以為是mysql版本問題導致,但是檢查后發現同步正常的那臺機器的mysql版本跟主庫不一致,而恰恰是出問題的機器與主庫的版本是一致的。出錯的語句是一個合并帖子的UPDATE
操作,由此引出了這個問題,問題最初是順安發現的。
1 問題描述
論壇從Discuz 7.2升級到X3.2,并基于Discuz X3.2版本做了許多的定制和插件開發,用到的MySQL版本為5.1,存儲引擎為MyISAM,binlog_format為STATEMENT。這個問題簡單描述就是:論壇里面有個帖子表,在合并帖子的時候會對帖子的位置信息進行更新,在執行位置更新語句的時候主庫和另一個從庫沒有報錯,而其中一個從庫報錯了。為了方便描述,這里先創建一個測試表post,
CREATE TABLE `post` (
`i` int(11) DEFAULT NULL,
`p` int(11) NOT NULL,
PRIMARY KEY (`p`)
) ENGINE=MyISAM DEFAULT CHARSET=latin1
然后在里面插入幾條數據 insert into post values (1,1), (2,3), (3,4)
。在論壇里面合并帖子的時候會執行一個帖子位置更新的操作 update post set p=p+1;
,這樣我們看到這個操作就報錯了。
mysql> insert into post values(1,1), (2,3),(3,4);
Query OK, 3 rows affected (0.00 sec)
Records: 3 Duplicates: 0 Warnings: 0
mysql> update post set p=p+1;
ERROR 1062 (23000): Duplicate entry '4' for key 'PRIMARY'
mysql> select * from post;
+------+---+
| i | p |
+------+---+
| 1 | 2 |
| 2 | 3 |
| 3 | 4 |
+------+---+
3 rows in set (0.00 sec)
2 解決方案
看到這里相比大家都發現了,這是因為UPDATE的順序問題導致出現了鍵值沖突,因為我們并沒有指定UPDATE的順序,而恰好我們看到的順序是按照p=1, 3, 4
的順序來執行,因此,第一條記錄(1,1)成功更新成了(1,2)
,而執行到p=3
的時候,因為p+1=4與已有的值4沖突報錯。要解決這個問題的方式也很簡單,在更新位置信息時指定順序為DESC
,這樣從大到小執行更新就不會出錯了,update post set p=p+1 order by p desc
(ps:之前很少在UPDATE語句中用ORDER BY 語句,還以為不支持,查了下手冊確認了下是OK的)。MySQL手冊里面還特意提到過這個問題,可能Discuz開發人員沒有注意到這一頁,當然Discuz里面數據庫操作有許多有性能問題的SQL語句,我們已經修過很多BUG了。
If an UPDATE statement includes an ORDER BY clause, the rows are updated in the
order specified by the clause. This can be useful in certain situations that might
otherwise result in an error. Suppose that a table t contains a column id that has
a unique index. The following statement could fail with a duplicate-key error,
depending on the order in which rows are updated:
UPDATE t SET id = id + 1;
For example, if the table contains 1 and 2 in the id column and 1 is updated to 2
before 2 is updated to 3, an error occurs. To avoid this problem, add an ORDER BY
clause to cause the rows with larger id values to be updated before those with
smaller values:
UPDATE t SET id = id + 1 ORDER BY id DESC;
3 幾個問題
-
1)MySQL的SELECT和UPDATE語句的默認順序是怎么樣的?
這個問題mysql手冊中也有提到過,SELECT/UPDATE語句的默認順序跟數據文件以及存儲引擎等相關,在寫SQL語句的時候千萬不要依賴默認順序,如有需要,請加上ORDER BY.
-
2)為什么版本一致,但是主庫沒有報錯而從庫報錯?
這就是第一個問題里面說的,雖然主庫和從庫版本相同,但是由于數據組織不同導致主庫UPDATE的順序跟從庫不同,巧合的避過了重復鍵的錯誤。如果本身主庫和從庫的UPDATE順序一致,那么主庫本身就會只執行部分更新并報錯,從之前的那篇 MySQL binlog格式解析可以知道,這種情況下binlog里面是會記錄一個錯誤碼的,那么從庫在執行的時候雖然也會有一個
Duplicate entry(1062)
錯誤,但是對比binlog的錯誤碼是一致的,所以同步并不會出錯。