Java高并發秒殺系統API(二)之Service層開發

開始Service層的編碼之前,我們首先需要進行Dao層編碼之后的思考:在Dao層我們只完成了針對表的相關操作包括寫了接口方法和映射文件中的sql語句,并沒有編寫邏輯的代碼,例如對多個Dao層方法的拼接,當我們用戶成功秒殺商品時我們需要進行商品的減庫存操作(調用SeckillDao接口)和增加用戶明細(調用SuccessKilledDao接口),這些邏輯我們都需要在Service層完成。這也是一些初學者容易出現的錯誤,他們喜歡在Dao層進行邏輯的編寫,其實Dao就是數據訪問的縮寫,它只進行數據的訪問操作,接下來我們便進行Service層代碼的編寫。

1.秒殺Service接口設計

在cn.codingxiaxw包下創建一個service包用于存放我們的Service接口和其實現類,創建一個exception包用于存放service層出現的異常例如重復秒殺商品異常、秒殺已關閉等異常,一個dto包作為傳輸層,dto和entity的區別在于:entity用于業務數據的封裝,而dto用于完成web和service層的數據傳遞。

首先創建我們Service接口,里面的方法應該是按"使用者"(程序員)的角度去設計,SeckillService.java,代碼如下:

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
     * @return
     */
    SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)
            throws SeckillException,RepeatKillException,SeckillCloseException;
}

該接口中前面兩個方法返回的都是跟我們業務相關的對象,而后兩個方法返回的對象與業務不相關,這兩個對象我們用于封裝service和web層傳遞的數據,方法的作用我們已在注釋中給出。相應在的dto包中創建Exposer.java,用于封裝秒殺的地址信息,各個屬性的作用在代碼中已給出注釋,代碼如下:

/**
 * Created by codingBoy on 16/11/27.
 * 暴露秒殺地址(接口)DTO
 */
public class Exposer {
    
    //是否開啟秒殺
    private boolean exposed;

    //對秒殺地址加密措施
    private String md5;

    //id為seckillId的商品的秒殺地址
    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.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;
    }
}

和SeckillExecution.java,用于判斷秒殺是否成功,成功就返回秒殺成功的所有信息(包括秒殺的商品id、秒殺成功狀態、成功信息、用戶明細),失敗就拋出一個我們允許的異常(重復秒殺異常、秒殺結束異常),代碼如下:

/**
 * 封裝執行秒殺后的結果:是否秒殺成功
 * Created by codingBoy on 16/11/27.
 */
public class SeckillExecution {

    private long seckillId;

    //秒殺執行結果的狀態
    private int state;

    //狀態的明文標識
    private String stateInfo;

    //當秒殺成功時,需要傳遞秒殺成功的對象回去
    private SuccessKilled successKilled;

    //秒殺成功返回所有信息
    public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = state;
        this.stateInfo = stateInfo;
        this.successKilled = successKilled;
    }

    //秒殺失敗
    public SeckillExecution(long seckillId, int state, String stateInfo) {
        this.seckillId = seckillId;
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public long getSeckillId() {
        return seckillId;
    }

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

    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 SuccessKilled getSuccessKilled() {
        return successKilled;
    }

    public void setSuccessKilled(SuccessKilled successKilled) {
        this.successKilled = successKilled;
    }
}

然后需要創建我們在秒殺業務過程中允許的異常,重復秒殺異常RepeatKillException.java:

/**
 * 重復秒殺異常,是一個運行期異常,不需要我們手動try catch
 * Mysql只支持運行期異常的回滾操作
 * Created by codingBoy on 16/11/27.
 */
public class RepeatKillException extends SeckillException {

    public RepeatKillException(String message) {
        super(message);
    }

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

秒殺關閉異常SeckillCloseException.java:

/**
 * 秒殺關閉異常,當秒殺結束時用戶還要進行秒殺就會出現這個異常
 * Created by codingBoy on 16/11/27.
 */
public class SeckillCloseException extends SeckillException{
    public SeckillCloseException(String message) {
        super(message);
    }

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

和一個異常包含與秒殺業務所有出現的異常SeckillException.java:

/**
 * 秒殺相關的所有業務異常
 * Created by codingBoy on 16/11/27.
 */
public class SeckillException extends RuntimeException {
    public SeckillException(String message) {
        super(message);
    }

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

到此,接口的工作便完成,接下來進行接口實現類的編碼工作。

2.秒殺Service接口的實現

在service包下創建impl包存放它的實現類,SeckillServiceImpl.java,內容如下:

public class SeckillServiceImpl implements SeckillService
{
    //日志對象
    private Logger logger= LoggerFactory.getLogger(this.getClass());

    //加入一個混淆字符串(秒殺接口)的salt,為了我避免用戶猜出我們的md5值,值任意給,越復雜越好
    private final String salt="shsdssljdd'l.";

    //注入Service依賴
    @Autowired //@Resource
    private SeckillDao seckillDao;

    @Autowired //@Resource
    private SuccessKilledDao successKilledDao;

    public List<Seckill> getSeckillList() {
        return seckillDao.queryAll(0,4);
    }

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

    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 (startTime.getTime()>nowTime.getTime() || endTime.getTime()<nowTime.getTime())
        {
            return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
        }

        //秒殺開啟,返回秒殺商品的id、用給接口加密的md5
        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;
    }

    //秒殺是否成功,成功:減庫存,增加明細;失敗:拋出異常,事務回滾
    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,1,"秒殺成功",successKilled);
                }
            }

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

    }
}

對上述代碼進行分析一下,在return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);代碼中,我們返回的state和stateInfo參數信息應該是輸出給前端的,但是我們不想在我們的return代碼中硬編碼這兩個參數,所以我們應該考慮用枚舉的方式將這些常量封裝起來,在cn.codingxiaxw包下新建一個枚舉包enums,創建一個枚舉類型SeckillStatEnum.java,內容如下:

public enum SeckillStatEnum {

    SUCCESS(1,"秒殺成功"),
    END(0,"秒殺結束"),
    REPEAT_KILL(-1,"重復秒殺"),
    INNER_ERROR(-2,"系統異常"),
    DATE_REWRITE(-3,"數據篡改");

    private int state;
    private String info;

    SeckillStatEnum(int state, String info) {
        this.state = state;
        this.info = info;
    }

    public int getState() {
        return state;
    }


    public String getInfo() {
        return info;
    }


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

然后修改執行秒殺操作的非業務類SeckillExecution.java里面涉及到state和stateInfo參數的構造方法:

   //秒殺成功返回所有信息
    public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = statEnum.getState();
        this.stateInfo = statEnum.getInfo();
        this.successKilled = successKilled;
    }

    //秒殺失敗
    public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
        this.seckillId = seckillId;
        this.state = statEnum.getState();
        this.stateInfo = statEnum.getInfo();
    }

然后便可修改實現類方法中的返回語句為:return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);,保證了一些常用常量數據被封裝在枚舉類型里。

目前為止我們Service的實現全部完成,接下來要將Service交給Spring的容器托管,進行一些配置。

3.使用Spring托管Service依賴配置

在spring包下創建一個spring-service.xml文件,內容如下:

<?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"
       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.xsd">

    <!--掃描service包下所有使用注解的類型-->
    <context:component-scan base-package="cn.codingxiaxw.service"/>
    
</beans>

然后采用注解的方式將Service的實現類加入到Spring IOC容器中:

//@Component @Service @Dao @Controller
@Service
public class SeckillServiceImpl implements SeckillService

下面我們來運用Spring的聲明式事務對我們項目中的事務進行管理。

4.使用Spring聲明式事務配置

聲明式事務的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空間,這種配置的好處就是一次配置永久生效。3.注解@Transactional的方式。在實際開發中,建議使用第三種對我們的事務進行控制,優點見下面代碼中的注釋。下面讓我們來配置聲明式事務,在spring-service.xml中添加對事務的配置:

  <!--配置事務管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入數據庫連接池-->
        <property name="dataSource" ref="dataSource"/>

    </bean>

    <!--配置基于注解的聲明式事務
    默認使用注解來管理事務行為-->
    <tx:annotation-driven transaction-manager="transactionManager"/>

然后在Service實現類的方法中,在需要進行事務聲明的方法上加上事務的注解:

 //秒殺是否成功,成功:減庫存,增加明細;失敗:拋出異常,事務回滾
    @Transactional
    /**
     * 使用注解控制事務方法的優點:
     * 1.開發團隊達成一致約定,明確標注事務方法的編程風格
     * 2.保證事務方法的執行時間盡可能短,不要穿插其他網絡操作RPC/HTTP請求或者剝離到事務方法外部
     * 3.不是所有的方法都需要事務,如只有一條修改操作、只讀操作不要事務控制
     */
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException {}

下面針對我們之前做的業務實現類來做集成測試。

5.使用集成測試Service邏輯

在SeckillService接口中使用IDEA快捷鍵shift+command+T,快速生成junit測試類。Service實現類中前面兩個方法很好實現,獲取列表或者列表中的一個商品的信息即可,測試如下:

@RunWith(SpringJUnit4ClassRunner.class)
//告訴junit spring的配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml",
                        "classpath:spring/spring-service.xml"})

public class SeckillServiceTest {

    private final Logger logger= LoggerFactory.getLogger(this.getClass());

    @Autowired
    private SeckillService seckillService;

    @Test
    public void getSeckillList() throws Exception {
        List<Seckill> seckills=seckillService.getSeckillList();
        System.out.println(seckills);

    }

    @Test
    public void getById() throws Exception {

        long seckillId=1000;
        Seckill seckill=seckillService.getById(seckillId);
        System.out.println(seckill);
    }
}

重點就是exportSeckillUrl()方法和executeSeckill()方法的測試,接下來我們進行exportSeckillUrl()方法的測試,如下:

 @Test
    public void exportSeckillUrl() throws Exception {

        long seckillId=1000;
        Exposer exposer=seckillService.exportSeckillUrl(seckillId);
        System.out.println(exposer);

    }

控制臺中輸入如下信息:

Exposer{exposed=false, md5='null', seckillId=1000, now=1480322072410, start=1451577600000, end=1451664000000}

沒有給我們返回id為1000的商品秒殺地址,是因為我們當前的時間并不在秒殺時間開啟之內,所以該商品還沒有開啟。需要修改數據庫中該商品秒殺活動的時間在我們測試時的當前時間之內,然后再進行該方法的測試,控制臺中輸出如下信息:

Exposer{exposed=true, md5='bf204e2683e7452aa7db1a50b5713bae', seckillId=1000, now=0, start=0, end=0}

可知開啟了id為1000的商品的秒殺,并給我們輸出了該商品的秒殺地址。而第四個方法的測試就需要傳入該地址讓用戶得到才能判斷該用戶是否秒殺到該地址的商品,然后進行第四個方法的測試,如下:

  @Test
    public void executeSeckill() throws Exception {

        long seckillId=1000;
        long userPhone=13476191876L;
        String md5="bf204e2683e7452aa7db1a50b5713bae";

        SeckillExecution seckillExecution=seckillService.executeSeckill(seckillId,userPhone,md5);

        System.out.println(seckillExecution);

    }

控制臺輸出信息:

SeckillExecution{seckillId=1000, state=1, stateInfo='秒殺成功', successKilled=SuccessKilled{seckillId=1000, userPhone=13476191876, state=0, createTime=Mon Nov 28 16:45:38 CST 2016}}

證明電話為13476191876的用戶成功秒殺到了該商品,查看數據庫,該用戶秒殺商品的明細信息已經被插入明細表,說明我們的業務邏輯沒有問題。但其實這樣寫測試方法還有點問題,此時再次執行該方法,控制臺報錯,因為用戶重復秒殺了。我們應該在該測試方法中添加try catch,將程序允許的異常包起來而不去向上拋給junit,更改測試代碼如下:

   @Test
    public void executeSeckill() throws Exception {

        long seckillId=1000;
        long userPhone=13476191876L;
        String md5="bf204e2683e7452aa7db1a50b5713bae";

        try {
            SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);

            System.out.println(seckillExecution);
        }catch (RepeatKillException e)
        {
            e.printStackTrace();
        }catch (SeckillCloseException e1)
        {
            e1.printStackTrace();
        }
    }

這樣再測試該方法,junit便不會再在控制臺中報錯,而是認為這是我們系統允許出現的異常。由上分析可知,第四個方法只有拿到了第三個方法暴露的秒殺商品的地址后才能進行測試,也就是說只有在第三個方法運行后才能運行測試第四個方法,而實際開發中我們不是這樣的,需要將第三個測試方法和第四個方法合并到一個方法從而組成一個完整的邏輯流程:

@Test//完整邏輯代碼測試,注意可重復執行
    public void testSeckillLogic() throws Exception {
        long seckillId=1000;
        Exposer exposer=seckillService.exportSeckillUrl(seckillId);
        if (exposer.isExposed())
        {

            System.out.println(exposer);

            long userPhone=13476191876L;
            String md5=exposer.getMd5();

            try {
                SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
                System.out.println(seckillExecution);
            }catch (RepeatKillException e)
            {
                e.printStackTrace();
            }catch (SeckillCloseException e1)
            {
                e1.printStackTrace();
            }
        }else {
            //秒殺未開啟
            System.out.println(exposer);
        }
    }

運行該測試類,控制臺成功輸出信息,庫存會減少,明細表也會增加內容。重復執行,控制臺不會報錯,只是會拋出一個允許的重復秒殺異常。

目前為止,Dao層和Service層的集成測試我們都已經完成,接下來進行Web層的開發編碼工作,請查看我的下篇文章Java高并發秒殺API之Web層開發

6.聯系

If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容