1. 程式人生 > >電商專案day19(秒殺功能實現)

電商專案day19(秒殺功能實現)

今日目標:

秒殺實現思路
    實現秒殺下單功能
    
    完成下單併發產生的訂單異常問題  超賣
    完成高併發下使用者下單排隊和超限問題

一.秒殺的思路分析

1.需求分析:

所謂“秒殺”,就是網路賣家釋出一些超低價格的商品,所有買家在同一時間網上搶購的一種銷售方式。通俗一點講就是網路商家為促銷等目的組織的網上限時搶購活動。由於商品價格低廉,往往一上架就被搶購一空,有時只用一秒鐘。

秒殺一共有,兩種限制:庫存限制和時間限制

需求:

(1)商家提交秒殺商品申請,錄入秒殺商品資料,主要包括:商品標題、原價、秒殺價、商品圖片、介紹等資訊
(2)運營商稽核秒殺申請
(3)秒殺頻道首頁列出秒殺商品(進行中的)點選秒殺商品圖片跳轉到秒殺商品詳細頁。
(4)商品詳細頁顯示秒殺商品資訊,點選立即搶購實現秒殺下單,下單時扣減庫存。當庫存為 0 或不在活動期範圍內時無法秒殺。
(5)秒殺下單成功,直接跳轉到支付頁面(微信掃碼),支付成功,跳轉到成功頁,填寫收貨地址、電話、收件人等資訊,完成訂單。

(6)當用戶秒殺下單 5 分鐘內未支付,取消預訂單,呼叫微信支付的關閉訂單介面,恢復庫存。

資料庫表的分析:

秒殺表的資料庫表,我們不放在其他的商品表中,單獨設計一個表結構

tb_seckill_goods表結構    

tb_seckill_order表主要是:秒殺成功後生成的訂單

我們分析什麼條件的商品的資料能夠在秒殺頁面展示?

     稽核通過

      有庫存

       當前實現大於開始時間,並小於秒殺結束時間   即正在秒殺的商品

 秒殺的實現思路分析:

         秒殺技術實現核心思想是運用快取減少資料庫瞬間的訪問壓力!讀取商品詳細資訊時運用快取,當用戶點選搶購時減少快取中的庫存數量,當庫存數為 0 時或活動期結束時,同步到資料庫。 產生的秒殺預訂單也不會立刻寫到資料庫中,而是先寫到快取,當用戶付款成
功後再寫入資料庫。

       基於redis快取,減少資料庫的訪問壓力,在秒殺之前就將資料庫的的資料放到快取中

       秒殺開始後,使用者搶購商品訂單,下單成功後,減少庫存,此時我們是操作redis中秒殺商品庫存資料

那麼我們在什麼時候更新資料庫的庫存呢,      ----------------->redis庫存為零,秒殺結束

使用redis快取,單執行緒伺服器,資料安全

定時任務spring-task實現:

定時任務主要常用的有兩種:     spring - task     和quartz   

我們在這介紹spring  task   :

它可以說是輕量級的quartz    ,    主要配置檔案和註解兩種形式  

定時任務框架都是基於cron表示式完成定時時間指定。

往往都是6位字元
            Seconds Minutes Hours DayofMonth Month   DayofWeek 
                秒    分     時     月中某天   月     週中某天   
                
                每週一凌晨1點執行任務:0 0 1 ? * 2
                
                每天15點55分執行任務:0 55 15 * * ?
                
                每月6號凌晨執行任務:0 0 0 6 * ?
                
                每隔10秒多久執行一次:0/10 0 0 * * ?
                                    
            
        注意:月中某天和週中某天只能出現一個*,不能同時為*,如果兩者中有任意一個賦值,另一個往往都賦予?

 

把資料庫的商品快取到redis中 這樣就能,在下單的時候通過 通過redis總訪問資料

建立一個定時任務的工程  seckill_task

配置檔案:

web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
         version="2.5">

    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>classpath*:spring/applicationContext*.xml</param-value>
    </context-param>
    <listener>
        <listener-class>
            org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>

</web-app>

applicationContext-task .xml

  <!--包掃描-->
    <context:component-scan base-package="com.pinyougou.seckill.task"/>

    <!--開啟註解驅動-->
    <task:annotation-driven/>

seckillTask:主要是通過定時,從資料中獲得資料快取到redis資料庫中  

@Component
public class SeckillTask {

    @Autowired
    private TbSeckillGoodsMapper seckillGoodsMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    @Scheduled(cron = "0/30 * * * * ?")//每30秒執行一次
    public  void synchronizeSeckillGoodsToRedis(){

        //1.查詢需要秒殺的商品
        //我們把符合的商品都查詢出來放到redis中
        TbSeckillGoodsExample example = new TbSeckillGoodsExample();
        TbSeckillGoodsExample.Criteria criteria = example.createCriteria();
        criteria.andStatusEqualTo("1").
                andStartTimeLessThanOrEqualTo(new Date()).
                andEndTimeGreaterThanOrEqualTo(new Date()).
                andStockCountEqualTo(0);
        List<TbSeckillGoods> seckillGoods = seckillGoodsMapper.selectByExample(example);

        //2.將查詢的商品的資料放到redis中
        //我們將查詢符合的資料放到hash格式放到redis中
        for (TbSeckillGoods seckillGood : seckillGoods) {
            redisTemplate.boundHashOps("SECKILL_GOODS").put(seckillGood.getId(),seckillGood);
            //商品的詳情頁,我們可以通過商品的id取值,如下就是的
           // List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
        }
        System.out.println("synchronizeSeckillGoodsToRedis  worker  finished...");
    }

}
List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();

這個資料我們在秒殺的詳情頁中展示,所以通過上面的格式儲存後,我們就可以通過商品的id取值

二.秒殺下單功能實現

構建秒殺的功能模組  web   interface   service

從redis 中獲取需要參加秒殺的商品

後臺程式碼:

service層:

@Service
@Transactional
public class SeckillServiceImpl implements SeckillService{

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 從redis 中查詢所有要參加秒殺的商品
     * @return
     */
    @Override
    public List<TbSeckillGoods> findAllSeckillGoodsFromRedis() {
        //獲取redis中的資料
        List seckill_goods = redisTemplate.boundHashOps("SECKILL_GOODS").values();
        return seckill_goods;
    }
}

interface層和controller層:

/**
     * 從redis中查詢所有的秒殺商品列表
     */
    public List<TbSeckillGoods> findAllSeckillGoodsFromRedis();

//controller層:
@RestController
@RequestMapping("/seckill")
public class SeckillController {


    @Reference
    private SeckillService seckillService;

    /**
     * 從redis中查詢所有的要參加秒殺的商品
     * @return
     */
    @RequestMapping("/findSeckillList")
    public List<TbSeckillGoods> findSeckillList(){
     return   seckillService.findAllSeckillGoodsFromRedis();
    }
}

前臺頁面的實現:

我們通過$http內建物件傳送請求

//服務層
app.service('seckillService',function($http){

    //查詢需要秒殺的商品列表
    this.findSeckillList=function(){
        return $http.get('seckill/findSeckillList.do');
    }
});
 //控制層 
app.controller('seckillController' ,function($scope,$controller   ,seckillService){
	
	$controller('baseController',{$scope:$scope});//繼承
    //,$location 跨域
    //查詢需要從redis中獲得需要的秒殺商品資料
    $scope.findSeckillList=function () {
        seckillService.findSeckillList().success(function (response) {
            $scope.seckillList=response;
        })
    }
});	

我們通過秒殺頁面跳轉到秒殺的詳情頁,通過路由傳參

詳情頁面的展示:

思路:我們通過商品的id查詢商品的詳情,前臺頁面進行展示,注意倒計時的處理,我們通過angularjs中$interval  一個內建物件進行設定

後臺我們先從spring-sceurity中獲得使用者登入的資訊,然後從redis中獲得商品的資料,組裝訂單,然後在redis的商品庫中減去一,儲存訂單,當庫存為零的是時候我們,更新資料庫,清除redis中該商品的資料,注意:一定要在秒殺成功後,扣減庫存

 /**
     * 儲存秒殺的訂單
     */
    @RequestMapping("/saveSeckillOrder")
    public Result saveSeckillOrder(Long seckillGoodsId){
        try {
            //基於安全獲取登入人資訊
            String userId = SecurityContextHolder.getContext().getAuthentication().getName();

            if(userId.equals("anonymousUser")){
                return  new Result(false,"請下登入,再下單");
            }

            seckillService.saveSeckillOrder(seckillGoodsId,userId);
            return new Result(true,"秒殺下單成功");
        } catch (RuntimeException e) {
            e.printStackTrace();
            return new Result(false,e.getMessage());
        }catch (Exception e) {
            e.printStackTrace();
            return new Result(false,"秒殺下單失敗");
        }
    }

service層:

/**
     * 儲存秒殺訂單
     * @param seckillGoodsId
     * @param userId
     */
    @Override
    public void saveSeckillOrder(Long seckillGoodsId, String userId) {

        //從快取中獲取秒殺商品
        TbSeckillGoods seckillGoods=  (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);

        if(seckillGoods==null || seckillGoods.getStockCount()<=0){
            throw new RuntimeException("商品售完");
        }


        //組裝秒殺訂單資料
        TbSeckillOrder seckillOrder = new TbSeckillOrder();
        /*
        tb_seckill_order
	  `id` bigint(20) NOT NULL COMMENT '主鍵',
	  `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒殺商品ID',
	  `money` decimal(10,2) DEFAULT NULL COMMENT '支付金額',
	  `user_id` varchar(50) DEFAULT NULL COMMENT '使用者',
	  `seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
	  `create_time` datetime DEFAULT NULL COMMENT '建立時間',
	  `status` varchar(1) DEFAULT NULL COMMENT '狀態',
         */
        seckillOrder.setId(idWorker.nextId());
        seckillOrder.setSeckillId(seckillGoodsId);
        seckillOrder.setMoney(seckillGoods.getCostPrice());
        seckillOrder.setUserId(userId);
        seckillOrder.setSellerId(seckillGoods.getSellerId());
        seckillOrder.setCreateTime(new Date());
        seckillOrder.setStatus("1");//未支付

        //設定秒殺商品庫存減一
        seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
        //儲存秒殺訂單
        seckillOrderMapper.insert(seckillOrder);
        if(seckillGoods.getStockCount()<=0){
            //商品售完,沒有庫存後,需要更新資料庫中秒殺商品庫存資料
            seckillGoodsMapper.updateByPrimaryKey(seckillGoods);

            //清除redis中該商品
            redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
        }
        //秒選下單成功後,扣減庫存
        redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);

    }

前臺程式碼:

 //秒殺下單
    this.saveSeckillOrder=function (seckillGoodsId) {
        return $http.get('seckill/saveSeckillOrder.do?seckillGoodsId='+seckillGoodsId)
    }

controller層:

//秒殺下單
    $scope.saveSeckillOrder=function () {
        seckillService.saveSeckillOrder($scope.seckillGoodsId).success(function (response) {
            alert(response.message);
        })
    }

測試:儲存訂單成功,下面我們進行秒殺的優化

三.秒殺優化的解決方案

1.解決同一個人重複購買此商品的問題:
    解決方案:使用者下單後,向redis中存放一個預支付資訊。當進入到下單的方法時,先判斷redis中是否存在該使用者的預支付資訊。
        如果存在,則丟擲異常提醒先去支付已買商品。

service層:

 

2.解決超賣的問題
    超賣是由於redis和mysql處理能力不同造成的。
    因為使用者從redis中獲取商品資訊時,redis處理能力是很強勁的。而使用者搶到商品下訂單後,訂單儲存到mysql資料庫,資料庫本來
    處理能力是沒有redis強勁的。所以,可能造成同時5個使用者,能夠為同一件商品下訂單,這是不允許的。
    
    可以使用redis佇列解決。
    使用list儲存形式。可以基於左右壓棧操作。
    
    基於redis佇列,快取某個秒殺商品還剩多個庫存。
    
    
    
    訊息佇列也可以實現。

在這我們通過redis的左壓棧實現

3.使用多執行緒解決操作mysql的問題
    因為mysql執行效率比redis要低,所以,需要充分利用CPU的資源,提升mysql處理操作
    可以基於spring整合多執行緒完成該操作。在spring配置檔案中配置執行緒池。

配置執行緒池:

我們將執行資料庫操作出去到執行緒中去

package com.pinyougou.seckill.service.impl;

import com.pinyougou.mapper.TbSeckillGoodsMapper;
import com.pinyougou.mapper.TbSeckillOrderMapper;
import com.pinyougou.pojo.TbSeckillGoods;
import com.pinyougou.pojo.TbSeckillOrder;
import com.pinyougou.util.IdWorker;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;

import java.util.Date;
import java.util.Map;

public class CreateSeckillOrder implements Runnable {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private IdWorker idWorker;

    @Autowired
    private TbSeckillOrderMapper seckillOrderMapper;

    @Autowired
    private TbSeckillGoodsMapper seckillGoodsMapper;
    @Override
    public void run() {
        
        //從redis中取出我們需要的訂單任務
        Map<String,Object> param = (Map<String, Object>) redisTemplate.boundListOps("seckill_order_queue").rightPop();
        Long seckillGoodsId = (Long) param.get("seckillGoodsId");
        String userId = (String) param.get("userId");
        //從快取中獲取秒殺商品
        TbSeckillGoods seckillGoods=  (TbSeckillGoods) redisTemplate.boundHashOps("seckill_goods").get(seckillGoodsId);
        //組裝秒殺訂單資料
        TbSeckillOrder seckillOrder = new TbSeckillOrder();
        /*
        tb_seckill_order
	  `id` bigint(20) NOT NULL COMMENT '主鍵',
	  `seckill_id` bigint(20) DEFAULT NULL COMMENT '秒殺商品ID',
	  `money` decimal(10,2) DEFAULT NULL COMMENT '支付金額',
	  `user_id` varchar(50) DEFAULT NULL COMMENT '使用者',
	  `seller_id` varchar(50) DEFAULT NULL COMMENT '商家',
	  `create_time` datetime DEFAULT NULL COMMENT '建立時間',
	  `status` varchar(1) DEFAULT NULL COMMENT '狀態',
         */
        seckillOrder.setId(idWorker.nextId());
        seckillOrder.setSeckillId(seckillGoodsId);
        seckillOrder.setMoney(seckillGoods.getCostPrice());
        seckillOrder.setUserId(userId);
        seckillOrder.setSellerId(seckillGoods.getSellerId());
        seckillOrder.setCreateTime(new Date());
        seckillOrder.setStatus("1");//未支付

        //設定秒殺商品庫存減一
        seckillGoods.setStockCount(seckillGoods.getStockCount()-1);
        //儲存秒殺訂單
        seckillOrderMapper.insert(seckillOrder);
        //秒殺下單成功後,我們要儲存一份預支付的訂單到redis,中,下次我們再在redis中判斷,是否是第二次購買該商品
        //解決同一個人,不能重複購買的問題
        redisTemplate.boundSetOps("seckill_goods_"+seckillGoodsId).add(userId);
        if(seckillGoods.getStockCount()<=0){
            //商品售完,沒有庫存後,需要更新資料庫中秒殺商品庫存資料
            seckillGoodsMapper.updateByPrimaryKey(seckillGoods);

            //清除redis中該商品
            redisTemplate.boundHashOps("seckill_goods").delete(seckillGoodsId);
        }
        //秒選下單成功後,扣減庫存
        redisTemplate.boundHashOps("seckill_goods").put(seckillGoodsId,seckillGoods);
    }
}

在service中呼叫

注意注入我們配置的執行緒池:

4.排隊人數提醒
    當一個請求進入下單的方法時,需要設定排隊人數加1,當排隊人多大於庫存一定值時(例如10,根據業務規則確定),丟擲異常,提醒排隊人數過多。
    當一個人下單買完商品後,排隊人數減一(在多執行緒下單模組完成)