1. 程式人生 > >PHP處理MySQL事務

PHP處理MySQL事務

MySQL5.X都已經發布好久了,但是還有很多人認為MySQL是不支援事務處理的,這不得不怪他們是孤陋寡聞的,其實,只要你的MySQL版本支援BDB或InnoDB表型別,那麼你的MySQL就具有事務處理的能力。這裡面,又以InnoDB表型別用的最多,雖然後來發生了諸如Oracle收購InnoDB等令MySQL不爽的事情,但那些商業上的鬥爭與技術無關,下面以InnoDB表型別為例簡單說一下MySQL中的事務。
 先來明確一下事務涉及的相關知識:
 事務都應該具備ACID特徵。所謂ACID是Atomic(原子性),Consistent(一致性),Isolated(隔離性),Durable(持續性)四個詞的首字母所寫,下面以“銀行轉帳”為例來分別說明一下它們的含義:
 原子性:組成事務處理的語句形成了一個邏輯單元,不能只執行其中的一部分。換句話說,事務是不可分割的最小單元。比如:銀行轉帳過程中,必須同時從一個帳戶減去轉帳金額,並加到另一個帳戶中,只改變一個帳戶是不合理的。
 一致性:在事務處理執行前後,資料庫是一致的。也就是說,事務應該正確的轉換系統狀態。比如:銀行轉帳過程中,要麼轉帳金額從一個帳戶轉入另一個帳戶,要麼兩個帳戶都不變,沒有其他的情況。
 隔離性:一個事務處理對另一個事務處理沒有影響。就是說任何事務都不可能看到一個處在不完整狀態下的事務。比如說,銀行轉帳過程中,在轉帳事務沒有提交之前,另一個轉帳事務只能處於等待狀態。
 持續性:事務處理的效果能夠被永久儲存下來。反過來說,事務應當能夠承受所有的失敗,包括伺服器、程序、通訊以及媒體失敗等等。比如:銀行轉帳過程中,轉帳後帳戶的狀態要能被儲存下來。
 再來看看哪些問題會用到事務處理:
 這裡不說“銀行轉帳”的例子了,說一個大家實際更容易遇到的“網上購書”的例子。先假設一下問題的背景:網上購書,某書(資料庫編號為123)只剩最後一本,而這個時候,兩個使用者對這本書幾乎同時發出了購買請求,讓我們看看整個過程:
 在具體分析之前,先來看看資料表的定義:
create table book
 (
    book_id unsigned int(10) not null auto_increment,
    book_name varchar(100) not null,
    book_price float(5, 2) not null, #我假設每本書的價格不會超過999.99元
    book_number int(10) not null,
    primary key (book_id)
 )
type = innodb; #engine = innodb也行
-------------------------------------------------------------------------------
 對於使用者甲來說,他的動作稍微比乙快一點點,其購買過程所觸發的動作大致是這樣的:
 -------------------------------------------------------------------------------
 1. SELECT book_number FROM book WHERE book_id = 123;
book_number大於零,確認購買行為並更新book_number
 2. UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
 購書成功
 -------------------------------------------------------------------------------
 而對於使用者乙來說,他的動作稍微比甲慢一點點,其購買過程所觸發的動作和甲相同:
 -------------------------------------------------------------------------------
 1. SELECT book_number FROM book WHERE book_id = 123;
 這個時候,甲剛剛進行完第一步的操作,還沒來得及做第二步操作,所以book_number一定大於零
 2. UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
 購書成功
 -------------------------------------------------------------------------------
對於使用者乙來說,他的動作稍微比甲慢一點點,其購買過程所觸發的動作和甲相同:
 -------------------------------------------------------------------------------
 1. SELECT book_number FROM book WHERE book_id = 123;
 這個時候,甲剛剛進行完第一步的操作,還沒來得及做第二步操作,所以book_number一定大於零
 2. UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
 購書成功
 -------------------------------------------------------------------------------
 表面上看甲乙的操作都成功了,他們都買到了書,但是庫存只有一本,他們怎麼可能都成功呢?再看看資料表裡book_number的內容,已經變成“-1”了,這當然是不能允許的(實際上,宣告這樣的列型別應該加上unsigned的屬性,以保證其不能為負,這裡是為了說明問題所以沒有這樣設定)
 好了,問題陳述清楚了,再來看看怎麼利用事務來解決這個問題,開啟MySQL手冊,可以看到想用事務來保護你的SQL正確執行其實很簡單,基本就是三個語句:開始,提交,回滾。
 -------------------------------------------------------------------------------
 開始:START TRANSACTION或BEGIN語句可以開始一項新的事務
 提交:COMMIT可以提交當前事務,是變更成為永久變更
 回滾:ROLLBACK可以回滾當前事務,取消其變更
 此外,SET AUTOCOMMIT = {0 | 1}可以禁用或啟用預設的autocommit模式,用於當前連線。
 -------------------------------------------------------------------------------
 那是不是隻要用事務語句包一下我們的SQL語句就能保證正確了呢?比如下面程式碼:
 -------------------------------------------------------------------------------
 BEGIN;
SELECT book_number FROM book WHERE book_id = 123;
 // ...
UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
COMMIT;
 -------------------------------------------------------------------------------
 答案是否定了,這樣依然不能避免問題的發生,如果想避免這樣的情況,實際應該如下:
 -------------------------------------------------------------------------------
 BEGIN;
SELECT book_number FROM book WHERE book_id = 123 FOR UPDATE;
 // ...
UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
COMMIT;
 -------------------------------------------------------------------------------
 由於加入了FOR UPDATE,所以會在此條記錄上加上一個行鎖,如果此事務沒有完全結束,那麼其他的事務在使用SELECT ... FOR UPDATE請求的時候就會處於等待狀態,直到上一個事務結束,它才能繼續,從而避免了問題的發生,需要注意的是,如果你其他的事務使用的是不帶FOR UPDATE的SELECT語句,將得不到這種保護。
 最後看看PHP + MySQL事務操作的程式碼演示:
 實際LAMP應用中,一般PHP使用AdoDB操作MySQL,下面給出AdoDB相應的程式碼方便大家查閱:
 -------------------------------------------------------------------------------
 <?php 
 // ...
$adodb->startTrans();
 //實際,getOne所呼叫的查詢也可以直接放到rowLock來進行,這裡只是為了演示效果能更明顯些。
$adodb->rowLock('book', 'book_id = 123');
$bookNumber = $adodb->getOne("SELECT book_number FROM book WHERE book_id = 123");
$adodb->execute("UPDATE book SET book_number = book_number - 1 WHERE book_id = 123");
$adodb->completeTrans();
 // ... 
 ?>
 -------------------------------------------------------------------------------
 其中,rowLock的方法就是呼叫的FOR UPDATE來實現的行鎖,你可能會想把“FOR UPDATE”直接寫到$adodb->getOne()呼叫的那條SQL語句裡面去實現行鎖的功能,不錯,那樣確實可以,但是並不是所有的資料庫都使用“FOR UPDATE”語法來實現行鎖功能,比如Sybase使用“HOLDLOCK”的語法來實現行鎖功能,所以為了你的資料庫抽象層保持可移植性,我還是勸你用rowLock來實現行鎖功能,至於可移植性就交給AdoDB好了,嗯,有點扯遠了,今兒就說到這裡了。
 -------------------------------------------------------------------------------
 附:
 AdoDB中存在一個setTransactionMode()方法,能夠設定事務的隔離級別,如下:
 SetTransactionMode allows you to pass in the transaction mode to use for all subsequent transactions for that connection session. Note: if you have persistent connections and using mysql or mssql, you might have to explicitly reset your transaction mode at the beginning of each page request. This is only supported in postgresql, mssql, mysql with InnoDB and oci8 currently. For example:
$db->SetTransactionMode("SERIALIZABLE");
$db->BeginTrans();
$db->Execute(...); $db->Execute(...);
$db->CommiTrans();
$db->SetTransactionMode(""); // restore to default
$db->StartTrans();
$db->Execute(...); $db->Execute(...);
$db->CompleteTrans();
 Supported values to pass in:
    * READ UNCOMMITTED (allows dirty reads, but fastest)
    * READ COMMITTED (default postgres, mssql and oci8)
    * REPEATABLE READ (default mysql)
    * SERIALIZABLE (slowest and most restrictive)
MYSQL的事務處理主要有兩種方法。
 1、用begin,rollback,commit來實現
 begin 開始一個事務
rollback 事務回滾
commit 事務確認
 2、直接用set來改變mysql的自動提交模式
MYSQL預設是自動提交的,也就是你提交一個QUERY,它就直接執行!我們可以通過
 set autocommit=0 禁止自動提交
 set autocommit=1 開啟自動提交
 來實現事務的處理。
 當你用 set autocommit=0 的時候,你以後所有的SQL都將做為事務處理,直到你用commit確認或rollback結束。
 注意當你結束這個事務的同時也開啟了個新的事務!按第一種方法只將當前的作為一個事務!
 個人推薦使用第一種方法!
MYSQL中只有INNODB和BDB型別的資料表才能支援事務處理!其他的型別是不支援的!
 ***:一般MYSQL資料庫預設的引擎是MyISAM,這種引擎不支援事務!如果要讓MYSQL支援事務,可以自己手動修改:
 方法如下:1.修改c:\appserv\mysql\my.ini檔案,找到skip-InnoDB,在前面加上#,後儲存檔案。
 2.在執行中輸入:services.msc,重啟mysql服務。
 3.到phpmyadmin中,mysql->show engines;(或執行mysql->show variables like 'have_%'; ),檢視InnoDB為YES,即表示資料庫支援InnoDB了。
 也就說明支援事務transaction了。
 4.在建立表時,就可以為Storage Engine選擇InnoDB引擎了。如果是以前建立的表,可以使用mysql->alter table table_name type=InnoDB;
 或 mysql->alter table table_name engine=InnoDB;來改變資料表的引擎以支援事務。
 */
/*************** transaction--1 ***************/
$conn = mysql_connect('localhost','root','root') or die ("資料連線錯誤!!!");
mysql_select_db('test',$conn);
mysql_query("set names 'GBK'"); //使用GBK中文編碼;
 //開始一個事務
mysql_query("BEGIN"); //或者mysql_query("START TRANSACTION");
$sql = "INSERT INTO `user` (`id`, `username`, `sex`) VALUES (NULL, 'test1', '0')";
$sql2 = "INSERT INTO `user` (`did`, `username`, `sex`) VALUES (NULL, 'test1', '0')";//這條我故意寫錯
$res = mysql_query($sql);
$res1 = mysql_query($sql2); 
 if($res && $res1){
mysql_query("COMMIT");
echo '提交成功。';
 }else{
mysql_query("ROLLBACK");
echo '資料回滾。';
 }
mysql_query("END"); 
 /**************** transaction--2 *******************/
 /*方法二*/
mysql_query("SET AUTOCOMMIT=0"); //設定mysql不自動提交,需自行用commit語句提交
$sql = "INSERT INTO `user` (`id`, `username`, `sex`) VALUES (NULL, 'test1', '0')";
$sql2 = "INSERT INTO `user` (`did`, `username`, `sex`) VALUES (NULL, 'test1', '0')";//這條我故意寫錯
$res = mysql_query($sql);
$res1 = mysql_query($sql2); 
 if($res && $res1){
mysql_query("COMMIT");
echo '提交成功。';
 }else{
mysql_query("ROLLBACK");
echo '資料回滾。';
 }
mysql_query("END"); //事務處理完時別忘記mysql_query("SET AUTOCOMMIT=1");自動提交
 /******************對於不支援事務的MyISAM引擎資料庫可以使用表鎖定的方法:********************/
 //MyISAM & InnoDB 都支援,
 /*
LOCK TABLES可以鎖定用於當前執行緒的表。如果表被其它執行緒鎖定,則造成堵塞,直到可以獲取所有鎖定為止。
UNLOCK TABLES可以釋放被當前執行緒保持的任何鎖定。當執行緒釋出另一個LOCK TABLES時,或當與伺服器的連線被關閉時,所有由當前執行緒鎖定的表被隱含地解鎖。
*/
mysql_query("LOCK TABLES `user` WRITE");//鎖住`user`表
$sql = "INSERT INTO `user` (`id`, `username`, `sex`) VALUES (NULL, 'test1', '0')";
$res = mysql_query($sql);
 if($res){
echo '提交成功。!';
 }else{
echo '失敗!';
 }
mysql_query("UNLOCK TABLES");//解除鎖定