一個訂單支付之后,我們需要做下面的步驟:
-
更改訂單的狀態為“已支付”
-
扣減商品庫存
-
給會員增加積分
-
創建銷售出庫單通知倉庫發貨
減庫存的業務實現
減庫存可以采用同步調用(Feign的方式),也可以采用異步調用(RabbitMQ傳遞消息),我們這里采用同步調用,接下來我們分析為什么
如果我們采用異步調用的方式,減庫存的這條消息發送到MQ就不管了,那么到底庫存減成功了沒有呢?這我們並不知道,如果庫存不足,那么我們減庫存失敗,但是service的業務不會回滾,這個問題就是分布式事務問題,即跨服務的事務。減庫存這個業務從訂單微服務跨越到了商品微服務,而事務是由Spring來管理的,兩套tomcat兩套Spring,本身沒有任何關聯,但是卻是一個事務,如果采用異步,這邊的微服務執行失敗另一邊的微服務並不知道,破壞了事務的一致性,我們解決的方案是什么呢?
變異步調用為同步調用,如果一個微服務執行失敗就會拋出異常,事務自然回滾(減庫存的操作只能放在創建訂單業務的最后,因為減庫存執行失敗事務自然回滾訂單也不會創建成功,但是如果上來就先減庫存,那玩意訂單創建失敗庫存無法回滾),但是這種方案也不是最優的,因為我們沒做優惠券功能,當我們做了優惠券功能,那計算優惠和減庫存哪個放在最后呢?哪個放在最后都不可行,這時候就必須解決分布式事務問題了
解決分布式事務問題:
2PC(兩階段提交):
第一階段,事務開始執行發送一條消息給相關的微服務告訴它們這個業務要開始執行,執行完畢后返回一條消息,告訴這個微服務業務執行成功了沒有;
第二階,如果上一階段返回的消息是執行成功,那么再發送一條消息告訴所有微服務事務執行成功了,相關所有事務都可以提交了,如果第一階段有一個微服務執行失敗,則所有事務都回滾
缺點:實現復雜、事務執行過程數據鎖定的范圍太大了,在本業務未執行完畢之前,數據庫相關的表都是鎖定狀態,因此這種處理方式性能較差,在高並發的業務中較少使用。
TCC(try-confirm-cancel):這種處理方式的前提是面對事務都要有一套確認事務執行的業務,一套取消執行的業務(即補償業務)。比如說減庫存這個業務,確認事務就是減庫存,補償事務就是加庫存。這種處理方式時所有業務都開始執行,互相不等待,完成了就提交,解決了兩階段提交問題中數據大面積鎖定的情況,但是如果業務A已經提交了,但是業務B失敗了,沒關系,會調用所有的補償事務,這種解決方案不是靠事務回滾的方式,靠的是事務的補償
缺點:解決了業務問題,但是使得業務變得復雜了,寫一個業務必須寫一個確定執行業務方法和一個補償業務方法,除此之外還要考慮補償方案的失敗問題,當補償方案也執行失敗了呢,這時候就要考慮重試問題、人工介入問題
異步確保:執行時發送一條消息,另一方接受消息,如果執行不成功會一直重試,直到成功
缺點:事務無法回滾,不合適減庫存這個業務
2PC+MQ:兩階段提交方式結合異步確保
綜上,在電商行業中適用的還是TCC,雖然業務變得復雜了,但是行之有效;如果是轉賬業務,適合異步確保,轉賬業務只需要消息可靠就可以,執行時間晚一點也無妨,所以異步確保的關鍵點是消息的可靠
但是在我們這個小項目中,無需把業務變得這么復雜,接下來討論我們采用的同步調用的解決方案。
同步調用中加鎖實現方式:
先查詢庫存,然后if判斷,庫存足夠就減庫存
邏輯是對的,但是這么做有線程上的安全問題,當線程很多的時候,有可能引發超賣問題
加鎖:synchronized
性能太差了,只有一個線程可以執行,當搭了集群時synchronized只鎖住了當前一個tomcat,看起來是可行的,但是在分布式系統下是不安全的
分布式鎖:zookeeper
zookeeper是樹結構,它利用節點的唯一性來實現,加了分布式鎖以后,任何一個邏輯進入到減庫存這個地方,都會創建一個節點,創建成功就認為得到了鎖,繼續執行代碼;反之則失敗,返回或者wait,因此只有一個人可以拿到這個鎖,執行完畢后刪除節點釋放鎖,其他人可以再次創建鎖
zookeeper可以創建臨時節點,當服務器宕機或者斷開連接,會自動刪除節點,自動釋放鎖
缺點:實現起來太復雜
Redis:SETNX命令
原理類似於上述的 節點 ,只能set不存在的key,如果不存在則創建;如果存在它會set失敗,並返回0,拿到鎖以后可以使用del命令釋放鎖
缺點是存在搜索問題,假如SETNX成功,成功之后開始執行代碼,但是此時服務器宕機,那del釋放鎖的命令一直沒有執行,相當於這個鎖一直被拿着,那么這個值將無法再被set成功
但是這里不推薦加鎖實現,因為用了鎖,就變成單線程了,相當於一執行這段代碼就把數據庫鎖死,同一時刻只能有一個人來操作,這樣的實現類似於悲觀鎖,默認線程安全問題一定會發生,在面對高並發時,往往性能很差。
那既然不推薦悲觀鎖,是不是可以采用樂觀鎖呢?樂觀鎖是默認線程安全問題不會發生,不加鎖,但是不加鎖會有線程安全問題,那怎么處理這件事情呢?
——我們不做查詢不做判斷,業務執行到減庫存代碼這里之后直接開始減庫存,唉?這不是會超賣嗎?不要緊,我們的sql內部可以加條件來判斷,失敗則事務回滾,所有人不論怎么操作,最后都會來操作數據庫,但是數據庫寫了判斷語句來判斷庫存,每個人來執行都會被判斷,本質上還是樂觀鎖。如果執行失敗會反饋失敗信息,而不像是悲觀鎖那樣線程阻塞,導致一直等待,性能上來將,這種處理方式優於加鎖,我們的sql語句如下:
"UPDATE tb_stock SET stock = stock - #{num} WHERE sku_id = #{id} AND stock >= #{num}"
高並發場景庫存如何計算
https://blog.csdn.net/u012946310/article/details/82878650
https://www.cnblogs.com/shoshana-kong/p/10889998.html
https://blog.csdn.net/sinat_38570489/article/details/91363054