日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率

發布時間:2025/3/20 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

在前兩篇文章的介紹下,我們完成了防止超賣商品和搶購接口的限流,已經能夠防止大流量把我們的服務器直接搞炸,這篇文章中,我們要開始關心一些細節問題。對于稍微懂點電腦的,點擊F12打開瀏覽器的控制臺,就能在點擊搶購按鈕后,獲取我們搶購接口的鏈接(手機APP等其他客戶端可以抓包來拿到)。一旦拿到了搶購的鏈接,只要稍微寫點爬蟲代碼,模擬一個搶購請求,就可以不通過點擊下單按鈕,直接在代碼中請求我們的接口,完成下單。他們只需要在搶購時刻的0毫秒,開始不間斷發起大量請求,絕對比大家在APP上點搶購按鈕要快,畢竟人的速度有限,更別說APP說不定還要經過幾層前端驗證才會真正發出請求。本篇我們從兩個方面對所出現的問題來進行相關限制:搶購接口隱藏和單用戶限制頻率。

搶購接口隱藏

如果在秒殺活動活動開始前不知道具體的接口,那是不是就不能發起請求了?所以我們可以將搶購接口進行隱藏,搶購接口隱藏(接口加鹽)的具體做法:

  • 每次點擊秒殺按鈕,先從服務器獲取一個秒殺驗證值(接口內判斷是否到秒殺時間);
  • Redis以緩存用戶ID和商品ID為Key,秒殺地址為Value緩存驗證值;
  • 用戶請求秒殺商品的時候,要帶上秒殺驗證值進行校驗。
  • 其實這種方式可以防住的是直接請求接口的人,但是只要把腳本寫復雜一點,先去請求一個驗證值,再立刻請求搶購,也是能夠搶購成功的。不過請求驗證值接口,也需要在搶購時間開始后,才能請求接口拿到驗證值,然后才能申請搶購接口。理論上來說在訪問接口的時間上受到了限制,并且我們還能通過在驗證值接口增加更復雜的邏輯,讓獲取驗證值的接口并不快速返回驗證值,進一步拉平普通用戶和惡意請求的下單時刻。所以接口加鹽還是有用的!下面我們就實現一種簡單的加鹽接口代碼,拋磚引玉。

    接口加鹽實現

    代碼還是使用之前的項目,我們在其上面增加兩個接口:獲取驗證值接口和攜帶驗證值下單接口。之前我們只有兩個表,一個stock表放庫存商品,一個stockOrder訂單表,放訂購成功的記錄。但是這次涉及到了用戶,所以我們新增用戶表,并且添加一個用戶王二。并且在訂單表中,不僅要記錄商品id,同時要寫入用戶id。

    創建SQL語句

    -- ---------------------------- -- Table structure for stock -- ---------------------------- DROP TABLE IF EXISTS `stock`; CREATE TABLE `stock` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL DEFAULT '' COMMENT '名稱',`count` int(11) NOT NULL COMMENT '庫存',`sale` int(11) NOT NULL COMMENT '已售',`version` int(11) NOT NULL COMMENT '樂觀鎖,版本號',PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ---------------------------- -- Records of stock -- ---------------------------- INSERT INTO `stock` VALUES ('1', 'iphone', '50', '0', '0'); INSERT INTO `stock` VALUES ('2', 'mac', '10', '0', '0');-- ---------------------------- -- Table structure for stock_order -- ---------------------------- DROP TABLE IF EXISTS `stock_order`; CREATE TABLE `stock_order` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,`sid` int(11) NOT NULL COMMENT '庫存ID',`name` varchar(30) NOT NULL DEFAULT '' COMMENT '商品名稱',`user_id` int(11) NOT NULL DEFAULT '0',`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '創建時間',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;-- ---------------------------- -- Records of stock_order -- ------------------------------ ---------------------------- -- Table structure for user -- ---------------------------- DROP TABLE IF EXISTS `user`; CREATE TABLE `user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_name` varchar(255) NOT NULL DEFAULT '',PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;-- ---------------------------- -- Records of user -- ---------------------------- INSERT INTO `user` VALUES ('1', '王二');

    獲取驗證值接口

    該接口要求傳用戶id和商品id,返回驗證值。我們在Controller中添加方法:

    /*** 獲取驗證值* @return*/ @RequestMapping(value = "/getVerifyHash", method = {RequestMethod.GET}) @ResponseBody public String getVerifyHash(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId) {String hash;try {hash = userService.getVerifyHash(sid, userId);} catch (Exception e) {LOGGER.error("獲取驗證hash失敗,原因:[{}]", e.getMessage());return "獲取驗證hash失敗";}return String.format("請求搶購驗證hash值為:%s", hash); }

    在UserService中添加方法:

    @Override public String getVerifyHash(Integer sid, Integer userId) throws Exception {// 驗證是否在搶購時間內LOGGER.info("請自行驗證是否在搶購時間內");// 檢查用戶合法性User user = userMapper.selectByPrimaryKey(userId.longValue());if (user == null) {throw new Exception("用戶不存在");}LOGGER.info("用戶信息:[{}]", user.toString());// 檢查商品合法性Stock stock = stockService.getStockById(sid);if (stock == null) {throw new Exception("商品不存在");}LOGGER.info("商品信息:[{}]", stock.toString());// 生成hashString verify = SALT + sid + userId;String verifyHash = DigestUtils.md5DigestAsHex(verify.getBytes());// 將hash和用戶商品信息存入redisString hashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId;stringRedisTemplate.opsForValue().set(hashKey, verifyHash, 3600, TimeUnit.SECONDS);LOGGER.info("Redis寫入:[{}] [{}]", hashKey, verifyHash);return verifyHash; }

    Redis的Cache常量枚舉類CacheKey如下:

    public enum CacheKey {HASH_KEY("miaosha_v1_user_hash"),LIMIT_KEY("miaosha_v1_user_limit");private String key;private CacheKey(String key) {this.key = key;}public String getKey() {return key;} }

    代碼整體來說比較簡單,拿到用戶id和商品id后,檢查商品和用戶信息是否在表中存在,并且會驗證現在的時間(我這里為了簡化,只是寫了一行LOGGER,大家可以根據需求自行實現)。在這樣的條件過濾下,才會給出hash值,并且將Hash值寫入了Redis中,緩存3600秒(1小時),如果用戶拿到這個hash值一小時內沒下單,則需要重新獲取hash值。
    這里有一個問題需要大家思考一下:為什么verify 除了使用商品id和用戶id還要額外加一個SALT 再使用MD5加密得到verifyHash ?

    String verify = SALT + sid + userId;

    其實用戶id并不一定是用戶不知道的(就比如我這種用自增id存儲的,肯定不安全),而商品id,萬一也泄露了出去,那么別人就可以通過md5直接就hash算出來。隨意這里給前面加了前綴,也就是一個salt(鹽),相當于給這個固定的字符串撒了一把鹽,寫死在了代碼里。這樣只要不猜到這個鹽,就沒辦法算出來verifyHash值。當然,我這里只是其中一種方式,大家在使用的時候可以依據自己的方式,比如可以結合時間戳等來保證verifyHash不容易被計算出來。

    攜帶驗證值下單接口

    用戶在前臺拿到了驗證值后,點擊下單按鈕,前端攜帶著特征值,即可進行下單操作。Controller中添加要求驗證的搶購接口:

    @RequestMapping(value = "/createOrderWithVerifiedUrl", method = {RequestMethod.GET}) @ResponseBody public String createOrderWithVerifiedUrl(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId,@RequestParam(value = "verifyHash") String verifyHash) {int stockLeft;try {stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash);LOGGER.info("購買成功,剩余庫存為: [{}]", stockLeft);} catch (Exception e) {LOGGER.error("購買失敗:[{}]", e.getMessage());return e.getMessage();}return String.format("購買成功,剩余庫存為:%d", stockLeft); }

    OrderService中添加方法:

    @Override public int createVerifiedOrder(Integer sid, Integer userId, String verifyHash) throws Exception {// 驗證是否在搶購時間內LOGGER.info("請自行驗證是否在搶購時間內,假設此處驗證成功");// 驗證hash值合法性String hashKey = CacheKey.HASH_KEY.getKey() + "_" + sid + "_" + userId;String verifyHashInRedis = stringRedisTemplate.opsForValue().get(hashKey);if (!verifyHash.equals(verifyHashInRedis)) {throw new Exception("hash值與Redis中不符合");}LOGGER.info("驗證hash值合法性成功");// 檢查用戶合法性User user = userMapper.selectByPrimaryKey(userId.longValue());if (user == null) {throw new Exception("用戶不存在");}LOGGER.info("用戶信息驗證成功:[{}]", user.toString());// 檢查商品合法性Stock stock = stockService.getStockById(sid);if (stock == null) {throw new Exception("商品不存在");}LOGGER.info("商品信息驗證成功:[{}]", stock.toString());//樂觀鎖更新庫存saleStockOptimistic(stock);LOGGER.info("樂觀鎖更新庫存成功");//創建訂單createOrderWithUserInfo(stock, userId);LOGGER.info("創建訂單成功");return stock.getCount() - (stock.getSale()+1); }

    在上面的方法中,我們驗證了時間,驗證值匹配,用戶信息,商品信息和庫存。這樣一個簡單的帶有驗證的下單接口就完成了。接下里讓我們來實際進行調用看一下情況。

    接口測試

    我們使用postman調用getVerifyHash獲取請求搶購驗證hash值:

    我們去redis看一下這個值已經存在其中:

    同時在控制臺看出各種驗證都是通過的:

    有了這個驗證值,接下來進行下單操作看是否可以下單成功:


    可以看出我們去拿不驗證通過并且成功下單。

    單用戶限制頻率時間

    假設我們做好了接口隱藏,但是像我上面說的,總有無聊的人會寫一個復雜的腳本,先請求hash值,再立刻請求購買,如果你的app下單按鈕做的很差,大家都要開搶后0.5秒才能請求成功,那可能會讓腳本依然能夠在大家前面搶購成功。這個時候我們需要在做一個額外的措施,來限制單個用戶的搶購頻率。實現方式其實也很簡單:用redis給每個用戶做訪問統計。

    我們使用外部緩存來解決問題,這樣即便是分布式的秒殺系統,請求被隨意分流的情況下,也能做到精準的控制每個用戶的訪問次數。這里選擇redis。同樣在Controller中添加要求驗證的搶購接口 + 單用戶限制訪問頻率的方法:

    @RequestMapping(value = "/createOrderWithVerifiedUrlAndLimit", method = {RequestMethod.GET}) @ResponseBody public String createOrderWithVerifiedUrlAndLimit(@RequestParam(value = "sid") Integer sid,@RequestParam(value = "userId") Integer userId,@RequestParam(value = "verifyHash") String verifyHash) {int stockLeft;try {int count = userService.addUserCount(userId);LOGGER.info("用戶截至該次的訪問次數為: [{}]", count);boolean isBanned = userService.getUserIsBanned(userId);if (isBanned) {return "購買失敗,超過頻率限制";}stockLeft = orderService.createVerifiedOrder(sid, userId, verifyHash);LOGGER.info("購買成功,剩余庫存為: [{}]", stockLeft);} catch (Exception e) {LOGGER.error("購買失敗:[{}]", e.getMessage());return e.getMessage();}return String.format("購買成功,剩余庫存為:%d", stockLeft); }

    UserService中增加兩個方法:addUserCount和getUserIsBanned。addUserCount進行每當訪問訂單接口,則增加一次訪問次數,寫入Redis;getUserIsBanned從Redis讀出該用戶的訪問次數,超過一定次數則不讓購買了,假設我們這里規定最多11次。代碼如下:

    @Overridepublic int addUserCount(Integer userId) throws Exception {String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId;String limitNum = stringRedisTemplate.opsForValue().get(limitKey);int limit = -1;if (limitNum == null) {stringRedisTemplate.opsForValue().set(limitKey, "0", 3600, TimeUnit.SECONDS);} else {limit = Integer.parseInt(limitNum) + 1;stringRedisTemplate.opsForValue().set(limitKey, String.valueOf(limit), 3600, TimeUnit.SECONDS);}return limit;}@Overridepublic boolean getUserIsBanned(Integer userId) {String limitKey = CacheKey.LIMIT_KEY.getKey() + "_" + userId;String limitNum = stringRedisTemplate.opsForValue().get(limitKey);if (limitNum == null) {LOGGER.error("該用戶沒有訪問申請驗證值記錄,疑似異常");return true;}return Integer.parseInt(limitNum) > ALLOW_COUNT;}

    接下來還是實際進行操作,看一下最終的結果。我們依然使用Jmeter進行同一用戶20次的訪問量,看一下執行結果:

    可以看到到第十一次的時候就不允許搶購了。所以我們實現了統一用戶訪問頻率的攔截。

    猜你感興趣
    教你從0到1搭建秒殺系統-防超賣
    教你從0到1搭建秒殺系統-限流
    教你從0到1搭建秒殺系統-搶購接口隱藏與單用戶限制頻率
    教你從0到1搭建秒殺系統-緩存與數據庫雙寫一致
    教你從0到1搭建秒殺系統-Canal快速入門(番外篇)
    教你從0到1搭建秒殺系統-訂單異步處理

    更多文章請點擊:更多…

    參考文章:
    https://cloud.tencent.com/developer/article/1488059
    https://juejin.im/post/5dd09f5af265da0be72aacbd
    https://zhenganwen.top/posts/30bb5ce6/
    https://www.cnblogs.com/java-my-life/archive/2012/06/08/2538146.html

    總結

    以上是生活随笔為你收集整理的教你从0到1搭建秒杀系统-抢购接口隐藏与单用户限制频率的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。