(二)Java高併發秒殺API之Service層
首先在編寫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
類的建構函式,把state
跟stateInfo
的值設定從建構函式裡面的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:如果覺得我的分享不錯,歡迎大家隨手點贊、轉發。
Java團長
專注於Java乾貨分享
掃描上方二維碼獲取更多Java乾貨
相關文章
- (三)Java高併發秒殺系統API之Web層開發JavaAPIWeb
- (四)Java高併發秒殺API之高併發優化JavaAPI優化
- SpringBoot實現Java高併發秒殺系統之Web層開發(三)Spring BootJavaWeb
- Java高併發秒殺系統【觀後總結】Java
- SSM框架實現高併發秒殺API學習筆記SSM框架API筆記
- 高併發秒殺系統架構詳解,不是所有的秒殺都是秒殺!架構
- RocketMQ實戰--高併發秒殺場景MQ
- 【高併發】秒殺系統架構解密,不是所有的秒殺都是秒殺(升級版)!!架構解密
- 高併發秒殺專案隨手筆記筆記
- 如何設計一個高可用、高併發秒殺系統
- 【高併發】Redis如何助力高併發秒殺系統,看完這篇我徹底懂了!!Redis
- Redis 實現高併發下的搶購 / 秒殺功能Redis
- PHP高併發商品秒殺問題的解決方案PHP
- 使用Redis構建高併發高可靠的秒殺拍賣系統 - LuisRedisUI
- 高併發業務場景下的秒殺解決方案 (初探)
- 高併發下秒殺商品,必須知道的9個細節
- 分享一個整合SSM框架的高併發和商品秒殺專案SSM框架
- 短影片直播系統,實現高併發秒殺的多種方式
- Redis+Lua解決高併發場景搶購秒殺問題Redis
- 6步帶你用Spring Boot開發出商城高併發秒殺系統Spring Boot
- Java高併發之CyclicBarrier簡介Java
- Java高併發之synchronized關鍵字Javasynchronized
- java高併發之ConcurrentSkipListMap的那些事Java
- 絕了!雙11千億流量「高併發秒殺架構設計」先睹為快架構
- PHP高併發 商品秒殺 問題的 2大種(MySQL or Redis) 解決方案PHPMySqlRedis
- Java併發之AQS原始碼分析(二)JavaAQS原始碼
- Java併發之ReentrantReadWriteLock原始碼解析(二)Java原始碼
- Java併發之Semaphore原始碼解析(二)Java原始碼
- Java併發之ReentrantLock原始碼解析(二)JavaReentrantLock原始碼
- Java併發之ThreadPoolExecutor原始碼解析(二)Javathread原始碼
- Java 高併發思路Java
- Java併發程式設計序列之JUC底層AQSJava程式設計AQS
- java併發系列——底層CPUJava
- 走進高併發(二)Java並行程式基礎Java並行行程
- 【併發程式設計】(二)Java併發機制底層實現原理——synchronized關鍵字程式設計Javasynchronized
- JAVA開發之簡化Dao層、提高開發效率(二)Java
- 並行化-你的高併發大殺器並行
- java併發之ConcurrentLinkedQueueJava