適合新手的SSM框架練手項目——秒殺系統

最近把SSM框架的基礎知識擼了一遍,跟着github上的這個項目 ,實現了一下秒殺系統,並對這個項目中存在的問題進行了 一些小小的改進,記錄一下。整個項目用到了Spring+SpringMVC+Mybatis+Redis框架,如果是剛學SSM框架希望找個小項目練手的,可以跟着這個項目來練練手,感覺涉及的知識還是很全面的。項目源碼已上傳到GitHub:https://github.com/MeteorCh/SecKill,需要的同學自取。

一、項目功能及涉及知識點

項目的整體業務流程如下(感覺自己畫的可能不是很標準,表達意思即可)
搶購流程

1.數據庫

項目涉及三個數據表

(1)用戶表user

在這裏插入圖片描述

(2)商品表seckill

在這裏插入圖片描述

(3)秒殺成功記錄表success_seckilled

在這裏插入圖片描述

2.登錄模塊

2.1 功能描述

在這裏使用了Spring的攔截器對訪問的網址進行攔截,如果沒登錄,就跳轉到登錄頁面登錄。如果輸入的用戶名和密碼都正確,則將用戶名和用戶名用MD5加密的token寫入Cookie,下次登錄時,首先判斷Cookie的登錄信息正不正確,正確的話自動登錄。此外,當用戶訪問登錄頁面時,如果存在cookie,則直接跳轉到列表頁,這些邏輯都寫在LoginInterceptor類中,具體內容下載源碼查看。登錄頁面如下:
在這裏插入圖片描述

2.2 涉及知識點

登錄模塊涉及的知識點主要有:Cookie、Session、SpringMVC攔截器、MD5驗證

3.秒殺商品列表模塊

3.1功能描述

展示所有的秒殺商品,需要注意的是,從數據庫中查找商品時,應把庫存數量爲0的過濾掉。秒殺商品列表頁面如下:
在這裏插入圖片描述

3.2 涉及知識

這個主要就涉及一些簡單的JSP和MyBatis的操作知識。沒有什麼複雜的。

4.商品詳情頁

4.1功能描述

點擊秒殺商品列表頁中的詳情頁,顯示秒殺商品的詳情並提供秒殺通道。這裏,是整個項目的核心,也是高併發的地方之一。所以,這裏使用了Redis作爲緩存。查詢商品詳情時,先去Redis中查找,如果沒有,則去數據庫中查並將結果寫入到Redis,以便下次查找的時候直接從Redis中查找。界面如下:
在這裏插入圖片描述

4.2涉及知識

這裏涉及的知識有:Redis緩存、緩存穿透的處理

5.秒殺

5.1功能描述

用戶點擊秒殺的時候,會首先寫入一條記錄到success_seckilled表中,如果寫入失敗,則說明是重複秒殺。如果寫入成功,再去seckill表中,將對應商品的數量-1,此處爲了防止超賣,需要限制-1時,剩餘商品的數量要大於0。
此處,爲了防止數據被篡改,同樣也需要同時攜帶商品ID用MD5加密的密文,並在後端判斷數據是否被篡改。最後,需要將秒殺的結果以Json的形式返回給瀏覽器,並在客戶端進行顯示。秒殺結果界面如下:
在這裏插入圖片描述
在這裏插入圖片描述

5.2涉及知識

這裏面也是涉及到Redis的 一些知識。具體在第二節討論。

二、問題及解決

原項目中存在這以下問題,我解決了一下,具體如下:

1.自動登錄

可以利用Cookie和Session來實現自動登錄。我這裏保存的Cookie有兩個:
在這裏插入圖片描述
其中UserKey爲用戶名,SsID爲UserKey通過MD5加密的結果。這樣,在後端,就可以通過判斷UserKey通過MD5加密的結果是否和SsID相等,來判斷是否能自動登錄。

2.緩存穿透的問題

緩存穿透是指,請求一條不可能存在的數據,請求時先去緩存中找,不存在,再去數據庫中找,數據庫中也不存在。這樣的話,緩存就沒有意義了。我這裏的解決方案是,一個商品詳情的請求,如果從數據庫中找不存在,首先去數據庫中找,如果結果爲空,仍然把這個數據存儲到Redis中,下次請求的時候,就直接從Redis緩存中找了。但是,用這種方法,這種數據的緩存有效期要儘量短一些,防止增加了數據一直查不到的情況。(我設置的是,有效商品的緩存有效期爲1小時,無效請求的有效期爲3分鐘)。

3.庫存爲0仍然可以顯示秒殺頁

我感覺這個是原項目特別需要完善的一個地方。因爲Redis緩存存儲的商品信息,自從寫入就一直沒有變化。當商品搶購完了以後,用戶點擊詳情頁,仍然可以進入到秒殺頁面,體驗不是很好。所以,我這裏的處理方法是,當商品庫存爲0後,去主動更新一下Redis緩存中對象的庫存值。在請求時,判斷從Redis中取到的商品庫存是否爲0,如果爲0,則跳轉到另一個頁面。

4.錯誤頁面的處理

有時候,我們再請求的過程中會出現異常,然後會在瀏覽器顯示異常的信息,看起來不是很好。我們可以通過簡單的配置web.xml文件,讓出現錯誤時,跳轉到我們自定義的錯誤頁面,配置信息如下:

<error-page>
    <error-code>404</error-code>
    <location>/error</location>
  </error-page>
  <error-page>
    <error-code>500</error-code>
    <location>/error</location>
  </error-page>
  <error-page>
    <error-code>400</error-code>
    <location>/error</location>
  </error-page>

三、併發測試

既然是秒殺系統,那我們需要做一個高併發的測試,看系統的性能到底怎麼樣。我這裏用多線程去模擬請求,測試高併發。代碼如下(我這裏是寫在測試類中):

@RunWith(SpringJUnit4ClassRunner.class)
//告訴junit spring的配置文件
@ContextConfiguration({"classpath:spring-dao.xml",
        "classpath:spring-service.xml"})
public class Tester {
    @Autowired
    SecKillService secKillService;
    @Autowired
    UserService userService;

    /**
     * 插入用戶
     * @throws InterruptedException
     */
    //@Test
    public void insertUser() throws InterruptedException {
        List<User> users=new ArrayList<>(2000);
        for (int i=500;i<2000;++i){
            User user=new User("user"+i,"12345");
            users.add(user);
        }
        userService.insertUsers(users);
    }

    /**
     *測試併發的入口
     */
    @Test
    public void simulateConcurrency(){
        try {
            calculateTime(1000);
        }catch (InterruptedException e){
            e.printStackTrace();
        }
    }


    /**
     * 模擬請求
     * @param userNum 併發數
     * @throws InterruptedException
     */
    public void calculateTime(int userNum) throws InterruptedException {
        long startTime=System.currentTimeMillis();
        ExecutorService service= Executors.newFixedThreadPool(userNum);
        for (int i=0;i<userNum;++i){
            final int num=i;
            service.execute(new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        request(num);
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }));
        }
        service.shutdown();
        service.awaitTermination(1, TimeUnit.HOURS);
        long endTime=System.currentTimeMillis();
        System.out.println("耗費時間:"+(endTime-startTime)/1000);
    }

    /**
     * 請求
     * @param i
     * @throws IOException
     */
    public void  request(int i) throws IOException {
        /**
         * 耗時統計:500併發10秒
         */
        //高併發請求測試
        int secID=1001;
        String urlPath = "http://localhost:8080/SecondKill/meteor/"+secID+"/"
                +CookieUtility.getMd5(secID)+"/execution";
        String userKey = "user"+i;
        String result = "";
        CookieStore cookieStore = new BasicCookieStore();
        CloseableHttpClient httpClient = HttpClients.custom()
                .setDefaultCookieStore(cookieStore)
                .build();
        RequestConfig requestConfig =  RequestConfig.custom().setSocketTimeout(1000000).setConnectTimeout(1000000).build();
        try {
            HttpPost post = new HttpPost(urlPath);//這裏發送post請求
            post.setConfig(requestConfig);
            List<BasicClientCookie> cookies=createCookie(userKey);
            for (BasicClientCookie cookie:cookies)
                cookieStore.addCookie(cookie);
            // 通過請求對象獲取響應對象
            HttpResponse response = httpClient.execute(post);
            // 判斷網絡連接狀態碼是否正常(0--200都數正常)
            result = EntityUtils.toString(response.getEntity(), "utf-8");
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }


    List<BasicClientCookie> createCookie(String userName){
        List<BasicClientCookie> cookies=new ArrayList<>();
        //用戶名
        BasicClientCookie cookie = new BasicClientCookie(ConstValue.USER_KEY, userName);
        cookie.setDomain("localhost");
        cookie.setPath("/SecondKill/");
        cookies.add(cookie);
        //ssid
        BasicClientCookie ssID = new BasicClientCookie(ConstValue.SS_ID,CookieUtility.getMd5(userName));
        ssID.setDomain("localhost");
        ssID.setPath("/SecondKill/");
        cookies.add(cookie);
        return cookies;
    }
}

使用時,首先通過insertUser插入2000個用戶,再調用simulateConcurrency函數,開多線程去模擬高併發,計算請求的時間。我這裏的測試是,500的併發量,耗時是10秒左右。

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