最近在整理關支付安全的內容,其中就是涉及到了一個在支付過程當中的條件競爭問題。如下都是基於mysql的與php的架構來描述該問題,大佬勿噴。php
什麼是條件競爭:mysql
競爭條件
發生在多個線程同時訪問同一個共享代碼、變量、文件等沒有進行鎖操做或者同步操做的場景中。【Wikipedia-computer_science】sql
後臺代碼實現以下:數據庫
以上是一個購買商品的流程,看似並無什麼問題。後端
若是每次請求都是一個單線程的請求是沒有什麼問題的,可是若是採用多線程的併發請求就會出現問題。安全
由於每次的購買流程都是須要必定的時間去按照購買的流程執行,若是咱們在第一次購買的流程3尚未結束時,就再去執行這一遍流程時,那麼在第流程2時查詢用戶的餘額就仍是初始的餘額,這是由於第一次購買的流程3尚未結束,沒有結束也就意味着餘額沒有扣除,因此金額就是初始的值。markdown
在上面的百年青花瓷購買的案例中,若是咱們只有1000塊錢,可是咱們在多線程的狀況下去購買青花瓷的時候就可能購買到10件以上的數目。多線程
在數據庫查看用戶數據架構
打開購買頁面併發
打開burp攔截購買商品的數據包,點擊購買。
設置intruder發送50個數據包,線程調到25後發包
併發請求後查看購買頁面,已擁有17件,餘額成了-700。
咱們調出mysql的查詢日誌。
從日誌中能夠看到咱們的請求是併發的執行的,在一次查詢尚未結束時就進行了下一次的查詢,因此這也很容易產生兩次查詢的餘額是相同的。
因此當我只剩100塊的只能購買一件商品的時候,可是有可能兩次查詢餘額都100
,是符合購買流程的操做的,後面也會對餘額100進行兩次扣除操做,因此最後餘額變成了負數,購買的數量也大於10。
這種解決的方案的意思是給mysql查詢進行一個事務的處理,在mysql的查詢前添加一個BEGIN
,開始一個事務,在結束時添加一個COMMIT
提交一個事務,完成一個查詢操做。
本地測試一下:
設置好線程再次併發購買一次,結果發現仍是失敗了。並無解決條件競爭帶來的問題,因此解決方案是不行的。
事務是一組原子性sql查詢語句,被看成一個工做單元。若mysql對改事務單元內的全部sql語句都正常的執行完,則事務操做視爲成功,全部的sql語句纔對數據生效,若sql中任意不能執行或出錯則事務操做失敗,全部對數據的操做則無效(經過回滾恢復數據)。
經過上面一句話差很少就知道了緣由,只有在查詢語句不能執行或出錯則事務操做失敗
, 因此咱們添加事務並不能解決mysql競爭的問題,由於咱們的查詢是不會錯誤的,既然不會出錯也就會照樣執行併發的請求。
悲觀併發控制(又名「悲觀鎖」,Pessimistic Concurrency Control,縮寫「PCC」)是一種併發控制的方法。它能夠阻止一個事務以影響其餘用戶的方式來修改數據。若是一個事務執行的操做對某行數據應用了鎖,那只有當這個事務把鎖釋放,其餘事務纔可以執行與該鎖衝突的操做。 悲觀併發控制主要用於數據爭用激烈的環境,以及發生併發衝突時使用鎖保護數據的成本要低於回滾事務的成本的環境中。
當咱們查詢的數據隨時可能會被其餘操做修改時,咱們對這個數據進添加一個悲觀鎖,若是想再次對這個數據進行操做時,只有這條查詢的操做結束後釋放這個悲觀鎖,其餘查詢才能夠對這條數據進行操做,若是鎖定沒有結束時,其餘查詢會一直進行一個等待的狀態。
select * from goods where id = 1 for update;
for update僅適用於InnoDB引擎,且必須在事務塊(BEGIN/COMMIT)中才能生效。在進行事務操做時,經過「for update」語句,MySQL會對查詢結果集中每行數據都添加排他鎖,其餘線程對該記錄的更新與刪除操做都會阻塞。排他鎖包含行鎖、表鎖。
使用悲觀鎖解決上述併發問題:
併發測試:
通過屢次測試後發現商品購買正常。沒有出現條件競爭的問題
咱們這邊來看下後端mysql查詢的日誌
咱們吧mysql的查詢分紅了11組
前七組都沒條件競爭的問題,全部操做都是有序執行的,可是在第八組的時候開始出現問題
在第八條數據查詢的事務尚未結束時就開始查詢第九條的數據了
可是因爲咱們使用了for update(悲觀鎖),對select語句進行鎖定,因此在執行到第九條的時候發現第八條的事務尚未結束,因此他就只能等待第八條的更新完庫存以後執行commit(提交事務)操做,第八條查詢的鎖纔會進行釋放,而後第九條查詢才能獲取到用戶的餘額進行下一步操做。因此經過悲觀鎖解決了條件競爭帶來的問題。
可是悲觀鎖的弊端在每次查詢都會對數據進行鎖定,在高併發的請請求下會變得很慢。因此高併發的請求不建議使用悲觀鎖。
做者:0xchery