假設庫存有6隻手機(stock)要賣,總共有8個人要搶購,手機庫存不能小於零,所以只有六個人會買到。把有誰在搶購(action_record)和誰搶購到紀錄(trade_record)到資料表,並更商品新庫存數量。並在交易後執行一些SQL,來確保交易後的SQL可以正常執行。
我在trade資料庫的資料表結構如下,我用下面這三張表做測試:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /*! action_record表是操作紀錄,有連線進來的紀錄都記進去,不受商品庫存影響 */ CREATE TABLE `action_record` ( `id` int (11) NOT NULL AUTO_INCREMENT, ` action ` varchar (255) NOT NULL , `ms` varchar (255) NOT NULL , `new_time` datetime NOT NULL , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*! stock表是商品資料表,記錄了庫存量,stock欄位 */ CREATE TABLE `stock` ( `id` int (11) NOT NULL AUTO_INCREMENT, ` name ` varchar (255) NOT NULL , `stock` int (11) unsigned NOT NULL , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; /*! trade_record表是交易記錄表,有交易成功才記錄 */ CREATE TABLE `trade_record` ( `id` int (11) NOT NULL AUTO_INCREMENT, `user_id` int (11) NOT NULL , `stock_id` int (11) NOT NULL , `action_id` int (11) NOT NULL , `new_time` datetime NOT NULL , PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; |
如何用一道指令模擬同時搶購的行為?
我們寫一個簡單的script來跑,print.sh:
1 2 | #!/usr/bin/bash echo "test:$1 ms:$(($(date +%s%N)/1000000))" >> print.log |
$ echo $(($(date +%s%N)/1000000)) # 取現在的timestamp
PHP取現在的timestamp的millisecond
echo round(microtime(true) * 1000);
有兩種方法
1. pipe line ( 不採用 )
$ bash print.sh 1 | bash print.sh 2 # 跑兩次
print.log:
test:1 ms:1426572647622
test:2 ms:1426572647624
test:2 ms:1426572652053
test:1 ms:1426572652057
用pipe line執行多次print.sh後,在print.log中我們可以看到,pipe line幾乎是同時執行的,有時候甚至第二個命令會比第一個快一點。而非第一個執行完才跑第二個
1.1. 證明pipe line同時執行的其他方法(robert)
1.1.1 先產生一個大一點的log檔(1G以上)
$ cat access_log.log >> tmp2.log => 跑多次
$ ls tmp2.log -lh
-rw-rw-r-- 1 bear bear 1.1G Nov 9 11:45 tmp2.log
1.1.2 使用pipe line,並切換screen 用top或ps檢查
$ cat tmp2.log |awk '{print $1}'|sort -n|uniq -c|sort -n
4372760 192.168.x.147
$ top
或
$ ps aux | egrep -e '(awk|sort|cat)'
bear 14113 19.0 0.0 7224 688 pts/7 S+ 14:32 0:00 cat tmp2.log
bear 14114 75.0 0.0 14896 984 pts/7 S+ 14:32 0:00 awk {print $1}
bear 14115 57.0 0.4 15728 8568 pts/7 R+ 14:32 0:00 sort -n
bear 14117 0.0 0.0 15724 2024 pts/7 S+ 14:32 0:00 sort -n
bear 14119 0.0 0.1 9452 2136 pts/6 S+ 14:32 0:00 egrep --color=auto -e (awk|sort|cat)
可以看到在原本tty結果出來之前,top和ps 都列出 pipe line 後面的 cat, awk, sort都已經在執行了
正規式grep: | egrep -e '(awk|sort|cat)'
2. nohup <command> & ( 採用 )
$ nohup bash print.sh 1 & nohup bash print.sh 2 & #跑兩次
print.log:
test:2 ms:1426573615274
test:1 ms:1426573615275
test:1 ms:1426573627275
test:2 ms:1426573627278
與pipe line一樣是同時執行,有時候甚至第二個命令會比第一個快一點。
模擬交易程式 buy.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | // useage: php buy.php <user_id> $user_id = $argv [1]; $stock_id = 1; exec ( "echo \"user_id:$user_id, start_time_ms:$(($(date +%s%N)/1000000))\" >> print.log" ); mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT); $db = new mysqli( "localhost" , "username" , "password" , "trade" ); $action = array ( 'user_id' => $user_id , "stock_id" => $stock_id ); $action_record_start_id = save_action_record( $db , $action ); $result = $db ->query( "SELECT * FROM `stock` WHERE id = 1" ); $row = $result ->fetch_assoc(); try { echo "try!\n" ; // First of all, let's begin a transaction $db ->autocommit(FALSE); // equal START TRANSACTION or BEGIN // A set of queries; if one fails, an exception should be thrown $db ->query( "INSERT INTO `trade_record` (`id`, `user_id`, `stock_id`, `action_id`, `new_time`) VALUES (NULL, '$user_id', '$stock_id', '$action_record_start_id', NOW());" ); $db ->query( "UPDATE stock SET stock=stock-1 WHERE id=1" ); // If we arrive here, it means that no exception was thrown // i.e. no query has failed, and we can commit the transaction $db ->commit(); } catch (Exception $e ) { // An exception has been thrown // We must rollback the transaction echo "Exception!\n" ; $db ->rollback(); } $db ->autocommit(TRUE); $action = array ( 'action_record_start_id' => $action_record_start_id , 'status' => 'END_TRANSACTION' ); save_action_record( $db , $action ); echo "\n" ; function milliseconds() { $mt = explode ( ' ' , microtime()); return $mt [1] * 1000 + round ( $mt [0] * 1000); } function save_action_record( $db , $action ) { $action_json = json_encode( $action ); $ms = milliseconds(); $sql = "INSERT INTO action_record VALUES (NULL, '$action_json', '$ms', NOW())" ; // echo $sql."\n"; $db ->query( $sql ); return $db ->insert_id; } </user_id> |
讓mysqli會丟出例外( Exceptions )
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
關於交易的四個特性(ACID)
A - Atomicity (原子性、不可分割):交易內的 SQL 指令,不管在任何情況,都只能是全部執行完成,或全部不執行。若是發生無法全部執行完成的狀況,則會回滾(rollback)到完全沒執行時的狀態。
C - Consistency (一致性):交易完成後,必須維持資料的完整性。所有資料必須符合預設的驗證規則、外鍵限制...等。
I - Isolation (隔離性):多個交易可以獨立、同時執行,不會互相干擾。
D - Durability (持久性):交易完成後,異動結果須完整的保留。
開始進入交易模式
START TRANSACTION 或 BEGIN 或 SET AUTOCOMMIT=1
結束交易模式
交易完成:使用 COMMIT 儲存所有變動,並結束交易。
交易過程異常:使用 ROLLBACK 回滾,取消交易,還原到未進行交易的狀態。(交易過程斷線亦會ROLLBACK)
更多的議題,如 儲存點 (SAVEPOINT)、不能 ROLLBACK 的指令、會造成自動終止交易並 COMMIT 的指令、查詢目前 AUTOCOMMIT 的設定值、InnoDB的3種加鎖的類型、InnoDB 實現的鎖定模式、不同 SQL Statements 在 InnoDB 的鎖定、讀取可能產生的異常情況、交易的4種隔離層級、設定交易層級、死結 (dead lock) ... 等更進階議題,請看參考資料中 XYZ的筆記本 的連結
使用pipe line 同時搶購
$ php buy.php 1 | php buy.php 2 | php buy.php 3 | php buy.php 4 | php buy.php 5 | php buy.php 6 | php buy.php 7 | php buy.php 8 #1~8號使用者同時搶
結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | mysql> use trade; Database changed mysql> select * from stock; + ----+-------+-------+ | id | name | stock | + ----+-------+-------+ | 1 | phone | 0 | + ----+-------+-------+ 1 row in set (0.00 sec) mysql> select * from trade_record; + ----+---------+----------+-----------+---------------------+ | id | user_id | stock_id | action_id | new_time | + ----+---------+----------+-----------+---------------------+ | 1 | 4 | 1 | 1 | 2015-03-17 13:58:52 | | 2 | 5 | 1 | 2 | 2015-03-17 13:58:53 | | 3 | 2 | 1 | 5 | 2015-03-17 13:58:53 | | 4 | 3 | 1 | 6 | 2015-03-17 13:58:53 | | 5 | 7 | 1 | 8 | 2015-03-17 13:58:53 | | 7 | 6 | 1 | 4 | 2015-03-17 13:58:53 | + ----+---------+----------+-----------+---------------------+ 6 rows in set (0.00 sec) mysql> select * from action_record; + ----+---------------------------------------------------------+---------------+---------------------+ | id | action | ms | new_time | + ----+---------------------------------------------------------+---------------+---------------------+ | 1 | { "user_id" : "4" , "stock_id" :1} | 1426571932876 | 2015-03-17 13:58:52 | | 2 | { "user_id" : "5" , "stock_id" :1} | 1426571932877 | 2015-03-17 13:58:52 | | 3 | { "user_id" : "1" , "stock_id" :1} | 1426571932878 | 2015-03-17 13:58:52 | | 4 | { "user_id" : "6" , "stock_id" :1} | 1426571932882 | 2015-03-17 13:58:52 | | 5 | { "user_id" : "2" , "stock_id" :1} | 1426571932884 | 2015-03-17 13:58:52 | | 6 | { "user_id" : "3" , "stock_id" :1} | 1426571932886 | 2015-03-17 13:58:52 | | 7 | { "user_id" : "8" , "stock_id" :1} | 1426571932886 | 2015-03-17 13:58:52 | | 8 | { "user_id" : "7" , "stock_id" :1} | 1426571932888 | 2015-03-17 13:58:52 | | 9 | { "action_record_start_id" :1, "status" : "END_TRANSACTION" } | 1426571933040 | 2015-03-17 13:58:53 | | 10 | { "action_record_start_id" :2, "status" : "END_TRANSACTION" } | 1426571933072 | 2015-03-17 13:58:53 | | 11 | { "action_record_start_id" :8, "status" : "END_TRANSACTION" } | 1426571933139 | 2015-03-17 13:58:53 | | 12 | { "action_record_start_id" :5, "status" : "END_TRANSACTION" } | 1426571933172 | 2015-03-17 13:58:53 | | 13 | { "action_record_start_id" :6, "status" : "END_TRANSACTION" } | 1426571933239 | 2015-03-17 13:58:53 | | 14 | { "action_record_start_id" :4, "status" : "END_TRANSACTION" } | 1426571933272 | 2015-03-17 13:58:53 | | 15 | { "action_record_start_id" :7, "status" : "END_TRANSACTION" } | 1426571933338 | 2015-03-17 13:58:53 | + ----+---------------------------------------------------------+---------------+---------------------+ 15 rows in set (0.00 sec) |
交易記錄(trade_record)有六筆資料 - 正常
交易前的操作記錄(action_record)有8筆資料 - 正常
交易後的操作記錄(action_record)有7筆資料 - 異常
經我反覆測試後,"交易後的操作記錄"會有問題,有時候正常,有時候不足8筆
使用nohup <command> & 同時搶購
$ nohup php buy.php 1 & nohup php buy.php 2 & nohup php buy.php 3 & nohup php buy.php 4 & nohup php buy.php 5 & nohup php buy.php 6 & nohup php buy.php 7 & nohup php buy.php 8 &
經多次測試後 "交易後的操作記錄"(action_record)都有8筆資料 - 正常
其他資料表與pipe line方法相同 - 正常
小結:
同時跑多個進程(process)時,還是用nohup比較靠譜一點
範例程式(含table schema):
https://github.com/kalecgos0616/tools/tree/master/transaction
參考資料:
http://stackoverflow.com/questions/2708237/php-mysql-transactions-examples PHP + MySQL transactions examples
http://xyz.cinc.biz/2013/05/mysql-transaction.html XYZ的筆記本 - MySQL 交易功能 Transaction 整理(較詳細,主要參考這篇)
http://stackoverflow.com/questions/12091971/how-to-start-and-end-transaction-in-mysqli How to start and end transaction in mysqli?
http://blog.longwin.com.tw/2006/03/innodb_transaction_2006/ MySQL InnoDB 的 Transaction 的簡易用法
http://stackoverflow.com/questions/16548528/linux-command-to-get-time-in-milliseconds Linux command to get time in milliseconds
http://stackoverflow.com/questions/3656713/how-to-get-current-time-in-milliseconds-in-php How to get current time in milliseconds in PHP?
http://stackoverflow.com/questions/14578243/turning-query-errors-to-exceptions-in-mysqli Turning query errors to Exceptions in MySQLi
http://oss.org.cn/man/database/mysql/inonodb_zh/8.htm InnoDB 事务模式与锁定
沒有留言:
張貼留言