PHP使用資料庫的併發問題

2022-04-17 07:01:36 字數 4009 閱讀 5501

在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單、支付等業務系統中,更需要注意運算元據庫的併發問題。

接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。

原載於我的部落格

在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單、支付等業務系統中,更需要注意運算元據庫的併發問題。 接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。 

首先,我們有這樣一張資料表:

mysql> select * from counter;

+----+-----+

| id | num |

+----+-----+

|  1 | 0 |

+----+-----+

1 row in set (0.00 sec)

這段**模擬了一次業務操作:

<?php

function dummy_business()

mysqli_close($conn);}

for ($i = 0; $i < 10; $i++) elseif (!$pid)

}?>

上面的**模擬了10個使用者同時併發執行一項業務的情況,每次業務操作都會使得num的值增加1,每個使用者都會執行10000次操作,最終num的值應當是100000。 

執行這段**,num的值和我們預期的值是一樣的:

mysql> select * from counter;

+----+--------+

| id | num  |

+----+--------+

|  1 | 100000 |

+----+--------+

1 row in set (0.00 sec)

這裡不會出現問題,是因為單條update語句操作是原子的,無論怎麼執行,num的值最終都會是100000。 然而很多情況下,我們業務過程中執行的邏輯,通常是先查詢再執行,並不像上面的自增那樣簡單:

<?php

function dummy_business()

mysqli_close($conn);}

for ($i = 0; $i < 10; $i++) elseif (!$pid)

}?>

改過的指令碼,將原來的原子操作update換成了先查詢再更新,再次執行我們發現,由於併發的緣故程式並沒有按我們期望的執行:

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;

+----+------+

| id | num  |

+----+------+

|  1 | 16328|

+----+------+

1 row in set (0.00 sec)

請注意,資料庫事務依照不同的事務隔離級別來保證事務的acid特性,也就是說事務不是一開啟就能解決所有併發問題。通常情況下,這裡的併發操作可能帶來四種問題:

通常資料庫有四種不同的事務隔離級別:

隔離級別

髒讀不可重複讀

幻讀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;

+----+--------+

| id | num  |

+----+--------+

|  1 | 100000 |

+----+--------+

1 row in set (0.00 sec)

由於悲觀鎖在開始讀取時即開始鎖定,因此在併發訪問較大的情況下效能會變差。對mysql inodb來說,通過指定明確主鍵方式查詢資料會單行鎖定,而查詢範圍操作或者非主鍵操作將會鎖表。 接下來,我們看一下如何使用樂觀鎖解決這個問題,首先我們為counter表增加一列字段:

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;

+----+--------+---------+

| id | num | version |

+----+--------+---------+

| 1 | 100000 | 100000 |

+----+--------+---------+

1 row in set (0.01 sec)

由於樂觀鎖最終執行的方式相當於原子化update,因此在效能上要比悲觀鎖好很多。 在使用doctrine orm框架的環境中,doctrine原生提供了對悲觀鎖和樂觀鎖的支援。具體的使用方式請參考手冊:  

hibernate框架中同樣提供了對兩種鎖的支援,在此不再贅述了。 在高效能系統中處理併發問題,受限於後端資料庫,無論何種方式加鎖效能都無法高效處理如電商秒殺搶購量級的業務。使用nosql資料庫、訊息佇列等方式才能更有效地完成業務的處理。 

參考文章

PHP使用資料庫的併發問題

摘要 在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單 支付等業務系統中,更需要注意運算元據庫的併發問題。接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。原載於我的部落格 在並行系統中併發問題永遠...

php 資料庫併發,PHP使用資料庫的併發問題

在並行系統中併發問題永遠不可忽視。儘管php語言原生沒有提供多執行緒機制,那並不意味著所有的操作都是執行緒安全的。尤其是在操作諸如訂單 支付等業務系統中,更需要注意運算元據庫的併發問題。接下來我通過乙個案例分析一下php運算元據庫時併發問題的處理問題。首先,我們有這樣一張資料表 mysql sele...

資料庫的併發問題

a事務讀取了b事務尚未提交的更改資料,並且在這個資料基礎上進行操作。如果此時恰巧b事務進行回滾,那麼a事務讀到的資料是根本不被承認的。以下是乙個取款事務和轉賬事務併發時引起的髒讀場景。時間轉賬事務a 取款事務b t1開始事務 t2開始事務 t3查詢賬戶餘額為1000元 t4取出500元,把餘額改為5...