pdo,mysql 中binlog日誌記錄的一個 bug
最近發現數據庫同步總是出問題,最詭異的時,主從數據庫寫入的數據不一樣,我勒個去。程浩同學看了半天終於找到原因,原來是PDO的一個大坑,加上binlog的一個大坑。
首先聲明,這篇文章有很強的攻擊性,如果你利用這裏麵寫的東西攻擊,所造成的一切後果,自負!
2010/12/15 我的領導,突然要求我們開始折騰一下機器。主要的目的是,沒做備份的,做一下備份,單個的數據庫做主從,線上的機器要做一個能快速恢複的熱備份。經過檢查發現機器若幹台需要整理,於是開始一一處理,其他的還算順利,但發現了一個 數據庫的同步經常有問題,主要問題表現在做好主從同步以後,經過一段時間就會發現重複的插入,引起同步失效。
1、首先考慮數據本身就不一致,造成的同步失效。
處理方法:
2、本以為問題解決,結果在周一,再次發現數據同步失敗。
實在沒辦法了,隻好吧這個庫挪到另外的單獨的一台機器上,單獨觀察,其他剩餘的庫做了同步。
5、 在那個庫挪走以後同步竟然神奇的好了,觀察了一周。
依次查找用到的 apache ,php ,zend,pdo 等。這真是不看不知道,一看嚇一跳!我發現這做運維的網管和程序員那就是一對天敵。看這些東西那叫一個鬱悶。
首先查看 apache ,發現是使用 apache 的 proxy 模塊代理到另一個機器。
到了那個機器我怎麼也找不到訪問的那個路徑,我不懂 php 但配置 apache 不菜啊,咋就沒有呢,這一通折騰才發現人家在那個目錄下寫了一個 .htaccess 又重定向了 !~
這次總算找到那個 php 了,一搜索光 Insert 函數就有 4、5 個。這玩意太多,還是找 mysql_query() 。找了半天沒有~~,仔細一看用的是 PDO
你用 PDO 你就 $var = new PDO(‘mysql:host=xx;dbname=xx;charset=gbk’,'xx’,”); 用吧,一找 new PDO 還沒有。
一點點的找下去發現 人家用的是 Zend_Db::factory(),還搞了一個超複雜的對象。 $config->db->adapter,$config->db->config->toArray() 這裏咬牙、跺腳若幹次。
總算找到正主了,找這個可真不容易: 訪問不直接訪問,用 proxy 弄跑了,弄到另一個機器上也不老老實實的訪問,整一個 302 跳轉了,php 鏈接 mysql 有現成的 mysql_query() 不用,非要調用 PDO ,PDO還不直接調用,要用 Zend 的框架調用。
接下來的事情就是跟蹤 PDO Zend 調用過程,抓包查看交互的數據,根據許許多多的調試信息來看,終於發現了,這個 PDO 處理數據的方式比較特別。
訪問 mysql server 的方式有兩種。
1、 直接訪問模式
2、 預處理模式
先說說這兩個結構的區別,直接訪問就像我們用客戶端連接進數據那樣,標準的 sql 語句插入、更新、刪除和查詢。這個要求就是每個命令裏麵都要指明 表、字段、等信息。
預處理就是:先告訴 mysql 一個表的結構,然後,後麵的全都按照這個表結構來,這樣就不用每次都發送 表、字段等信息了。這樣的優勢是大量的插入會快一點。特點是隻在第一次發送表結構。而不是每次都發送一遍,問題是 mysql binlog 裏麵不支持這種格式。
這兩種方式比較起來,第一種 安全,第二種 快速 。第二種因為沒有表結構,所以當任何一個字段出現問題,就會造成所有的數據問題,而不像第一種,隻影響那一句。那個 PDO 使用的就是第二種方式,而且他錯誤的認為一切都是字符串,把所有的數據都轉換成 16 進製了。
在第一次插入的時候 mysql 使用第二種方式插入數據,但 binlog 裏麵因為沒有這種結構,所以他自己把語句轉換成了 第一種模式,加上了表、及字段信息,但 mysql 不會對 int 形做相應的轉換,(這個在字符串表示中是沒有錯誤的),造成了記錄的日誌是按照字符串的方式記錄的。這樣在吧一個字符串插入 int 形就出現了插入的數據和日誌不一致的情況。要解決這個問題隻有 1、給mysql 寫一個補丁,解決這個問題。(現在功力不夠還寫不出來) 2、在我們公司禁用 預處理結構體方式的數據寫入。看來目前我們隻能使用第二種方法了。
總的來說,pdo 寫的有問題,mysql 的 log 記錄轉換的方式也存在問題。下麵是我寫的一個能夠觸發這個 bug 的代碼。
|
測試過程如下:
|
以上轉自:https://blog.chinaunix.net/uid-8746761-id-2015321.html
下麵是我們的php測試代碼:
<?php $dsn = "mysql:host=localhost;dbname=wanke"; $db = new PDO($dsn, 'wanke', 'wanke'); $db->setAttribute(PDO::ATTR_EMULATE_PREPARES,false); //必須加 $db->exec('SET NAMES gbk'); //必須加 //$sth = $db->prepare("DELETE FROM `atest` WHERE t2=:t1"); //$sth = $db->prepare("INSERT INTO atest (`t1`,`t2`) VALUES(?,?)"); $sth = $db->prepare("INSERT INTO atest (`t1`,`t2`) VALUES(:a,:b)"); //必須是這種形式,不能是問號的 //$sth->bindValue(':t1','666777',PDO::PARAM_STR); //$sth->bindValue(1,67890); //$sth->bindValue(2,'ttttttasdfasttttt'); $sth->bindValue(':a','6784444'); $sth->bindValue(':b','ttttttasdfasttttt'); $count = $sth->execute(); echo $count; $db = null; ?>
另外一種解決方案
主庫使用:binlog_format="ROW" 模式可以避免這種情況的發生,不用修改PDO屬性。mysql 默認的binlog 使用的是 Statement 模式
最後更新:2017-04-03 18:52:08