1. 程式人生 > >三、高併發秒殺API之Service層設計實現

三、高併發秒殺API之Service層設計實現

Dao層設計與實現———-》介面設計和sql編寫

實現程式碼和SQL的分離,方便Review,dao層也叫做資料訪問層,是對遠端儲存系統執行操作的過程,這些操作統一存放在Dao層。

而通過Dao層組成的邏輯則是由Service來完成。

一、秒殺Service介面設計

這裡寫圖片描述

service包:存放我們的service介面和實現類
exception包:存放service層出現的異常,例如:重複秒殺商品異常、秒殺已關閉等異常
dto包:作為傳輸層,dto和entity的區別在於 entity用於業務資料的封裝,而dto用於完成web和service層的資料傳遞

service包:
建立Service介面SeckillService.java

package service;

import dto.Exposer;
import dto.SeckillExecution;
import entity.Seckill;
import exception.RepeatKillException;
import exception.SeckillCloseException;
import exception.SeckillException;

import java.util.List;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/4
 */
public
interface SeckillService { /** * 查詢所有秒殺記錄 * @return */ List<Seckill> getSeckillList(); /** * 查詢單個秒殺記錄 * @param seckillId * @return */ Seckill getById(long seckillId); /** * 秒殺開啟時輸出秒殺介面地址, * 否則輸出系統時間和秒殺時間 * @param seckillId */
Exposer exportSeckillUrl(long seckillId); /** * 執行秒殺操作 * @param seckillId * @param userPhone * @param md5 * @throws SeckillException 秒殺業務相關異常 * @throws RepeatKillException 秒殺重複異常 * @throws SeckillCloseException 秒殺關閉異常 * */ SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)throws SeckillException, RepeatKillException, SeckillCloseException; }

/**
* 秒殺開啟時輸出秒殺介面地址,
* 否則輸出系統時間和秒殺時間
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
Exposer.java:

package dto;

/**
 * @Author:peishunwu
 * @Description:暴露秒殺地址DTO
 * @Date:Created  2018/6/4
 */
public class Exposer {
    //是否開啟秒殺
    private boolean exposed;
    //一種加密處理
    private String md5;

    private long seckillId;

    //系統當前時間(毫秒)
    private long now;

    private long start;

    private long end;

    public Exposer(boolean exposed, String md5, long seckillId) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

    public Exposer(boolean exposed, long seckillId, long now, long start, long end) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
        this.now = now;
        this.start = start;
        this.end = end;
    }

    public Exposer(boolean exposed, long seckillId) {
        this.exposed = exposed;
        this.seckillId = seckillId;
    }

    public boolean isExposed() {
        return exposed;
    }

    public void setExposed(boolean exposed) {
        this.exposed = exposed;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public long getNow() {
        return now;
    }

    public void setNow(long now) {
        this.now = now;
    }

    public long getStart() {
        return start;
    }

    public void setStart(long start) {
        this.start = start;
    }

    public long getEnd() {
        return end;
    }

    public void setEnd(long end) {
        this.end = end;
    }
}

/**
* 執行秒殺操作
* @param seckillId
* @param userPhone
* @param md5
* @throws SeckillException 秒殺業務相關異常
* @throws RepeatKillException 秒殺重複異常
* @throws SeckillCloseException 秒殺關閉異常
*
*/

丟擲的三個異常:
秒殺業務相關異常SeckillException.java

package exception;

/**
 * @Author:peishunwu
 * @Description:秒殺業務相關異常
 * @Date:Created  2018/6/4
 */
public class SeckillException extends RuntimeException{
    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}

秒殺關閉異常SeckillCloseException.java

package exception;

/**
 * @Author:peishunwu
 * @Description:秒殺關閉異常,當秒殺結束時候使用者還要進行秒殺就會出現這個異常
 * @Date:Created  2018/6/4
 */
public class SeckillCloseException extends SeckillException{
    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}

秒殺重複異常RepeatKillException.java

package exception;

/**
 * @Author:peishunwu
 * @Description:重複秒殺異常,是一個執行期異常,不需要我們手動try catch
 * mysql只支援執行期異常的回滾操作
 * @Date:Created  2018/6/4
 */
public class RepeatKillException extends SeckillException{
    public RepeatKillException(String message) {
        super(message);
    }

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
}

二、秒殺Service介面實現

這裡寫圖片描述

Service包下建立impl包:存放service包介面的實現類

SeckillServiceImpl.java:

package service.impl;

import dao.SeckillDao;
import dao.SuccessKilledDao;
import dto.Exposer;
import dto.SeckillExecution;
import entity.Seckill;
import entity.SuccessKilled;
import enums.SeckillStateEnum;
import exception.RepeatKillException;
import exception.SeckillCloseException;
import exception.SeckillException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;
import service.SeckillService;

import java.util.Date;
import java.util.List;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/4
 */
public class SeckillServiceImpl implements SeckillService {
    private Logger logger = LoggerFactory.getLogger(SeckillServiceImpl.class);

    private SeckillDao seckillDao;

    private SuccessKilledDao successKilledDao;

    //md5鹽值字串,用於混淆md5;
    private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v";
    @Override
    public List<Seckill> getSeckillList() {
        return seckillDao.queryAll(0,4);
    }

    @Override
    public Seckill getById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        Seckill seckill = seckillDao.queryById(seckillId);
        if(seckill == null){
            return new Exposer(false,seckillId);
        }
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系統當前時間
        Date nowTime = new Date();
        if(nowTime.getTime() < startTime.getTime() || nowTime.getTime()>endTime.getTime()){
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }
        String md5 = getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }

    private String getMD5(long seckillId){
        String base = seckillId+"/"+salt;
        //通過鹽值轉化為加密資料
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
    //秒殺是否成功,成功:減庫存,增加明細;失敗:丟擲異常,事務回滾
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            throw new SeckillException("秒殺資料被重寫了 (seckill data rewrite)");//秒殺資料被重寫了
        }
        //執行秒殺邏輯:減庫存+增加購買明細
        Date nowTime = new Date();
        try{
            //減庫存
            int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
            if(updateCount <= 0){
                //沒有更新庫存記錄,說明秒殺結束
                throw new SeckillCloseException("說明秒殺結束(seckill is closed)");
            }else {
                //否則更新庫存成功,秒殺成功,增加明細
                int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
               //看是否該明細被重複插入,即使用者是否重複秒殺
                if(insertCount <= 0){
                    throw new RepeatKillException("重複秒殺(seckill repeated)");
                }else{
                    //秒殺成功,得到成功插入的明細記錄,並返回秒殺資訊
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS,successKilled);
                }
            }

        }catch (SeckillCloseException e1){
            throw e1;
        }catch (RepeatKillException e2){
            throw e2;
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            //所以編譯期異常轉化為執行期異常
            throw new SeckillException(""+e.getMessage());
        }
    }
}

在上述方法中, return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS,successKilled);
其中SeckillStateEnum.SUCCESS這個表示的是一種操作執行的狀態,用來輸出給前端,通常用資料欄位記錄這些狀態。
在這裡考慮用列舉封存常量表示狀態,實現資料字典;

建立列舉
SeckillStateEnum.java:

package enums;

/**
 * @Author:peishunwu
 * @Description:使用列舉表示常量資料字典
 * @Date:Created  2018/6/4
 */
public enum SeckillStateEnum {
    SUCCESS(1,"秒殺成功"),
    END(0,"秒殺結束"),
    REPEAT_KILL(-1,"重複秒殺"),
    INNER_ERROR(-2,"系統異常"),
    DATA_REWRITE(-3,"資料篡改");

    private int state;

    private String stateInfo;

    SeckillStateEnum(int state, String stateInfo) {
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public void setStateInfo(String stateInfo) {
        this.stateInfo = stateInfo;
    }

    public static SeckillStateEnum stateOf(int index){
        for(SeckillStateEnum state : values()){
            if(state.getState() == index){
                return state;
            }
        }
        return null;
    }
}

修改秒殺操作的非業務類SeckillExcution.java裡面涉及到的state和stateInfo引數的構造方法,將其替換為列舉型別:
SeckillExecution .java

package dto;

import entity.SuccessKilled;
import enums.SeckillStateEnum;

/**
 * @Author:peishunwu
 * @Description:封裝秒殺執行後的結果
 * @Date:Created  2018/6/4
 */
public class SeckillExecution {
    private long seckillId;
    //秒殺執行結果狀態
    private int state;
    //狀態表示
    private String stateInfo;
    //秒殺成功物件
    private SuccessKilled successKilled;
    public SeckillExecution(long seckillId, SeckillStateEnum seckillStateEnum, SuccessKilled successKilled) {
        super();
        this.seckillId = seckillId;
    }
    public SeckillExecution(long seckillId, SeckillStateEnum stateEnum) {
        super();
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getStateInfo();
    }

}

至此,Service介面實現類實現完成,接下來要將Service交付給Spring容器託管,主要是進行一些配置。

三、基於Spring的Service依賴管理

Spring託管Service,實際就是通過Spring IOC管理依賴,主要通過依賴注入。

利用物件工程這些依賴進行依賴管理,給出一致的訪問介面,通過applicationContext或者註解來拿到一個管理例項。

這裡寫圖片描述

那麼對該專案有哪些依賴呢?

這裡寫圖片描述

那麼,為什麼使用Spring IOC呢?

  • 物件的建立統一託管
  • 規範的生命週期的管理
  • 靈活的依賴注入
  • 一致的物件注入

Spring-IOC注入方式和場景是怎樣的?

這裡寫圖片描述

實現:
在spring包下面建立一個檔案spring-service.xml檔案:用於掃描service類

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context-3.0.xsd
         http://www.springframework.org/schema/tx
         http://www.springframework.org/schema/tx/spring-tx.xsd
         " >
    <!--掃描service包下素有使用註解型別-->
    <context:component-scan base-package="service"/>
</beans>

然後採用註解的方式將Service介面的實現類新增到SpringIOC容器中對其進行管理:
這裡寫圖片描述

package service.impl;

import dao.SeckillDao;
import dao.SuccessKilledDao;
import dto.Exposer;
import dto.SeckillExecution;
import entity.Seckill;
import entity.SuccessKilled;
import enums.SeckillStateEnum;
import exception.RepeatKillException;
import exception.SeckillCloseException;
import exception.SeckillException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import service.SeckillService;

import java.util.Date;
import java.util.List;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/4
 */
@Service
public class SeckillServiceImpl implements SeckillService {
    private Logger logger = LoggerFactory.getLogger(SeckillServiceImpl.class);
    @Autowired
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;

    //md5鹽值字串,用於混淆md5;
    private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v";
    @Override
    public List<Seckill> getSeckillList() {
        return seckillDao.queryAll(0,4);
    }

    @Override
    public Seckill getById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        Seckill seckill = seckillDao.queryById(seckillId);
        if(seckill == null){
            return new Exposer(false,seckillId);
        }
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系統當前時間
        Date nowTime = new Date();
        if(nowTime.getTime() < startTime.getTime() || nowTime.getTime()>endTime.getTime()){
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }
        String md5 = getMD5(seckillId);
        return new Exposer(true,md5,seckillId);
    }

    private String getMD5(long seckillId){
        String base = seckillId+"/"+salt;
        //通過鹽值轉化為加密資料
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
    //秒殺是否成功,成功:減庫存,增加明細;失敗:丟擲異常,事務回滾
    /*使用註解控制事務方法的優點:
    1、開發團隊達成一致約定,明確標註事務方法的程式設計風格
    2、保證事務方法的執行時間儘可能短,不要穿插其他網路操作(RPC/HTTP請求),或者剝離到事務方法外部
    3、不是所有的方法都需要事務,如只有一條修改操作或只讀操作不需要事務控制*/
    @Transactional
    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        if(md5 == null || !md5.equals(getMD5(seckillId))){
            throw new SeckillException("秒殺資料被重寫了 (seckill data rewrite)");//秒殺資料被重寫了
        }
        //執行秒殺邏輯:減庫存+增加購買明細
        Date nowTime = new Date();
        try{
            //減庫存
            int updateCount = seckillDao.reduceNumber(seckillId,nowTime);
            if(updateCount <= 0){
                //沒有更新庫存記錄,說明秒殺結束
                throw new SeckillCloseException("說明秒殺結束(seckill is closed)");
            }else {
                //否則更新庫存成功,秒殺成功,增加明細
                int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
               //看是否該明細被重複插入,即使用者是否重複秒殺
                if(insertCount <= 0){
                    throw new RepeatKillException("重複秒殺(seckill repeated)");
                }else{
                    //秒殺成功,得到成功插入的明細記錄,並返回秒殺資訊
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
                    return new SeckillExecution(seckillId,SeckillStateEnum.SUCCESS,successKilled);
                }
            }

        }catch (SeckillCloseException e1){
            throw e1;
        }catch (RepeatKillException e2){
            throw e2;
        }catch (Exception e){
            logger.error(e.getMessage(),e);
            //所以編譯期異常轉化為執行期異常
            throw new SeckillException(""+e.getMessage());
        }
    }
}

對Service類使用@Service註解表明這是一個Service類,注入SpringIOC容器被管理。
對要使用的例項使用@Autowired宣告,實現例項的獲取,並自動注入。

接下來,我們來運用Spring宣告式事務對我們專案中的事務進行管理。

四、使用Spring宣告式事務配置管理事務 ##、

事務管理流程:事務開啟——》修改SQL1,修改SQL2,修改SQLn——–》提交/回滾事務

現在使用第三方框架管理這個流程,使其擺脫事務編碼,這個叫做宣告式事務,
方式一、早期的spring管理事務是ProxyFactoryBean+XML的方式
方式二、後來添加了tx:advice+aop名稱空間使得一次配置永久生效
方式三、使用註解@Transactional來控制事務,也是推薦的一種方式

為什麼推薦使用註解控制事務呢?

1、開發團隊達成一致約定,明確標註事務方法的程式設計風格
2、保證事務方法的執行時間儘可能短,不要穿插其他網路操作(RPC/HTTP請求),或者剝離到事務方法外部
3、不是所有的方法都需要事務,如只有一條修改操作或者只讀操作不需要事務。
事務方法巢狀,是宣告式事務獨有的概念,主要體現在其傳播行為上。

什麼時候回滾事務?

丟擲執行期異常(RuntimeException)可以回滾,如果丟擲非執行期異常(部分成功、部分失敗),則不會回滾,所以拋異常的時候一定要小心不當的try-catch;

實現:在上面的spring-service中新增對事物的配置
這裡寫圖片描述

<!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
         <!--注入資料庫連線池,在spring-dao.xml中已經配置,此處引用即可-->
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--配置基於註解的宣告式事務,預設使用註解來管理事務行為-->

然後,在Service介面實現類的方法中,在需要事務宣告的方法上加上事務的註解:
這裡寫圖片描述

五、Service整合測試

整合測試Dao層和Service層。

建立SeckillServiceTest.java測試類

package dao;


import dto.Exposer;
import dto.SeckillExecution;
import entity.Seckill;
import exception.RepeatKillException;
import exception.SeckillCloseException;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import service.SeckillService;

import java.util.List;

/**
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/4
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
        "classpath:spring/spring-dao.xml",
        "classpath:spring/spring-service.xml"
})
public class SeckillServiceTest {
    private final Logger logger = LoggerFactory.getLogger(SeckillServiceTest.class);

    @Autowired
    private SeckillService seckillService;


    @Test
    public void getSeckillListTest()throws Exception{
        List<Seckill> list = seckillService.getSeckillList();
        logger.info("list="+list.toString());
        System.out.println(list.toString());
        for(Seckill sk : list){
            System.out.println(sk);
        }
    }

    //完整的邏輯程式碼整合測試
    @Test
    public void testExportSeckillLogic()throws Exception{
        long id = 1001;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        //判斷秒殺是否開啟,如果開啟則保留地址和鹽值md5開始秒殺
        if(exposer.isExposed()){

            System.out.println(exposer);

            long userPhone = 15718879112L;
            String md5 = exposer.getMd5();
            try{
                SeckillExecution seckillExecution = seckillService.executeSeckill(id,userPhone,md5);
            }catch (RepeatKillException e){
                e.printStackTrace();
            }catch (SeckillCloseException e1){
                e1.printStackTrace();
            }
        }else {
            System.out.println("秒殺未開啟");
        }
    }

    //單獨執行測試
    public void testExecuteSeckill()throws Exception{
        long id = 1001;
        long phone = 15718879112L;
        String md5 = "1da8af7e7ad6829f9eb2e6f18cb45225";
        try {
            SeckillExecution seckillExecution = seckillService.executeSeckill(id, phone, md5);
            logger.info("result={}",seckillExecution);
            System.out.println(seckillExecution);
        }catch (RepeatKillException e)
        {
            e.printStackTrace();
        }catch (SeckillCloseException e1)
        {
            e1.printStackTrace();
        }
    }
}

單元測試getSeckillListTest()和getByIdTest()方法,可以查詢出秒殺的商品列表和秒殺單一商品詳情。

執行testExportSeckillLogic()進行邏輯上的整合測試:首先判斷秒殺狀態,如果在秒殺時間內則生成秒殺連線並返回exposer【 Exposer exposer=seckillService.exportSeckillUrl(id);】,繼續利用生成的URL資訊執行秒殺【SeckillExecutionexecution=seckillService.executeSeckill(id, phone, md5);】,返回一個秒殺結果,輸出秒殺結果即可。

重複秒殺會丟擲異常,不可重複秒殺,為了不使其報錯,這裡try-catch掉即可。