SpringBoot+Redis+RabbitMQ 實現訂單秒殺

前言

電商平臺有時候會搞一些秒殺活動,秒殺活動的併發量特別高,會導致訪問變慢、商品超賣等問題。所以,寫一篇博客記錄一下主要實現思路,解決一些小問題。

一、建表

就簡單的建一張表,秒殺活動表。然後就可以寫代碼,通過頁面添加秒殺活動了,這裏就不寫這些了。

CREATE TABLE `seckill_promotion_table` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `goods_id` varchar(20) DEFAULT '0' COMMENT '商品id',
  `ps_count` int(11) DEFAULT NULL COMMENT '秒殺數量',
  `current_price` decimal(10,2) DEFAULT NULL COMMENT '秒殺價格',
  `start_time` datetime DEFAULT NULL COMMENT '開始時間',
  `end_time` datetime DEFAULT NULL COMMENT '結束時間',
  `status` int(11) NOT NULL DEFAULT '0' COMMENT '0-未開始 1-進行中  2-已結束',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8 COMMENT='秒殺活動表';

二、數據靜態化

把商品信息等等一些不經常改變的數據都寫死在html頁面裏,不用從後臺獲取,這樣秒殺的時候就能加快用戶的訪問速度,秒殺倒計時和評論信息可以從後臺獲取進行渲染。可以使用thymeleaf或者freemarker來渲染,然後生成html文件,直接放在nginx裏就可以訪問了。這個也很簡單,就不在贅述了。

三、初始化進redis,防止超賣

添加秒殺活動的時候會設置開始時間和結束時間。設置的開始時間並不是馬上就開始,所以我們就需要定時任務去查詢當前是否有開始的秒殺任務。

然後把要開始的秒殺任務扔進redis裏,爲什麼要扔進redis裏呢?因爲redis操作都是串行的,同時又好多請求的時候,會排隊一個一個的執行,防止商品的超賣。所以, 有幾個要秒殺的商品,我們就初始化幾個list對象放進redis裏。

@Mapper
public interface SeckillPromotionLWQDAO{

    @InsertProvider(method = "insertSeckillPromotion", type = SeckillPromotionLWQProvider.class)
    @Options(useGeneratedKeys = true,keyProperty = "seckillpromotion.id")
	Integer insert(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;

    @UpdateProvider(method = "updateSeckillPromotion", type = SeckillPromotionLWQProvider.class)
	Integer updateById(@Param("seckillpromotion")SeckillPromotionDO seckillpromotion)throws Exception;

    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table "
    		+ "WHERE id = #{id}")
	SeckillPromotionDO getByPrimaryKey(Integer id)throws Exception;

    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status FROM seckill_promotion_table ")
	List<SeckillPromotionDO> listAllSeckillPromotionDO()throws Exception;
    
    /**
     * 查詢未開始的秒殺動
     * @return
     * @throws Exception
     */
    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
    		+ "FROM seckill_promotion_table "
    		+ "WHERE now() BETWEEN start_time AND end_time AND status = 0")
	List<SeckillPromotionDO> listUnstartSeckill()throws Exception;
    
    /**
     * 查詢已經過期的秒殺活動
     * @return
     * @throws Exception
     */
    @Select("SELECT id,goods_id,ps_count,current_price,start_time,end_time,status "
    		+ "FROM seckill_promotion_table "
    		+ "WHERE now() > end_time AND status = 1")
	List<SeckillPromotionDO> listExpireSeckill()throws Exception;
    
}
@Component
public class SeckillTask {

	private static final String PREFIX = "seckill:count:";
	
	@Autowired
	private SeckillPromotionLWQDAO seckillDAO;
	@Autowired
	private RedisTemplate<String, Object> redisTemplate;
	
	@Scheduled(cron = "0/5 * * * * ?")
	public void startSecKill() throws Exception{
		
		List<SeckillPromotionDO> list = seckillDAO.listUnstartSeckill();
		if (!list.isEmpty()) {
			
			for (SeckillPromotionDO seckill : list) {
				
				 //刪掉以前重複的活動任務緩存
	            redisTemplate.delete(PREFIX + seckill.getId());
	            
	            //有幾個庫存商品,則初始化幾個list對象
	            for(int i = 0 ; i < seckill.getPsCount() ; i++){
	                redisTemplate.opsForList().rightPush(PREFIX + seckill.getId(), seckill.getGoodsId());
	            }
	            seckill.setStatus(1);
	            seckillDAO.updateById(seckill);
				
			}
			
		}
		
	}
	
	@Scheduled(cron = "0/5 * * * * ?")
	public void endSecKill() throws Exception{
		
		List<SeckillPromotionDO> list = seckillDAO.listExpireSeckill();
		if (! list.isEmpty()) {
			
			for (SeckillPromotionDO seckill : list) {
				
				seckill.setStatus(2);
				seckillDAO.updateById(seckill);
				
				redisTemplate.delete(PREFIX + seckill.getId());
			}
		}
		
	}
	
}

四、開始秒殺

接下來就是秒殺商品了,我們會去redis裏取上一步放進去的list,如果能取到就是秒殺成功了,然後把用戶id放進redis的set裏,這樣秒殺的用戶就不回重複了。

搶到商品後,生成訂單信息,然後把訂單信息扔進MQ裏,用於削峯處理。

public ReturnDataDTO<Object> processSeckill(String userId, Integer seckillId) throws Exception {
		
		SeckillPromotionDO seckill = seckillDAO.getByPrimaryKey(seckillId);
		if (Objects.equals(seckill, null)) {
			throw new MyException(LwqExceptionEnum.NOT_EXIT);
		}
		Integer status = seckill.getStatus();
		if (status == 0) {
			throw new MyException(LwqExceptionEnum.NOT_START);
		}
		if (status == 2) {
			throw new MyException(LwqExceptionEnum.EVER_END);
		}
		String goodsIdInRedis = (String) redisTemplate.opsForList().leftPop(GOODS_PREFIX + seckill.getId());
		if (goodsIdInRedis != null) {
            //判斷是否已經搶購過
            boolean isExisted = redisTemplate.opsForSet().isMember(USER_PREFIX + seckill.getId(), userId);
            if (!isExisted) {
            	//搶到商品了
                redisTemplate.opsForSet().add("seckill:users:" + seckill.getId(), userId);
            }else{
            	//已經搶購過的用戶,再把商品id放回去
                redisTemplate.opsForList().rightPush("seckill:count:" + seckill.getId(), seckill.getGoodsId());
                throw new MyException(LwqExceptionEnum.EVER_SECKILL);
            }
        } else {
            throw new MyException(1018,"抱歉,該商品已被搶光,下次再來吧!!");
        }
	    
		String goodsId = seckill.getGoodsId();
		GoodsDO goodsDO = goodsDAO.getByPrimaryKey(goodsId);
		if (Objects.equals(goodsDO, null)) {
			throw new MyException(1019, "沒有該商品");
		}
		BigDecimal currentPrice = goodsDO.getCurrentPrice();
		
		String orderNumber = UUID.randomUUID().toString();
		Snowflake snowflake = IdUtil.createSnowflake(1, 1);
		String id = snowflake.nextIdStr();
		
		GoodsOrderDO order = new GoodsOrderDO();
		order.setId(id);
		order.setOrderNumber(orderNumber);
		order.setUserId(userId);
		order.setTotalAmount(currentPrice);
		order.setStatus(1);
		
		//把訂單信息發送到MQ,用於削峯處理
		JSONObject json = (JSONObject) JSONObject.toJSON(order);
	    seckillProducer.sendSeckillOrderMessage(json);
		
	    //返給前端訂單號,用於主動查詢訂單是否創建成功
	    JSONObject data = new JSONObject(true);
	    data.put("userId", userId);
	    data.put("orderNumber", orderNumber);
	    
		return ReturnDataDTO.ok(data);
	}

五、生成訂單

MQ的消費者就可以把訂單信息入庫,這樣就生成了訂單。

用戶端就可以根據訂單號去查詢是否生成訂單了。MQ消費者訂單沒有入庫的時候,就給用戶展示訂單正在生成中等提示信息。

有了訂單信息就可以去支付了。。

六、寫在最後的話

主要是貼了一些主要的代碼,思路就時有多少秒殺商品就初始化幾個list到redis裏,秒殺的時候去redis裏拿list,能拿到就說明秒殺成功(注意判斷用戶的重複),就把用戶放進set裏,生成訂單信息,扔進MQ。這時候就輪訓訂單是否生成。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章