在php與數(shù)據(jù)庫的交互中,如果并發(fā)量大,并且都去進行數(shù)據(jù)庫的修改的話,就有一個問題需要注意.數(shù)據(jù)的鎖問題.就會牽扯數(shù)據(jù)庫的事務(wù)跟隔離機制
數(shù)據(jù)庫事務(wù)依照不同的事務(wù)隔離級別來保證事務(wù)的ACID特性,也就是說事務(wù)不是一開啟就能解決所有并發(fā)問題。通常情況下,這里的并發(fā)操作可能帶來四種問題:- 更新丟失:一個事務(wù)的更新覆蓋了另一個事務(wù)的更新,這里出現(xiàn)的就是丟失更新的問題。
- 臟讀:一個事務(wù)讀取了另一個事務(wù)未提交的數(shù)據(jù)。
- 不可重復(fù)讀:一個事務(wù)兩次讀取同一個數(shù)據(jù),兩次讀取的數(shù)據(jù)不一致。
- 幻象讀:一個事務(wù)兩次讀取一個范圍的記錄,兩次讀取的記錄數(shù)不一致。
通常數(shù)據(jù)庫有四種不同的事務(wù)隔離級別:隔離級別 | 臟讀 | 不可重復(fù)讀 | 幻讀 | Read uncommitted | √ | √ | √ | Read committed | × | √ | √ | Repeatable read | × | × | √ | Serializable | × | × | × |
大多數(shù)數(shù)據(jù)庫的默認(rèn)的事務(wù)隔離級別是提交讀(Read committed),而MySQL的事務(wù)隔離級別是重復(fù)讀(Repeatable read)。對于丟失更新,只有在序列化(Serializable)級別才可得到徹底解決。不過對于高性能系統(tǒng)而言,使用序列化級別的事務(wù)隔離,可能引起死鎖或者性能的急劇下降。因此使用悲觀鎖和樂觀鎖十分必要。 并發(fā)系統(tǒng)中,悲觀鎖(Pessimistic Locking)和樂觀鎖(Optimistic Locking)是兩種常用的鎖: - 悲觀鎖認(rèn)為,別人訪問正在改變的數(shù)據(jù)的概率是很高的,因此從數(shù)據(jù)開始更改時就將數(shù)據(jù)鎖住,直到更改完成才釋放。悲觀鎖通常由數(shù)據(jù)庫實現(xiàn)(使用SELECT...FOR UPDATE語句)。
- 樂觀鎖認(rèn)為,別人訪問正在改變的數(shù)據(jù)的概率是很低的,因此直到修改完成準(zhǔn)備提交所做的的修改到數(shù)據(jù)庫的時候才會將數(shù)據(jù)鎖住,完成更改后釋放
*** 以mysql為例子: myisam存儲引擎使用表縮 innodb使用行鎖(明確指定了主鍵的情況下,否則也是表鎖)與表鎖
一般的做法是: 1 開啟事務(wù) 2 進行數(shù)據(jù)更改 3 回滾或者提交
在具體的業(yè)務(wù)邏輯中,由于隔離機制的不同,導(dǎo)致結(jié)果的不同. 樂觀鎖與悲觀鎖使用的也比較多.
***有這么一張表
- mysql> select * from counter;
- +----+-----+
- | id | num |
- +----+-----+
- | 1 | 0 |
- +----+-----+
- 1 row in set (0.00 sec)
復(fù)制代碼
悲觀鎖的例子:
- <?php
- function dummy_business() {
- $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());
- mysqli_select_db($conn, 'test');
- for ($i = 0; $i < 10000; $i++) {
- mysqli_query($conn, 'BEGIN');
- $rs = mysqli_query($conn, 'SELECT num FROM counter WHERE id = 1 FOR UPDATE');
- if($rs == false || mysqli_errno($conn)) {
- // 回滾事務(wù)
- mysqli_query($conn, 'ROLLBACK');
- // 重新執(zhí)行本次操作
- $i--;
- continue;
- }
- mysqli_free_result($rs);
- $row = mysqli_fetch_array($rs);
- $num = $row[0];
- mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1 WHERE id = 1');
- if(mysqli_errno($conn)) {
- mysqli_query($conn, 'ROLLBACK');
- } else {
- mysqli_query($conn, 'COMMIT');
- }
- }
- mysqli_close($conn);
- }
-
- for ($i = 0; $i < 10; $i++) {
- $pid = pcntl_fork();
-
- if($pid == -1) {
- die('can not fork.');
- } elseif (!$pid) {
- dummy_business();
- echo 'quit'.$i.PHP_EOL;
- break;
- }
- }
- ?>
復(fù)制代碼 由于悲觀鎖在開始讀取時即開始鎖定,因此在并發(fā)訪問較大的情況下性能會變差。對MySQL Inodb來說,通過指定明確主鍵方式查找數(shù)據(jù)會單行鎖定,而查詢范圍操作或者非主鍵操作將會鎖表。 接下來,我們看一下如何使用樂觀鎖解決這個問題,首先我們?yōu)閏ounter表增加一列字段:
- mysql> select * from counter;
- +----+------+---------+
- | id | num | version |
- +----+------+---------+
- | 1 | 1000 | 1000 |
- +----+------+---------+
- 1 row in set (0.01 sec)
復(fù)制代碼 實現(xiàn)方式:
- <?php
- function dummy_business() {
- $conn = mysqli_connect('127.0.0.1', 'public', 'public') or die(mysqli_error());
- mysqli_select_db($conn, 'test');
- for ($i = 0; $i < 10000; $i++) {
- mysqli_query($conn, 'BEGIN');
- $rs = mysqli_query($conn, 'SELECT num, version FROM counter WHERE id = 1');
- mysqli_free_result($rs);
- $row = mysqli_fetch_array($rs);
- $num = $row[0];
- $version = $row[1];
- mysqli_query($conn, 'UPDATE counter SET num = '.$num.' + 1, version = version + 1 WHERE id = 1 AND version = '.$version);
- $affectRow = mysqli_affected_rows($conn);
- if($affectRow == 0 || mysqli_errno($conn)) {
- // 回滾事務(wù)重新提交
- mysqli_query($conn, 'ROLLBACK');
- $i--;
- continue;
- } else {
- mysqli_query($conn, 'COMMIT');
- }
- }
- mysqli_close($conn);
- }
-
- for ($i = 0; $i < 10; $i++) {
- $pid = pcntl_fork();
-
- if($pid == -1) {
- die('can not fork.');
- } elseif (!$pid) {
- dummy_business();
- echo 'quit'.$i.PHP_EOL;
- break;
- }
- }
- ?>
復(fù)制代碼 由于樂觀鎖最終執(zhí)行的方式相當(dāng)于原子化UPDATE,因此在性能上要比悲觀鎖好很多 |