實現一個小需求,有個test表,表里有個name字段,往表里插入數據,name不能重復,不使用laravel的unique
版本一代碼如下
// routes.php
Route::get('/pessimism', function () {
$name = 'laravel';
$count = DB::table('test')->where('name', $name)->count();
if ($count <= 0) {
DB::table('test')->insert(['name' => $name]);
echo 1;
} else {
echo 0;
}
});
看起來沒毛病,跑起來很順暢,這里我的host是127.0.0.1:85,自行修改,在終端執行
curl http://127.0.0.1:85/pessimism
// 結果打印1
curl http://127.0.0.1:85/pessimism
// 再次執行打印0
乍看沒問題,但是當并發達到一定數量的時候呢,這里使用Apache的ab test來模擬并發,Ubuntu14.04用戶使用命令 sudo apt-get install apache2-utils
安裝即可,然后在終端執行
ab -n 150 -c 100 http://127.0.0.1:85/pessimism
命令解釋:100為并發數,150為請求數,可以理解為有100個人同時去窗口辦理了業務,這樣的操作執行了150次(可能比喻不是很恰當)
返回結果如下
This is ApacheBench, Version 2.3 <$Revision: 1528965 $>
Copyright 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
Licensed to The Apache Software Foundation, http://www.apache.org/
Benchmarking 127.0.0.1 (be patient)
Completed 100 requests
Finished 160 requests
Server Software: nginx
Server Hostname: 127.0.0.1
Server Port: 85
Document Path: /begin
Document Length: 1 bytes
Concurrency Level: 150
Time taken for tests: 21.110 seconds
Complete requests: 160
// 如果返回結果不一樣則結果會有大量的Failed requests
// 讓成功跟失敗返回的HTML的length一致即可,所以示例代碼返回1或0
Failed requests: 0
Total transferred: 99113 bytes
HTML transferred: 160 bytes
Requests per second: 7.58 [#/sec] (mean)
Time per request: 19791.002 [ms] (mean)
Time per request: 131.940 [ms] (mean, across all concurrent requests)
Transfer rate: 4.58 [Kbytes/sec] received
Connection Times (ms)
min mean[+/-sd] median max
Connect: 0 20 6.4 21 26
Processing: 896 10984 5814.6 11055 21078
Waiting: 895 10984 5814.7 11055 21078
Total: 916 11004 5809.7 11076 21093
Percentage of the requests served within a certain time (ms)
50% 11076
66% 13995
75% 16316
80% 16987
90% 18434
95% 19976
98% 20478
99% 21091
100% 21093 (longest request)
執行成功,發現插入了不止一條name為laravel的結果(結果以自己的為準,可能只有1條,或者不止兩條,可以修改name值多試幾次)
為什么會出現這樣的情況?看下圖
時間 | 小明 | 小紅 |
---|---|---|
T1 | 讀取name為laravel的count為1 | |
T2 | 讀取name為laravel的count為1 | |
T3 | insert一條數據 | |
T4 | insert一條數據(事實上這時候count已經為0) |
結果就是不應該插入的數據插入了
解決辦法是,使用悲觀鎖,在小明讀取數據的時候加鎖,小紅這時候可以讀,但是不能操作修改新增刪除,直到小明commit提交事務開鎖
網上對悲觀鎖的解釋是,在讀取數據時鎖住那幾行,其他對這幾行的更新需要等到悲觀鎖結束時才能繼續,這里要結合事務使用,打開兩個終端窗口
窗口一
注意在select語句結尾那里加上了
for update
窗口二
窗口一執行commit提交事務
窗口二
窗口一解鎖了之后窗口二就能繼續操作了,窗口一的select必須包裹在begin跟commit之間才能生效
修改版本一代碼如下
實現一
// 使用transaction閉包實現
Route::get('/pessimism', function () {
DB::transaction(function () {
$name = 'php';
// 注意加上lockForUpdate,否則無效
$count = DB::table('test')->where('name', $name)->lockForUpdate()->count();
if ($count <= 0) {
DB::table('test')->insert(['name' => $name]);
echo 1;
} else {
echo 0;
}
});
});
實現二
Route::get('/pessimism', function () {
DB::beginTransaction();
$name = 'java';
$count = DB::table('test')->where('name', $name)->lockForUpdate()->count();
if ($count <= 0) {
DB::table('test')->insert(['name' => $name]);
echo 1;
} else {
echo 0;
}
DB::commit();
});
------------------------------更新與2019--07-30------------------------------
上面這種寫法出現錯誤不會自動rollback,感謝網友 追夢小窩 的補充,修改的版本如下
Route::get('/pessimism', function () {
DB::beginTransaction();
try {
$name = 'java';
$count = DB::table('test')->where('name', $name)->lockForUpdate()->count();
if ($count <= 0) {
DB::table('test')->insert(['name' => $name]);
echo 1;
} else {
echo 0;
}
// 提交事務
DB::commit();
} catch (\Exception $e) {
// 回滾事務
DB::rollBack();
}
});
最后用ab再測試一下
ab -n 150 -c 100 http://127.0.0.1:85/pessimism
測試多次均未出現重復數據
試想一下這樣的應用場景,抽獎活動只剩下一個獎品n個人同時抽中了這個獎品,如果不做處理很容易出現被多個人領取的情況,當然現實的情況更加復雜,還要結合實際場景做相應的變動修改?。?/p>