三、高併發秒殺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掉即可。