您好,登錄后才能下訂單哦!
今天小編給大家分享一下怎么用SpringBoot實現秒殺系統的相關知識點,內容詳細,邏輯清晰,相信大部分人都還太了解這方面的知識,所以分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后有所收獲,下面我們一起來了解一下吧。
一般來說秒殺系統的功能不會很多,有:
制定秒殺計劃。在某天幾點開始,售賣什么商品,準備賣多少個,持續多久。
展示秒殺計劃列表。一般都是顯示當天的,8點賣一些,10點賣一些這種。
商品詳情頁。
下單購買。
等等
本文主要目的還是用代碼實現一下防止商品超賣的功能,所以像制定秒殺計劃,展示商品等功能就不著重寫了。
還有電商的商品主要是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詳情這種接口就堅決不能與數據庫連接起來。
步驟應該是:
B端管理系統操作數據庫(這個并發不會高)。
數據入庫后,發送消息給MQ。
相關處理程序在接收到訂閱的MQ的Topic后,從數據庫取出信息,放入Redis。
相關服務接口只從Redis取數據。
在實際項目中,建議將ToC端的秒殺產品相關接口組合為一個微服務,product-server。售賣接口組合為一個微服務,order-server。
省略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; }
說明:
正如前文所說,秒殺的商品應該展示的是SPU,售賣扣庫存的是SKU,本文為了方便,只用product來替代。
用戶購買秒殺商品,有兩種方式:
一個用戶只允許購買一件。
一個用戶可以多次購買多件。
所以本類使用buyOneFlag做標識。
planStatus代表本次秒殺是否真正執行。0不展示給C端,不進行售賣;1展示給C端,進行售賣。
@RestController public class ProductController { @Resource private RedisTemplate<String, String> redisTemplate; // 隨機生成秒殺計劃設置到Redis中 @GetMapping("/addSecKillPlan") @ResponseBody public DefaultResult<List<SecKillPlanEntity>> addSecKillPlan(@RequestParam("saledate") String saleDate) { DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); Random rand = new Random(); Gson gson = new Gson(); List<SecKillPlanEntity> list = Lists.newArrayList(); for (int i = 0; i < 10; i++) { long productId = rand.nextInt(100) + 1; long price = rand.nextInt(100) + 1; long stock = rand.nextInt(100) + 1; String saleStartTime = " 10:00:00"; String saleEndTime = " 12:00:00"; int buyOneFlag = 0; if (i > 4) { saleStartTime = " 14:00:00"; saleEndTime = " 16:00:00"; buyOneFlag = 1; } SecKillPlanEntity entity = new SecKillPlanEntity(); entity.setId(i + 1L); entity.setProductId(productId); entity.setProductName("商品" + productId); entity.setBuyOneFlag(buyOneFlag); entity.setLinePrice(999999L); entity.setPlanStatus(1); entity.setPrice(price * 100); entity.setStock(stock); entity.setEndTime(Date .from(LocalDateTime.parse(saleDate + saleEndTime, dtf).atZone(ZoneId.systemDefault()).toInstant())); entity.setStartTime(Date.from( LocalDateTime.parse(saleDate + saleStartTime, dtf).atZone(ZoneId.systemDefault()).toInstant())); entity.setCreateTime(new Date()); // 商品詳情寫入Redis ValueOperations<String, String> setProduct = redisTemplate.opsForValue(); setProduct.set("product_" + productId, gson.toJson(entity)); // 寫入庫存 if (buyOneFlag == 1) { // 一個用戶只買一件商品 // 商品購買用戶Set redisTemplate.opsForSet().add("product_buyers_" + productId, ""); // 商品庫存 for (int j = 0; j < stock; j++) { redisTemplate.opsForList().leftPush("product_one_stock_" + productId, "1"); } } else { // 用戶可買多個 redisTemplate.opsForValue().set("product_stock_" + productId, stock + ""); } list.add(entity); System.out.println(gson.toJson(entity)); } redisTemplate.opsForValue().set("seckill_plan_" + saleDate, gson.toJson(list)); return DefaultResult.success(list); } @GetMapping("/findSecKillPlanByDate") @ResponseBody public DefaultResult<List<SecKillPlanEntity>> findSecKillPlanByDate(@RequestParam("saledate") String saleDate) { Gson gson = new Gson(); String planJson = redisTemplate.opsForValue().get("seckill_plan_" + saleDate); List<SecKillPlanEntity> list = gson.fromJson(planJson, new TypeToken<List<SecKillPlanEntity>>() { }.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); } }
說明:
addSecKillPlan就是隨機生成10個售賣計劃,有僅售一件的,也有售多件的。并將相關數據壓入Redis。
seckill_plan_日期,代表某日的所有秒殺計劃,列表展示用。
product_商品ID,代表某商品信息,詳情頁使用。
product_one_stock_商品ID,代表僅售一件商品的庫存數,值是List,有多少庫存,就往里面push多少個“1”。
product_buyers_商品ID,代表僅售一件商品的購買者,已購買過的用戶不允許再買。
product_stock_商品ID,代表可售多件商品的庫存數,值是庫存數。
findSecKillPlanByDate,展示某日秒殺售賣計劃。庫存數從庫存相關的兩個KEY取。
僅售一件buyone.lua:
--商品庫存Key product_one_stock_XXX local stockKey = KEYS[1] --商品購買用戶記錄Key product_buyers_XXX local buyersKey = KEYS[2] --用戶ID local 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 end else --已經購買過 return -3 end
可售多件buymore.lua:
--商品Key local key = KEYS[1] --購買數 local val = ARGV[1] --現有總庫存 local stock = redis.call("GET", key) if (tonumber(stock)<=0) then --沒有庫存 return -1 else --獲取扣減后的總庫存=總庫存-購買數 local decrstock=redis.call("DECRBY", key, val) if(tonumber(decrstock)>=0) then --扣減購買數后沒有超賣,返回現庫存 return decrstock else --超賣了,把扣減的再加回去 redis.call("INCRBY", key, val) return -2 end end
說明:
1、僅售一件。先把購買者的ID用命令“sadd”進product_buyers_商品ID,如果返回1,代表此用戶之前沒有購買過,否則返回-3,已經購買過。
在從product_one_stock_商品ID中lpop出數值,如果還有庫存,必會返回1,有庫存,否則就是nil,無庫存。
【參考文檔】
2.、可售多件。之前講過,不再描述。
將兩個lua文件,放在Spring Boot工程的resources目錄下。
@RestController public class OrderController { @Resource private RedisTemplate<String, String> redisTemplate; @GetMapping("/addOrder") @ResponseBody public DefaultResult<Void> addOrder(@RequestParam("uid") long userId, @RequestParam("pid") long productId, @RequestParam("quantity") int quantity) { Gson gson = new Gson(); String productJson = redisTemplate.opsForValue().get("product_" + productId); SecKillPlanEntity entity = gson.fromJson(productJson, SecKillPlanEntity.class); //TODO 要校驗售賣計劃是否已提交,是否到了售賣時間 long code = 0; if (entity.getBuyOneFlag() == 1) { // 用戶只買一件 code = this.buyOne("product_one_stock_" + productId, "product_buyers_" + productId, userId); } else { // 用戶買多件 code = this.buyMore("product_stock_" + productId, quantity); } DefaultResult<Void> result = DefaultResult.success(null); // 錯誤代碼的處理應該使用ENUM,本文就節省了 if (code < 0) { result.setCode(code); if (code == -1) { result.setMsg("沒有庫存"); } else if (code == -2) { result.setMsg("庫存不足"); } else if (code == -3) { result.setMsg("已經購買過"); } } return result; } private Long buyOne(String stockKey, String buysKey, long userId) { DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>(); defaultRedisScript.setResultType(Long.class); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buyone.lua"))); // "{pre}:" List<String> keys = Lists.newArrayList(stockKey, buysKey, userId + ""); Long result = redisTemplate.execute(defaultRedisScript, keys, ""); return result; } private Long buyMore(String stockKey, int quantity) { DefaultRedisScript<Long> defaultRedisScript = new DefaultRedisScript<Long>(); defaultRedisScript.setResultType(Long.class); defaultRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("buymore.lua"))); List<String> keys = Lists.newArrayList(stockKey); Long result = redisTemplate.execute(defaultRedisScript, keys, quantity+""); return result; } }
說明:
1、主要看buyOne、buyMore兩個私有方法,里面寫的是如何使用RedisTemplate執行lua腳本。
另外我看有資料說如果使用的是Redis集群,則會報錯,因為我沒有Redis的集群環境,所以也沒法測試,大家有環境的可以試一試。
2、addOrder有一些代碼為了節省時間,就寫得很low了,比如一些校驗沒有加,錯誤碼應該使用ENUM等。
測試用例:
A用戶購買僅售一件商品1,成功。
A用戶再購買僅售一件商品1,失敗。
N用戶購買僅售一件商品1,庫存不足。
A用戶購買可售多件商品2,成功。
A用戶購買可售多件商品2,庫存不足。
以上就是“怎么用SpringBoot實現秒殺系統”這篇文章的所有內容,感謝各位的閱讀!相信大家閱讀完這篇文章都有很大的收獲,小編每天都會為大家更新不同的知識,如果還想學習更多的知識,請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。