關聯閱讀
《高併發秒殺場景下,基於Redis、LUA防止商品超賣 》
本文就是這篇文章在Spring Boot上的實現。關鍵知識點就是基於RedisTemplate來執行LUA指令碼,從而實現防止商品超賣。
相關需求&說明
一般來說秒殺系統的功能不會很多,有:
1、制定秒殺計劃。在某天幾點開始,售賣什麼商品,準備賣多少個,持續多久。
2、展示秒殺計劃列表。一般都是顯示當天的,8點賣一些,10點賣一些這種。
3、商品詳情頁。
4、下單購買。
等。
本文主要目的還是用程式碼實現一下防止商品超賣的功能,所以像制定秒殺計劃,展示商品等功能就不著重寫了。
還有電商的商品主要是SPU(例如iPhone 12,iPhone 11就是兩個SPU)及SKU(例如iPhone 12 64G 白色,iPhone 12 128G 黑色就是兩個SKU)的處理,展示的是SPU,購買扣庫存的是SKU,本文為了方便,就直接用product來替代了。
下單購買還會有一些前置條件,比如要經過風控系統,確認你是不是黃牛;營銷系統,有沒有相關的優惠券,虛擬貨幣之類的。
下單完成還要走庫管、物流,還有積分之類的,本文就不涉及了。
本文不涉及資料庫,一切都在Redis上操作,不過還是想說一下資料庫與快取資料一致性的問題。
如果我們的系統併發不高,資料庫撐得住,則直接操作資料庫即可,為防止超賣,可以採用:
悲觀鎖
select * from SKU表 where sku_id=1 for update;
或樂觀鎖
update SKU表 set stock=stock-1 where sku_id=1 and update_version=舊版本號;
如果併發高一些,例如商品詳情頁一般併發最高,為了減少資料庫的壓力,都會使用Redis等快取,為了保證資料庫與Redis的一致性,多是採用“修改後刪除”方案。
但是這個方案在更高併發情況下,如C10K、C10M等,在修改資料庫並刪除Redis內容的一瞬間,大量查詢併發會傳導至資料庫,產生異常。
這種情況,SPU詳情這種介面就堅決不能與資料庫連線起來。
步驟應該是:
1、B端管理系統操作資料庫(這個併發不會高)。
2、資料入庫後,傳送訊息給MQ。
3、相關處理程式在接收到訂閱的MQ的Topic後,從資料庫取出資訊,放入Redis。
4、相關服務介面只從Redis取資料。
程式碼實現
在實際專案中,建議將ToC端的秒殺產品相關介面組合為一個微服務,product-server。售賣介面組合為一個微服務,order-server。可以參考之前的Spring Cloud系列文章進行編碼,本文就簡單使用了一個Spring Boot工程。
《SpringCloud2020替換Netflix套件實踐一 》
《SpringCloud2020替換Netflix套件實踐二》
《SpringCloud2020替換Netflix套件實踐三》
《SpringCloud2020替換Netflix套件實踐四 》
秒殺計劃實體類:
省略get/set
public class SecKillPlanEntity implements Serializable { private static final long serialVersionUID = 8866797803960607461L; /** * id */ private Long id; /** * 商品id */ private Long productId; /** * 商品名稱 */ private String productName; /** * 價格 單位:分 */ private Long price; /** * 劃線價 單位:分 */ private Long linePrice; /** * 庫存數 */ private Long stock; /** * 一個使用者只買一件商品標識 0否1是 */ private int buyOneFlag; /** * 計劃狀態 0未提交,1已提交 */ private int planStatus; /** * 開始時間 */ private Date startTime; /** * 結束時間 */ private Date endTime; /** * 建立時間 */ private Date createTime;}
說明:
1、正如前文所說,秒殺的商品應該展示的是SPU,售賣扣庫存的是SKU,本文為了方便,只用product來替代。
2、使用者購買秒殺商品,有兩種方式:
A、一個使用者只允許購買一件。
B、一個使用者可以多次購買多件。
所以本類使用buyOneFlag做標識。
3、planStatus代表本次秒殺是否真正執行。0不展示給C端,不進行售賣;1展示給C端,進行售賣。
新增秒殺計劃&查詢秒殺計劃:
@RestControllerpublic class ProductController { @Resource private RedisTemplate> addSecKillPlan(@RequestParam(“saledate”) String saleDate) { DateTimeFormatter dtf = DateTimeFormatter。ofPattern(“yyyy-MM-dd HH:mm:ss”); Random rand = new Random(); Gson gson = new Gson(); List
> findSecKillPlanByDate(@RequestParam(“saledate”) String saleDate) { Gson gson = new Gson(); String planJson = redisTemplate。opsForValue()。get(“seckill_plan_” + saleDate); List
>() { }。getType()); // 設定新的庫存 for (SecKillPlanEntity entity : list) { if (entity。getBuyOneFlag() == 1) { long newStock = redisTemplate。opsForList()。size(“product_one_stock_” + entity。getProductId()); entity。setStock(newStock); } else { long newStock = Long 。parseLong(redisTemplate。opsForValue()。get(“product_stock_” + entity。getProductId())); entity。setStock(newStock); } } return DefaultResult。success(list); }}
說明:
1、addSecKillPlan就是隨機生成10個售賣計劃,有僅售一件的,也有售多件的。並將相關資料壓入Redis。
seckill_plan_日期,代表某日的所有秒殺計劃,列表展示用。
product_商品ID,代表某商品資訊,詳情頁使用。
product_one_stock_商品ID,代表僅售一件商品的庫存數,值是List,有多少庫存,就往裡面push多少個“1”。
product_buyers_商品ID,代表僅售一件商品的購買者,已購買過的使用者不允許再買。
product_stock_商品ID,代表可售多件商品的庫存數,值是庫存數。
2、findSecKillPlanByDate,展示某日秒殺售賣計劃。庫存數從庫存相關的兩個KEY取。
LUA指令碼:
僅售一件buyone。lua:
——商品庫存Key product_one_stock_XXXlocal stockKey = KEYS[1]——商品購買使用者記錄Key product_buyers_XXXlocal buyersKey = KEYS[2]——使用者IDlocal uid = KEYS[3]——校驗使用者是否已經購買local result=redis。call(“sadd” , buyersKey , uid )if(tonumber(result)==1)then ——沒有購買過,可以購買 local stock=redis。call(“lpop” , stockKey ) ——除了nil和false,其他值都是真(包括0) if(stock) then ——有庫存 return 1 else ——沒有庫存 return -1 endelse ——已經購買過 return -3end
可售多件buymore。lua:
——商品Keylocal key = KEYS[1]——購買數local val = ARGV[1]——現有總庫存local stock = redis。call(“GET”, key)if (tonumber(stock)<=0) then ——沒有庫存 return -1else ——獲取扣減後的總庫存=總庫存-購買數 local decrstock=redis。call(“DECRBY”, key, val) if(tonumber(decrstock)>=0) then ——扣減購買數後沒有超賣,返回現庫存 return decrstock else ——超賣了,把扣減的再加回去 redis。call(“INCRBY”, key, val) return -2 endend
說明:
1、僅售一件。先把購買者的ID用命令“sadd”進product_buyers_商品ID,如果返回1,代表此使用者之前沒有購買過,否則返回-3,已經購買過。
在從product_one_stock_商品ID中lpop出數值,如果還有庫存,必會返回1,有庫存,否則就是nil,無庫存。
2、可售多件。之前講過,不再描述。
將兩個lua檔案,放在Spring Boot工程的resources目錄下。
售賣介面:
@RestControllerpublic class OrderController { @Resource private RedisTemplate
說明:
1、主要看buyOne、buyMore兩個私有方法,裡面寫的是如何使用RedisTemplate執行lua指令碼。
另外我看有資料說如果使用的是Redis叢集,則會報錯,因為我沒有Redis的叢集環境,所以也沒法測試,大家有環境的可以試一試。
2、addOrder有一些程式碼為了節省時間,就寫得很low了,比如一些校驗沒有加,錯誤碼應該使用ENUM等。
測試用例:
1、A使用者購買僅售一件商品1,成功。
2、A使用者再購買僅售一件商品1,失敗。
3、N使用者購買僅售一件商品1,庫存不足。
4、A使用者購買可售多件商品2,成功。
5、A使用者購買可售多件商品2,庫存不足。
本人在本機環境下跑過,沒有問題,結果我就不放了。
牛年的第一篇文章,如果您覺得不錯,想繼續看的話,還請關注、點贊、評論、收藏、轉發[謝謝]。