1、前言
為什么要構建鎖呢?因為構建合適的鎖可以在高并發下能夠保持數據的一致性,即客戶端在執行連貫的命令時上鎖的數據不會被別的客戶端的更改而發生錯誤。同時還能夠保證命令執行的成功率。
看到這里你不禁要問redis中不是有事務操作么?事務操作不能夠實現上面的功能么?
的確,redis中的事務可以watch可以監控數據,從而能夠保證連貫執行的時數據的一致性,但是我們必須清楚的認識到,在多個客戶端同時處理相同的數據的時候,很容易導致事務的執行失敗,甚至會導致數據的出錯。
在關系型數據庫中,用戶首先向數據庫服務器發送BEGIN,然后執行各個相互一致的寫操作和讀操作,最后用戶可以選擇發送COMMIT來確認之前的修改,或者發送ROLLBACK進行回滾。
在redis中,通過特殊的命令MULTI為開始,之后用戶傳入一連貫的命令,最后EXEC為結束(在這一過程中可以使用watch進行監控一些key)。進一步分析,redis事務中的命令會先推入隊列,等到EXEC命令出現的時候才會將一條條命令執行。假若watch監控的key發生改變,這個事務將會失敗。這也就說明Redis事務中不存在鎖,其他客戶端可以修改正在執行事務中的有關數據,這也就為什么在多個客戶端同時處理相同的數據時事務往往會發生錯誤。
2、簡單理解redis的單線程IO多路復用
Redis采用單線程IO多路復用模型來實現高內存數據服務。何為單線程IO多路復用呢?從字面的意思可以知道redis采用的是單線程、使用的是多個IO。整個過程簡單的來講就是,哪個命令的數據流先到達就先執行。
請看下面的形象理解圖:圖中是一座窄橋,只能允許一輛車通過,左邊是車輛進入的通道,哪一輛車先到達就先進入。即哪個IO流先到達就先處理哪個。
Linux下網絡IO使用socket套接字來通訊,普通IO模型只能監聽一個socket,而IO多路復用可同時監控多個socket。IO多路復用避免阻塞在IO上,單線程保存多個socket的狀態后輪循處理。
3、并發測試
我們就模擬一個簡單典型的并發測試,然后從這個測試中得出問題,再進一步研究。
并發測試思路:
1、在redis中設置一個字符串count,運用程序將其取出來加+1,再存儲回去,一直循環十萬次
2、在兩個瀏覽器上同時執行這個代碼
3、將count取出來,查看結果
測試步驟:
1、建立test.php文件
<?php $redis=new Redis(); $redis->connect('192.168.95.11','6379'); for ($i=0; $i < 100000; $i++) { $count=$redis->get('count'); $count=$count+1; $redis->set('count',$count); } echo "this OK"; ?>
2、分別在兩個瀏覽器中訪問test.php文件
結果由上圖可知,總共執行兩次,count原本應該是二十萬才對的,但實際上count等于十三萬多,遠遠小于二十萬,這是為什么呢?
由前面的內容可知,redis是采用單線程IO多路復用模型的。因此我們使用兩個瀏覽器即為兩個會話(A、B),取出、加1、存入這三個命令并不是原子操作,并且在執行取出、存入這兩個redis命令時是哪個客戶端先到就先執行。
例如:
1、此時count=120
2、A取出count=120,緊接著B的取出命令流到了,也將count=120取出
3、A取出后立即加1,并將count=121存回去
4、此時B也緊跟著,也將count=121存進去了
注意:
1、設置循環次數盡量大一點,太小的話,當在第一個瀏覽器執行完畢,第二個瀏覽器還沒開始進行呢
2、必須要兩個瀏覽器同時執行。假若在一個瀏覽器中同時執行兩次test.php文件,不管是否同時執行,最終結果就是count=200000。因為在同一個瀏覽器中執行,都是屬于同一個會話(所有命令都在同一個通道通過),所以redis會讓先執行的十萬次執行完,再接著執行其他的十萬次。
4、事務解決與原子性操作解決
4.1、事務解決
更改后的test.php文件
<?php header("content-type: text/html;charset=utf8;"); $start=time(); $redis=new Redis(); $redis->connect('192.168.95.11','6379'); for ($i=0; $i < 100000; $i++) { $redis->multi(); $count=$redis->get('count'); $count=$count+1; $redis->set('count',$count); $redis->exec(); } $end=time(); echo "this OK<br/>"; echo "執行時間為:".($end-$start); ?>
執行結果失敗,表名使用事務不能夠解決此問題。
分析原因: