秒殺方案
首先,要明確一點,高併發場景下系統的瓶頸出現在哪裡,其實主要就是資料庫,那麼就要想辦法為資料庫做層層防護,減輕資料庫的壓力。
1. 業務場景
1.秒殺頻道首頁列出秒殺商品,點選秒殺商品圖片可以跳轉到秒殺商品詳細頁面
2.商品詳細頁面顯示秒殺商品資訊,點選立即搶購實現秒殺下單,下單時扣減庫存,當庫存為0或者不存在活動時間範圍內時無法秒殺
3.秒殺下單成功,直接跳轉到支付頁面(掃碼),支付成功,跳轉到成功頁面,填寫收貨、電話、收件人等資訊,完成訂單。
2.資料庫的設計
應為秒殺活動是經常舉行的,而且防止商品表,訂單表上面冗餘太多的欄位,對於秒殺我們公司有一套專門的表。
-- 秒殺商品表 CREATE TABLE `miaosha_goods` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '秒殺的商品表', `goods_id` bigint(20) DEFAULT NULL COMMENT '商品Id', `miaosha_price` decimal(10,2) DEFAULT '0.00' COMMENT '秒殺價', `stock_count` int(11) DEFAULT NULL COMMENT '庫存數量', `start_date` datetime DEFAULT NULL COMMENT '秒殺開始時間', `end_date` datetime DEFAULT NULL COMMENT '秒殺結束時間', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
--秒殺訂單表
CREATE TABLE `miaosha_order` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '使用者ID',
`order_id` bigint(20) DEFAULT NULL COMMENT '訂單ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品ID',
PRIMARY KEY (`id`),
UNIQUE KEY `u_uid_gid` (`user_id`,`goods_id`) USING BTREE
/*!40101 SET character_set_client = @saved_cs_client */;
--訂單詳情表
CREATE TABLE `order_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '使用者ID',
`goods_id` bigint(20) DEFAULT NULL COMMENT '商品ID',
`delivery_addr_id` bigint(20) DEFAULT NULL COMMENT '收穫地址ID',
`goods_count` int(11) DEFAULT '0' COMMENT '商品數量',
`goods_price` decimal(10,2) DEFAULT '0.00' COMMENT '商品單價',
`order_channel` tinyint(4) DEFAULT '0' COMMENT '1pc,2android,3ios',
`status` tinyint(4) DEFAULT '0' COMMENT '訂單狀態,0新建未支付,1已支付,2已發貨,3已收貨,4已退款,5已完成',
`create_date` datetime DEFAULT NULL COMMENT '訂單的建立時間',
`pay_date` datetime DEFAULT NULL COMMENT '支付時間',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1565 DEFAULT CHARSET=utf8mb4;
--秒殺使用者表
CREATE TABLE `miaosha_user` (
`id` bigint(20) NOT NULL COMMENT '使用者ID,手機號碼',
`nickname` varchar(255) NOT NULL,
`password` varchar(32) DEFAULT NULL COMMENT 'MD5(MD5(pass明文+固定salt) + salt)',
`salt` varchar(10) DEFAULT NULL,
`head` varchar(128) DEFAULT NULL COMMENT '頭像,雲端儲存的ID',
`register_date` datetime DEFAULT NULL COMMENT '註冊時間',
`last_login_date` datetime DEFAULT NULL COMMENT '上蔟登入時間',
`login_count` int(11) DEFAULT '0' COMMENT '登入次數',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
三.秒殺實現思路
實現秒殺,注意3點
1. 防止賣超。(在資料庫層解決,其他的什麼快取reids 花裡胡哨的判斷,只能說是優化,最基本的解決就是在資料庫)
解決方式 樂觀鎖,在商品減庫存的時候,增加count>0的條件。
2. 防止重複下單。(在資料庫層解決,其他的什麼快取reids 花裡胡哨的判斷,只能說是優化,最基本的解決就是在資料庫)
解決方式 唯一索引 在秒殺訂單表中 使用 商品id 和使用者id 當中唯一索引。
3. 解決併發,提升qps 。
解決方式: 減少資料庫訪問次數,使用記憶體,快取redis ,訊息佇列 rabbitMq。
四.實現關鍵步驟說明
1. redis預減庫存,減少對資料庫的訪問。
2.記憶體標記減少對redis 的訪問。
3. 請求先入隊快取,直接返回排隊中。
4. 然後通過mq 請求出隊 ,非同步操作。做後續的工作,比如資料庫的減庫存,生成訂單。
5. 客戶端輪詢呼叫查詢是否秒殺成功介面介面
程式碼邏輯。
1. 系統初始化的時候,查詢商品資訊,快取到redis中,同時也快取到本地標識中,其實就是一個hashMap
@Controller @RequestMapping("/miaosha") public class MiaoshaController implements InitializingBean { private static volatile boolean isGlobalActivityOver = false; private static HashMap<Long, Integer> stockMap = new HashMap<Long, Integer>(); //記憶體標記減少對redis 的訪問 private HashMap<Long, Boolean> localOverMap = new HashMap<Long, Boolean>(); /** * 系統初始化 實現 implements InitializingBean 就可以完成 系統初始化載入資料 * */
@Override public void afterPropertiesSet() throws Exception { List<GoodsVo> goodsList = goodsService.listGoodsVo(); if(goodsList == null) { return; } for(GoodsVo goods : goodsList) { redisService.set(GoodsKey.getMiaoshaGoodsStock, ""+goods.getId(), goods.getStockCount()); //記憶體標記,把商品的資訊放到map中,進行判斷減少對redis 的訪問 localOverMap.put(goods.getId(), false); }
2. 使用者點選秒殺介面
2.1 首先 判斷該商品在記憶體(hashMap)是否存在,不存在,直接給出返回 商品已經秒殺完畢 資訊,後續的redis 判斷都沒必要訪問了。
2.2 如果判斷判斷該商品在記憶體中有,然後讀入reids 中商品的資料,預減庫存(redis中的資料),並返回該商品在redis 中的數量。
如果redis 中的商品數量大於商品實際的數量了。說明該商品已經賣完,同時,把hashMap 中對應商品的值,設定為true。
2.3 通過商品id 和使用者id 去redis 中判斷該使用者是否秒殺到商品了,如果有,直接給出返回資訊 不能重複秒殺。否則 把商品id和使用者id 入隊 放到rabbitMq中。
-- -- 以上操作是沒有訪問過資料庫的。 程式碼如下
//記憶體標記,減少redis訪問 boolean over = localOverMap.get(goodsId); if(over) { return Result.error(CodeMsg.MIAO_SHA_OVER); } //預減庫存 long stock = redisService.decr(GoodsKey.getMiaoshaGoodsStock, ""+goodsId);//10 if(stock < 0) { localOverMap.put(goodsId, true); return Result.error(CodeMsg.MIAO_SHA_OVER); } //判斷是否已經秒殺到了 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return Result.error(CodeMsg.REPEATE_MIAOSHA); } //入隊 MiaoshaMessage mm = new MiaoshaMessage(); mm.setUser(user); mm.setGoodsId(goodsId); sender.sendMiaoshaMessage(mm); return Result.success(0);//排隊中
2.4 訊息出隊,根據商品id 去資料庫查詢商品的數量,如果小於零,直接return,否則在去資料庫查詢該商品是否已經秒殺到了,如果秒殺到了,直接return。
log.info("receive message:"+message); MiaoshaMessage mm = RedisService.stringToBean(message, MiaoshaMessage.class); MiaoshaUser user = mm.getUser(); long goodsId = mm.getGoodsId(); //判斷商品數量是否大於零 GoodsVo goods = goodsService.getGoodsVoByGoodsId(goodsId); int stock = goods.getStockCount(); if(stock <= 0) { return; } //判斷是否已經秒殺到了 MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdGoodsId(user.getId(), goodsId); if(order != null) { return; }
2.5減庫存 ,下訂單 寫入秒殺訂單,同時向資料庫中寫入該使用者生成的訂單資訊(應為前端會輪詢的呼叫我們查詢該使用者下單結果介面,到時候查詢該快取資訊就行)。
//減庫存
//減庫存 下訂單 寫入秒殺訂單 boolean success = goodsService.reduceStock(goods); if(success) { return orderService.createOrder(user, goods); }else { setGoodsOver(goods.getId()); return null; }
//下單
@Transactional public OrderInfo createOrder(MiaoshaUser user, GoodsVo goods) { OrderInfo orderInfo = new OrderInfo(); orderInfo.setCreateDate(new Date()); orderInfo.setDeliveryAddrId(0L); orderInfo.setGoodsCount(1); orderInfo.setGoodsId(goods.getId()); orderInfo.setGoodsName(goods.getGoodsName()); orderInfo.setGoodsPrice(goods.getMiaoshaPrice()); orderInfo.setOrderChannel(1); orderInfo.setStatus(0); orderInfo.setUserId(user.getId()); orderDao.insert(orderInfo); MiaoshaOrder miaoshaOrder = new MiaoshaOrder(); miaoshaOrder.setGoodsId(goods.getId()); miaoshaOrder.setOrderId(orderInfo.getId()); miaoshaOrder.setUserId(user.getId()); orderDao.insertMiaoshaOrder(miaoshaOrder);
//訂單資訊儲存到redis redisService.set(OrderKey.getMiaoshaOrderByUidGid, ""+user.getId()+"_"+goods.getId(), miaoshaOrder); return orderInfo; }
3. 對於秒殺一些其他的小優化。
3.1 圖形驗證碼功能,這樣對於緩解併發也是一個不錯的手段。
3.2 介面限流放刷。
如有需要原始碼,請聯絡我。