1. 程式人生 > 實用技巧 >秒殺方案

秒殺方案

前言

首先,要明確一點,高併發場景下系統的瓶頸出現在哪裡,其實主要就是資料庫,那麼就要想辦法為資料庫做層層防護,減輕資料庫的壓力。

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

) ENGINE=InnoDB AUTO_INCREMENT=1551 DEFAULT CHARSET=utf8mb4;
/*!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_name` varchar(16) DEFAULT NULL COMMENT '冗餘過來的商品名稱',
`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 介面限流放刷。

  如有需要原始碼,請聯絡我。