(二)Java高併發秒殺API之Service層

Java團長_發表於2018-12-12

上一篇分享一個整合SSM框架的高併發和商品秒殺專案



首先在編寫Service層程式碼前,我們應該首先要知道這一層到底時幹什麼的,這裡摘取來自ITEYE一位博主的原話

Service層主要負責業務模組的邏輯應用設計。同樣是首先設計介面,再設計其實現的類,接著再Spring的配置檔案中配置其實現的關聯。這樣我們就可以在應用中呼叫Service介面來進行業務處理。Service層的業務實現,具體要呼叫到已定義的DAO層的介面,封裝Service層的業務邏輯有利於通用的業務邏輯的獨立性和重複利用性,程式顯得非常簡潔。

在專案中要降低耦合的話,分層是一種很好的概念,就是各層各司其職,儘量不做不相干的事,所以Service層的話顧名思義就是業務邏輯,處理程式中的一些業務邏輯,以及呼叫DAO層的程式碼,這裡我們的DAo層就是連線資料庫的那一層,呼叫關係可以這樣表達:
View(頁面)>Controller(控制層)>Service(業務邏輯)>Dao(資料訪問)>Database(資料庫)

  • 首先還是介面的設計,設計Service秒殺商品的介面 SeckillService 首先在som.suny包下建立interfaces這個包,這個包裡面存放Service相關的介面,然後建立SeckillService介面檔案,程式碼如下:


public interface SeckillService {

 /**
  *  查詢全部的秒殺記錄.
  * @return 資料庫中所有的秒殺記錄
  */

 List<Seckill> getSeckillList();

 /**
  *   查詢單個秒殺記錄
  * @param seckillId   秒殺記錄的ID
  * @return   根據ID查詢出來的記錄資訊
  */

 Seckill getById(long seckillId);

 /**
  * 在秒殺開啟時輸出秒殺介面的地址,否則輸出系統時間跟秒殺地址
  * @param seckillId  秒殺商品Id
  * @return  根據對應的狀態返回對應的狀態實體
  */

 Exposer exportSeckillUrl(long seckillId);

 /**
  * 執行秒殺操作,有可能是失敗的,失敗我們就丟擲異常
  * @param seckillId  秒殺的商品ID
  * @param userPhone 手機號碼
  * @param md5   md5加密值
  * @return   根據不同的結果返回不同的實體資訊
  */

 SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;


建立後介面之後我們要寫實現類了,在寫實現類的時候我們肯定會碰到一個這樣的問題,你要向前端返回json資料的話,你是返回什麼樣的資料好?直接返回一個數字狀態碼或者時文字?這樣設計肯定是不好的,所以我們應該向前段返回一個實體資訊json,裡面包含了一系列的資訊,無論是哪種狀態都應該可以應對,既然是與資料庫欄位無關的類,那就不是PO了,所以我們建立一個DTO資料傳輸類,關於常見的幾種物件我的解釋如下:

  • PO: 也就是我們在為每一張資料庫表寫一個實體的類

  • VO, 對某個頁面或者展現層所需要的資料,封裝成一個實體類

  • BO, 就是業務物件,我也不是很瞭解

  • DTO, 跟VO的概念有點混淆,也是相當於頁面需要的資料封裝成一個實體類

  • POJO, 簡單的無規則java物件

com.suny下建立dto包,然後建立Exposer類,這個類是秒殺時資料庫那邊處理的結果的物件

public class Exposer {
 /*是否開啟秒殺 */
 private boolean exposed;
 /*  對秒殺地址進行加密措施  */
 private String md5;
 /* id為seckillId的商品秒殺地址   */
 private long seckillId;
 /* 系統當前的時間   */
 private LocalDateTime now;
 /* 秒殺開啟的時間   */
 private LocalDateTime start;
 /*  秒殺結束的時間  */
 private LocalDateTime end;

 public Exposer() {
 }

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

 public Exposer(boolean exposed, long seckillId, LocalDateTime now, LocalDateTime start, LocalDateTime 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 LocalDateTime getNow() {
     return now;
 }

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

 public LocalDateTime getStart() {
     return start;
 }

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

 public LocalDateTime getEnd() {
     return end;
 }

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

 @Override
 public String toString() {
     return "Exposer{" +
             "秒殺狀態=" + exposed +
             ", md5加密值='" + md5 + '\'' +
             ", 秒殺ID=" + seckillId +
             ", 當前時間=" + now +
             ", 開始時間=" + start +
             ", 結束=" + end +
             '}';
 }
}


然後我們給頁面返回的資料應該是更加友好的封裝資料,所以我們再在com.suny.dto包下再建立SeckillExecution用來封裝給頁面的結果:


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;
   }

   @Override
   public String toString() {
       return "SeckillExecution{" +
               "秒殺的商品ID=" + seckillId +
               ", 秒殺狀態=" + state +
               ", 秒殺狀態資訊='" + stateInfo + '\'' +
               ", 秒殺的商品=" + successKilled +
               '}';
   }
}
定義秒殺中可能會出現的異常
  • 定義一個基礎的異常,所有的子異常繼承這個異常SeckillException

/**
*  秒殺基礎異常
* Created by 孫
*/

public class SeckillException extends RuntimeException {
 public SeckillException(String message) {
     super(message);
 }

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


+ 首選可能會出現秒殺關閉後被秒殺情況,所以建立秒殺關閉異常`SeckillCloseException`,需要繼承我們一開始寫的基礎異常


/**
* 秒殺已經關閉異常,當秒殺結束就會出現這個異常
* Created by 孫
*/

public class SeckillCloseException extends SeckillException{
   public SeckillCloseException(String message) {
       super(message);
   }

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


  • 然後還有可能發生重複秒殺異常RepeatKillException


/**
* 重複秒殺異常,不需要我們手動去try catch
* Created by 孫
*/

public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
    super(message);
}

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

實現Service介面

@Service
public class SeckillServiceImpl implements SeckillService {
   private Logger logger = LoggerFactory.getLogger(this.getClass());
   /* 加入一個鹽值,用於混淆*/
   private final String salt = "thisIsASaltValue";

   @Autowired
   private SeckillMapper seckillMapper;
   @Autowired
   private SuccessKilledMapper successKilledMapper;


   /**
    * 查詢全部的秒殺記錄.
    *
    * @return 資料庫中所有的秒殺記錄
    */

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

   /**
    * 查詢單個秒殺記錄
    *
    * @param seckillId 秒殺記錄的ID
    * @return 根據ID查詢出來的記錄資訊
    */

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

   /**
    * 在秒殺開啟時輸出秒殺介面的地址,否則輸出系統時間跟秒殺地址
    *
    * @param seckillId 秒殺商品Id
    * @return 根據對應的狀態返回對應的狀態實體
    */

   @Override
   public Exposer exportSeckillUrl(long seckillId) {
       // 根據秒殺的ID去查詢是否存在這個商品
      /* Seckill seckill = seckillMapper.queryById(seckillId);
       if (seckill == null) {
           logger.warn("查詢不到這個秒殺產品的記錄");
           return new Exposer(false, seckillId);
       }*/

       Seckill seckill = redisDao.getSeckill(seckillId);
       if (seckill == null) {
           // 訪問資料庫讀取資料
           seckill = seckillMapper.queryById(seckillId);
           if (seckill == null) {
               return new Exposer(false, seckillId);
           } else {
               // 放入redis
               redisDao.putSeckill(seckill);
           }
       }

       // 判斷是否還沒到秒殺時間或者是過了秒殺時間
       LocalDateTime startTime = seckill.getStartTime();
       LocalDateTime endTime = seckill.getEndTime();
       LocalDateTime nowTime = LocalDateTime.now();
       //   開始時間大於現在的時候說明沒有開始秒殺活動    秒殺活動結束時間小於現在的時間說明秒殺已經結束了
      /* if (!nowTime.isAfter(startTime)) {
           logger.info("現在的時間不在開始時間後面,未開啟秒殺");
           return new Exposer(false, seckillId, nowTime, startTime, endTime);
       }
       if (!nowTime.isBefore(endTime)) {
           logger.info("現在的時間不在結束的時間之前,可以進行秒殺");
           return new Exposer(false, seckillId, nowTime, startTime, endTime);
       }*/

       if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
           //秒殺開啟,返回秒殺商品的id,用給介面加密的md5
           String md5 = getMd5(seckillId);
           return new Exposer(true, md5, seckillId);
       }
       return new Exposer(false, seckillId, nowTime, startTime, endTime);


   }

   private String getMd5(long seckillId) {
       String base = seckillId + "/" + salt;
       return DigestUtils.md5DigestAsHex(base.getBytes());
   }

   /**
    * 執行秒殺操作,失敗的,失敗我們就丟擲異常
    *
    * @param seckillId 秒殺的商品ID
    * @param userPhone 手機號碼
    * @param md5       md5加密值
    * @return 根據不同的結果返回不同的實體資訊
    */

   @Override
   public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
       if (md5 == null || !md5.equals(getMd5(seckillId))) {
           logger.error("秒殺資料被篡改");
           throw new SeckillException("seckill data rewrite");
       }
       // 執行秒殺業務邏輯
       LocalDateTime nowTIme = LocalDateTime.now();

       try {
           //執行減庫存操作
           int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
           if (reduceNumber <= 0) {
               logger.warn("沒有更新資料庫記錄,說明秒殺結束");
               throw new SeckillCloseException("seckill is closed");
           } else {
               // 這裡至少減少的數量不為0了,秒殺成功了就增加一個秒殺成功詳細
               int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
               // 檢視是否被重複插入,即使用者是否重複秒殺
               if (insertCount <= 0) {
                   throw new RepeatKillException("seckill repeated");
               } else {
                   // 秒殺成功了,返回那條插入成功秒殺的資訊
                   SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
//                    return new SeckillExecution(seckillId,1,"秒殺成功");
                     return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);
               }
           }
       } catch (SeckillCloseException | RepeatKillException e1) {
           throw e1;
       } catch (Exception e) {
           logger.error(e.getMessage(), e);
           // 把編譯期異常轉換為執行時異常
           throw new SeckillException("seckill inner error : " + e.getMessage());
       }
}


在這裡我們捕獲了執行時異常,這樣做的原因就是Spring的事物預設就是發生了RuntimeException才會回滾,可以檢測出來的異常是不會導致事物的回滾的,這樣的目的就是你明知道這裡會發生異常,所以你一定要進行處理.如果只是為了讓編譯通過的話,那捕獲異常也沒多意思,所以這裡要注意事物的回滾.
然後我們還發現這裡存在硬編碼的現象,就是返回各種字元常量,例如秒殺成功,秒殺失敗等等,這些字串時可以被重複使用的,而且這樣維護起來也不方便,要到處去類裡面尋找這樣的字串,所有我們使用列舉類來管理這樣狀態,在con.suny包下建立enum包,專門放置列舉類,然後再建立SeckillStatEnum列舉類:


/**
* 常量列舉類
* Created by 孫
*/

public enum SeckillStatEnum {
   SUCCESS(1, "秒殺成功"),
   END(0, "秒殺結束"),
   REPEAT_KILL(-1, "重複秒殺"),
   INNER_ERROR(-2, "系統異常"),
   DATE_REWRITE(-3, "資料篡改");

   private int state;
   private String info;

   SeckillStatEnum() {
   }

   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 statEnum : values()) {
           if (statEnum.getState() == index) {
               return statEnum;
           }
       }
       return null;
   }
}


既然把這些改成了列舉,那麼在SeckillServiceImpl類中的executeSeckill方法中成功秒殺的返回值就應該修改為


return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);


改了這裡以後會發現會報錯,因為在實體類那邊建構函式可不是這樣的,然後修改SeckillExecution類的建構函式,把statestateInfo的值設定從建構函式裡面的SeckillStatEnum中取出值來設定:


/*  秒殺成功返回的實體  */
   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();
   }

下一步肯定要注入Service了

首先在resources/spring下建立applicationContext-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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"
>


   <!--配置自動掃描service包下的註解,在這裡配置了自動掃描後,com.suny.service包下所有帶有@Service註解的類都會被加入Spring容器中-->
   <context:component-scan base-package="com.suny.service"/>

   <!--配置事物,這裡時使用基於註解的事物-->
   <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
       <!--注入資料庫連線池-->
       <property name="dataSource" ref="dataSource"/>
   </bean>

   <!--開啟基於註解的申明式事物-->
   <tx:annotation-driven transaction-manager="transactionManager"/>

</beans>


在這裡開啟了基於註解的事物,常見的事物操作有以下幾種方法

  • 在Spring早期版本中是使用ProxyFactoryBean+XMl方式來配置事物.

  • 在Spring配置檔案使用tx:advice+aop名稱空間,好處就是一次配置永久生效,你無須去關心中間出的問題,不過出錯了你很難找出來在哪裡出了問題

  • 註解@Transactional的方式,註解可以在方法定義,介面定義,類定義,public方法上,但是不能註解在private,final,static等方法上,因為Spring的事物管理預設是使用Cglib動態代理的:

    • private方法因為訪問許可權限制,無法被子類覆蓋

    • final方法無法被子類覆蓋

    • static時類級別的方法,無法被子類覆蓋

    • protected方法可以被子類覆蓋,因此可以被動態位元組碼增強

不能被Spring AOP事物增強的方法
序號動態代理策略不能被事物增強的方法
1基於介面的動態代理出了public以外的所有方法,並且 public static 的方法也不能被增強
2基於Cglib的動態代理private,static,final的方法

然後你要在Service類上新增註解@Service,不用在介面上新增註解:

@Service
public class SeckillServiceImpl implements SeckillService


既然已經開啟了基於註解的事物,那我們就去需要被事物的方法上加個註解@Transactional吧:


@Transactional
   @Override
   public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException

Service層的測試

寫測試類,我這裡的測試類名為SeckillServiceImplTest:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/applicationContext-dao.xml", "classpath:spring/applicationContext-service.xml"})
public class SeckillServiceImplTest {
   private Logger logger = LoggerFactory.getLogger(this.getClass());

   @Autowired
   private SeckillService seckillService;

   @Test
   public void getSeckillList() throws Exception {
       List<Seckill> seckillList = seckillService.getSeckillList();
       logger.info(seckillList.toString());
       System.out.println(seckillList.toString());
   }

   @Test
   public void getById() throws Exception {
       long seckillId = 1000;
       Seckill byId = seckillService.getById(seckillId);
       System.out.println(byId.toString());
   }

   @Test
   public void exportSeckillUrl() throws Exception {
       long seckillId = 1000;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       System.out.println(exposer.toString());
   }

   @Test
   public void executeSeckill() throws Exception {
       long seckillId = 1000;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       if (exposer.isExposed()) {
           long userPhone = 12222222222L;
           String md5 = "bf204e2683e7452aa7db1a50b5713bae";
           try {
               SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
               System.out.println(seckillExecution.toString());
           } catch (SeckillCloseException | RepeatKillException e) {
               e.printStackTrace();
           }
       } else {
           System.out.println("秒殺未開啟");
       }
   }

   @Test
   public void executeSeckillProcedureTest() {
       long seckillId = 1001;
       long phone = 1368011101;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       if (exposer.isExposed()) {
           String md5 = exposer.getMd5();
           SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
           System.out.println(execution.getStateInfo());
       }
   }


}


測試的話如果每個方法測試都通過就說明通過,如果報錯了話就仔細看下哪一步錯了檢查下




PS:如果覺得我的分享不錯,歡迎大家隨手點贊、轉發。


640?

Java團長

專注於Java乾貨分享

640

掃描上方二維碼獲取更多Java乾貨

相關文章