秒殺的場景有不少,好比:搶購、搶票、搶紅包等等。總之,就是在極短期內有大量的請求。html
咱們都知道,這種系統設計的大方向就是限流,即經過層層過濾,最終只讓相對較少的請求進入到核心業務處理層。mysql
這裏不談秒殺設計,不談使用隊列等使請求串行化,就談下怎麼用鎖來保證數據正確,就是已經到減庫存那一步了,在這一步中若是保證不超賣。sql
用隊列的話,能夠是Java自動的隊列,也能夠用Redis的LPUSH RPOP數據庫
重點是扣減庫存多線程
我理解,主要的方式是加鎖。加鎖有兩個層面:一個是程序層面,另外一個是數據庫層面。併發
這種場景下應該不多有人用Java自帶的鎖(好比:synchronized、Lock)吧,由於它們只在同一個JVM內有效,若是你的應用部署了多臺的話,應該用分佈式鎖。分佈式
關於Redis分佈式鎖,能夠看我以前的一篇《基於Redis的分佈式鎖的簡單實現》測試
其實,這裏加分佈式鎖就是將多線程請求轉成單線程請求,由於每次只有一個線程得到鎖並執行,其他都被阻塞了。優化
這裏有一點須要注意,就是當你應用了事務的話可能會存在問題,請看下面的代碼spa
可能有人會這樣寫,第一眼看起來挺好的,沒問題啊,但仔細實踐證實是由問題的。
咱們知道,mysql默認的事務隔離級別是REPEATABLE-READ
關於事務隔離級別這塊兒,可參考《mysql事務隔離級別》
在這種隔離級別下,同一個事務中屢次讀取,返回的數據是同樣的
同時,Spring聲明式事務默認的傳播特性REQUIRED
Spring聲明式事務是Spring AOP最好的例子,Spring是經過AOP代理的方式來實現事務的,也就是說在調用reduceStock()方法的以前就已經開啓了事務。
那麼,在併發狀況下可能會存在這樣的狀況,假設線程T1和T2都執行到這裏,因而它們都開啓了事務S1和S2,T1先執行,T2後執行,
因爲T2執行的時候事務已經建立了,根據隔離級別,這個時候事務S2讀取不到S1已提交的數據,因而就會出現T1和T2讀取到的值是同樣的,即T2讀取的是T1更新前的庫存數據。
關於這一點,你們能夠本身寫個代碼測試一下,下面是一段參考:
鑑於這種狀況呢,能夠將庫存放到Redis中,咱們直接讀寫Redis,這樣能夠避免受數據庫事務的影響,固然這也會帶來新的問題,再也不討論。
在Java中,一個線程想修改某個變量的值,那麼第一步是將變量的值從主內存中讀取到本身工做內存中,而後修改,最後寫回主內存。這個過程能夠歸結爲:讀取——修改——寫入,在寫回內存的時候可能當前內存中那個值已經發生了變化,這個時候若是繼續寫則會覆蓋別人的數據,只有當內存中的那個值和它修改以前讀到的那個值同樣,才能夠寫入。這個跟數據庫是同樣的。Java中經過Unsafe中compareAndSwapObject這樣的方法類實現的,它直接調用CPU指令。
數據庫中也有CAS,樂觀鎖就是一種CAS
數據增長一個版本標識,通常是經過爲數據庫表增長一個數字類型的 「version」 字段來實現。當讀取數據時,將version字段的值一同讀出,數據每更新一次,對此version值加一。當咱們提交更新的時候,判斷數據庫表對應記錄的當前版本信息與第一次取出來的version值進行比對,若是數據庫表當前版本號與第一次取出來的version值相等,則予以更新,不然認爲是過時數據。
更新的時候帶上版本號,只有當前版本號與更新以前查詢時的版本一致,纔會更新
這裏順便多提一句,CAS中的ABA問題
假設,原先的值是A,線程-1讀取到的值是A,想把它改爲D,可是在此期間,有可能其餘線程已經屢次修改過這個值,只不過最後當線程-1準備將A改爲D的時候,它發現剛好仍是A,覺得沒有人改過,其實這時候的A已經不是原來的A了。
也就是說,儘管修改以前作了比較,固然,仍然會出現以下狀況:
ABA問題致使的緣由,是CAS過程當中只簡單進行了「值」的校驗,有些狀況下,「值」雖然相同,卻已經不是原來的數據了。
CAS不能只比對「值」,還必須確保的是原來的數據,才能修改爲功。
「版本號」的比對,一個數據一個版本,版本變化,即便值相同,也不該該修改爲功。
不只要關注值,還要關注是否是原來的對象
基於「值」的CAS樂觀鎖,可能致使ABA問題。CAS樂觀鎖,必須保證修改時的「此數據」就是「彼數據」,應該由「值」比對,優化爲「版本號」比對。
https://www.sohu.com/a/150900817_178889
https://blog.csdn.net/zhjunjun93/article/details/78560700
https://blog.csdn.net/rexct392358928/article/details/52230737