在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單、支付等業務系統中,更需要注意運算元據庫的併發問題。
接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。
原載於我的部落格
在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單、支付等業務系統中,更需要注意運算元據庫的併發問題。 接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。
首先,我們有這樣一張資料表:
mysql> select * from counter;這段**模擬了一次業務操作:+----+-----+
| id | num |
+----+-----+
| 1 | 0 |
+----+-----+
1 row in set (0.00 sec)
<?php上面的**模擬了10個使用者同時併發執行一項業務的情況,每次業務操作都會使得num的值增加1,每個使用者都會執行10000次操作,最終num的值應當是100000。function dummy_business()
mysqli_close($conn);}
for ($i = 0; $i < 10; $i++) elseif (!$pid)
}?>
執行這段**,num的值和我們預期的值是一樣的:
mysql> select * from counter;這裡不會出現問題,是因為單條update語句操作是原子的,無論怎麼執行,num的值最終都會是100000。 然而很多情況下,我們業務過程中執行的邏輯,通常是先查詢再執行,並不像上面的自增那樣簡單:+----+--------+
| id | num |
+----+--------+
| 1 | 100000 |
+----+--------+
1 row in set (0.00 sec)
<?php改過的指令碼,將原來的原子操作update換成了先查詢再更新,再次執行我們發現,由於併發的緣故程式並沒有按我們期望的執行:function dummy_business()
mysqli_close($conn);}
for ($i = 0; $i < 10; $i++) elseif (!$pid)
}?>
mysql> select * from counter;入門程式設計師特別容易犯的錯誤是,認為這是沒開啟事務引起的。現在我們給它加上事務:+----+------+
| id | num |
+----+------+
| 1 | 21495|
+----+------+
1 row in set (0.00 sec)
<?php依然沒能解決問題:function dummy_business() else
} mysqli_close($conn);}
for ($i = 0; $i < 10; $i++) elseif (!$pid)
}?>
mysql> select * from counter;請注意,資料庫事務依照不同的事務隔離級別來保證事務的acid特性,也就是說事務不是一開啟就能解決所有併發問題。通常情況下,這裡的併發操作可能帶來四種問題:+----+------+
| id | num |
+----+------+
| 1 | 16328|
+----+------+
1 row in set (0.00 sec)
通常資料庫有四種不同的事務隔離級別:
隔離級別
髒讀不可重複讀
幻讀read uncommitted√√
√read committed×√
√repeatable read××
√serializable××
×大多數資料庫的預設的事務隔離級別是提交讀(read committed),而mysql的事務隔離級別是重複讀(repeatable read)。對於丟失更新,只有在序列化(serializable)級別才可得到徹底解決。不過對於高效能系統而言,使用序列化級別的事務隔離,可能引起死鎖或者效能的急劇下降。因此使用悲觀鎖和樂觀鎖十分必要。 併發系統中,悲觀鎖(pessimistic locking)和樂觀鎖(optimistic locking)是兩種常用的鎖:
上面的例子,我們用悲觀鎖來實現:
<?php可以看到,這次業務以期望的方式正確執行了:function dummy_business()
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)) else
} mysqli_close($conn);}
for ($i = 0; $i < 10; $i++) elseif (!$pid)
}?>
mysql> select * from counter;由於悲觀鎖在開始讀取時即開始鎖定,因此在併發訪問較大的情況下效能會變差。對mysql inodb來說,通過指定明確主鍵方式查詢資料會單行鎖定,而查詢範圍操作或者非主鍵操作將會鎖表。 接下來,我們看一下如何使用樂觀鎖解決這個問題,首先我們為counter表增加一列字段:+----+--------+
| id | num |
+----+--------+
| 1 | 100000 |
+----+--------+
1 row in set (0.00 sec)
mysql> select * from counter;實現方式如下:+----+------+---------+
| id | num | version |
+----+------+---------+
| 1 | 1000 | 1000 |
+----+------+---------+
1 row in set (0.01 sec)
<?php這次,我們也得到了期望的結果:function dummy_business() else
} mysqli_close($conn);}
for ($i = 0; $i < 10; $i++) elseif (!$pid)
}?>
mysql> select * from counter;由於樂觀鎖最終執行的方式相當於原子化update,因此在效能上要比悲觀鎖好很多。 在使用doctrine orm框架的環境中,doctrine原生提供了對悲觀鎖和樂觀鎖的支援。具體的使用方式請參考手冊:+----+--------+---------+
| id | num | version |
+----+--------+---------+
| 1 | 100000 | 100000 |
+----+--------+---------+
1 row in set (0.01 sec)
hibernate框架中同樣提供了對兩種鎖的支援,在此不再贅述了。 在高效能系統中處理併發問題,受限於後端資料庫,無論何種方式加鎖效能都無法高效處理如電商秒殺搶購量級的業務。使用nosql資料庫、訊息佇列等方式才能更有效地完成業務的處理。
參考文章
PHP使用資料庫的併發問題
摘要 在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單 支付等業務系統中,更需要注意運算元據庫的併發問題。接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。原載於我的部落格 在並行系統中併發問題永遠...
php 資料庫併發,PHP使用資料庫的併發問題
在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單 支付等業務系統中,更需要注意運算元據庫的併發問題。接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。首先,我們有這樣一張資料表 mysql sele...
資料庫的併發問題
a事務讀取了b事務尚未提交的更改資料,並且在這個資料基礎上進行操作。如果此時恰巧b事務進行回滾,那麼a事務讀到的資料是根本不被承認的。以下是乙個取款事務和轉賬事務併發時引起的髒讀場景。時間轉賬事務a 取款事務b t1開始事務 t2開始事務 t3查詢賬戶餘額為1000元 t4取出500元,把餘額改為5...