Java秒殺系統(tǒng):web層詳解
設計Restful接口
根據(jù)需求設計前端交互流程。
三個職位:
- 產(chǎn)品:解讀用戶需求,搞出需求文檔
- 前端:不同平臺的頁面展示
- 后端:存儲、展示、處理數(shù)據(jù)
前端頁面流程:

詳情頁流程邏輯:

標準系統(tǒng)時間從服務器獲取。
Restful:一種優(yōu)雅的URI表述方式、資源的狀態(tài)和狀態(tài)轉移。
Restful規(guī)范:
- GET 查詢操作
- POST 添加/修改操作(非冪等)
- PUT 修改操作(冪等,沒有太嚴格區(qū)分)
- DELETE 刪除操作
URL設計:
/模塊/資源/{標示}/集合/...
/user/{uid}/friends -> 好友列表
/user/{uid}/followers -> 關注者列表
秒殺API的URL設計
GET /seckill/list 秒殺列表
GET /seckill/{id}/detail 詳情頁
GET /seckill/time/now 系統(tǒng)時間
POST /seckill/{id}/exposer 暴露秒殺
POST /seckill/{id}/{md5}/execution 執(zhí)行秒殺
下一步就是如何實現(xiàn)這些URL接口。
SpringMVC
理論

適配器模式(Adapter Pattern),把一個類的接口變換成客戶端所期待的另一種接口, Adapter模式使原本因接口不匹配(或者不兼容)而無法在一起工作的兩個類能夠在一起工作。
SpringMVC的handler(Controller,HttpRequestHandler,Servlet等)有多種實現(xiàn)方式,例如繼承Controller的,基于注解控制器方式的,HttpRequestHandler方式的。由于實現(xiàn)方式不一樣,調(diào)用方式就不確定了。
看HandlerAdapter接口有三個方法:
// 判斷該適配器是否支持這個HandlerMethod boolean supports(Object handler); // 用來執(zhí)行控制器處理函數(shù),獲取ModelAndView 。就是根據(jù)該適配器調(diào)用規(guī)則執(zhí)行handler方法。 ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; long getLastModified(HttpServletRequest request, Object handler);
問流程如上圖,用戶訪問一個請求,首先經(jīng)過DispatcherServlet轉發(fā)。利用HandlerMapping得到想要的HandlerExecutionChain(里面包含handler和一堆攔截器)。然后利用handler,得到HandlerAdapter,遍歷所有注入的HandlerAdapter,依次使用supports方法尋找適合這個handler的適配器子類。最后通過這個獲取的適配器子類運用handle方法調(diào)用控制器函數(shù),返回ModelAndView。
注解映射技巧
- 支持標準的URL
- ?和*和**等字符,如/usr/*/creation會匹配/usr/AAA/creation和/usr/BBB/creation等。/usr/**/creation會匹配/usr/creation和/usr/AAA/BBB/creation等URL。帶{xxx}占位符的URL。
- 如/usr/{userid}匹配/usr/123、/usr/abc等URL.
請求方法細節(jié)處理
- 請求參數(shù)綁定
- 請求方式限制
- 請求轉發(fā)和重定向
- 數(shù)據(jù)模型賦值
- 返回json數(shù)據(jù)
- cookie訪問

返回json數(shù)據(jù)

cookie訪問:

項目整合SpringMVC
web.xml下配置springmvc需要加載的配置文件:
<!--?xml version="1.0" encoding="UTF-8"?-->
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true">
<!--修改servlet版本為3.1-->
<!--配置DispatcherServlet-->
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--配置springmvc需要加載的配置文件
spring-dao.xml spring-service.xml spring-web.xml-->
<!--整合:mybatis -> spring -> springmvc-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
在resources文件夾下的spring文件夾添加spring-web.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" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemalocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置springmvc-->
<!--1. 開啟springmvc注解模式-->
<!-- 簡化配置,
自動注冊handlermapping,handleradapter
默認提供了一系列功能:數(shù)據(jù)綁定,數(shù)字和日期的format,xml和json的讀寫支持
-->
<mvc:annotation-driven>
<!--servlet-mapping 映射路徑:"/"-->
<!--2. 靜態(tài)資源默認servlet配置
靜態(tài)資源處理:js,gif,png..
允許使用/做整體映射
-->
<mvc:default-servlet-handler>
<!--3. jsp的顯示viewResolver-->
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView">
<property name="prefix" value="/WEB-INF/jsp">
<property name="suffix" value=".jsp">
</property></property></property></bean>
<!--4. 掃描web相關的bean-->
<context:component-scan base-package="cn.orzlinux.web">
</context:component-scan></mvc:default-servlet-handler></mvc:annotation-driven></beans>
使用SpringMVC實現(xiàn)Restful接口
新建文件:

首先是SeckillResult.java,這個保存controller的返回結果,做一個封裝。
// 所有ajax請求返回類型,封裝json結果
public class SeckillResult<t> {
private boolean success; //是否執(zhí)行成功
private T data; // 攜帶數(shù)據(jù)
private String error; // 錯誤信息
// getter setter contructor
}
在Seckillcontroller.java中,實現(xiàn)了我們之前定義的幾個URL:
GET /seckill/list 秒殺列表
GET /seckill/{id}/detail 詳情頁
GET /seckill/time/now 系統(tǒng)時間
POST /seckill/{id}/exposer 暴露秒殺
POST /seckill/{id}/{md5}/execution 執(zhí)行秒殺
具體代碼如下:
@Controller // @Service @Component放入spring容器
@RequestMapping("/seckill") // url:模塊/資源/{id}/細分
public class SeckillController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SecKillService secKillService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public String list(Model model) {
// list.jsp + model = modelandview
List<seckill> list = secKillService.getSecKillList();
model.addAttribute("list",list);
return "list";
}
@RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)
public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
if (seckillId == null) {
// 0. 不存在就重定向到list
// 1. 重定向訪問服務器兩次
// 2. 重定向可以重定義到任意資源路徑。
// 3. 重定向會產(chǎn)生一個新的request,不能共享request域信息與請求參數(shù)
return "redrict:/seckill/list";
}
SecKill secKill = secKillService.getById(seckillId);
if (secKill == null) {
// 0. 為了展示效果用forward
// 1. 轉發(fā)只訪問服務器一次。
// 2. 轉發(fā)只能轉發(fā)到自己的web應用內(nèi)
// 3. 轉發(fā)相當于服務器跳轉,相當于方法調(diào)用,在執(zhí)行當前文件的過程中轉向執(zhí)行目標文件,
// 兩個文件(當前文件和目標文件)屬于同一次請求,前后頁 共用一個request,可以通
// 過此來傳遞一些數(shù)據(jù)或者session信息
return "forward:/seckill/list";
}
model.addAttribute("seckill",secKill);
return "detail";
}
// ajax json
@RequestMapping(value = "/{seckillId}/exposer",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF8"})
@ResponseBody
public SeckillResult<exposer> exposer(Long seckillId) {
SeckillResult<exposer> result;
try {
Exposer exposer = secKillService.exportSecKillUrl(seckillId);
result = new SeckillResult<exposer>(true,exposer);
} catch (Exception e) {
logger.error(e.getMessage(),e);
result = new SeckillResult<>(false,e.getMessage());
}
return result;
}
@RequestMapping(value = "/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF8"})
public SeckillResult<seckillexecution> execute(
@PathVariable("seckillId") Long seckillId,
// required = false表示cookie邏輯由我們程序處理,springmvc不要報錯
@CookieValue(value = "killPhone",required = false) Long userPhone,
@PathVariable("md5") String md5) {
if (userPhone == null) {
return new SeckillResult<seckillexecution>(false, "未注冊");
}
SeckillResult<seckillexecution> result;
try {
SeckillExecution execution = secKillService.executeSeckill(seckillId, userPhone, md5);
result = new SeckillResult<seckillexecution>(true, execution);
return result;
} catch (SeckillCloseException e) { // 秒殺關閉
SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.END);
return new SeckillResult<seckillexecution>(false,execution);
} catch (RepeatKillException e) { // 重復秒殺
SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.REPEAT_KILL);
return new SeckillResult<seckillexecution>(false,execution);
} catch (Exception e) {
// 不是重復秒殺或秒殺結束,就返回內(nèi)部錯誤
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.INNER_ERROR);
return new SeckillResult<seckillexecution>(false,execution);
}
}
@RequestMapping(value = "/time/now",method = RequestMethod.GET)
@ResponseBody
public SeckillResult<long> time() {
Date now = new Date();
return new SeckillResult<long>(true,now.getTime());
}
}
頁面

這里修改數(shù)據(jù)庫為合適的時間來測試我們的代碼。
點擊后跳轉到詳情頁。

詳情頁涉及到比較多的交互邏輯,如cookie,秒殺成功失敗等等。放到邏輯交互一節(jié)來說。
運行時發(fā)現(xiàn)jackson版本出現(xiàn)問題,pom.xml修改為:
<dependency> <groupid>com.fasterxml.jackson.core</groupid> <artifactid>jackson-databind</artifactid> <version>2.10.2</version> </dependency>
list.jsp代碼為:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--引入jstl--%>
<%--標簽通用頭,寫在一個具體文件,直接靜態(tài)包含--%>
<%@include file="common/tag.jsp"%>
<title>Bootstrap 模板</title>
<%--靜態(tài)包含:會合并過來放到這,和當前文件一起作為整個輸出--%>
<%@include file="common/head.jsp"%>
<%--頁面顯示部分--%>
<div class="container">
<div class="panel panel-default">
<div class="panel panel-heading text-center">
<h1>秒殺列表</h1>
</div>
<div class="panel-body">
<c:foreach var="sk" items="${list}">
</c:foreach><table class="table table-hover">
<thead>
<tr>
<th>名稱</th>
<th>庫存</th>
<th>開始時間</th>
<th>結束時間</th>
<th>創(chuàng)建時間</th>
<th>詳情頁</th>
</tr>
</thead>
<tbody>
<tr>
<td>${sk.name}</td>
<td>${sk.number}</td>
<td>
<fmt:formatdate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss">
</fmt:formatdate></td>
<td>
<fmt:formatdate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss">
</fmt:formatdate></td>
<td>
<fmt:formatdate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss">
</fmt:formatdate></td>
<td>
<a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">
link
</a>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<!-- jQuery (Bootstrap 的 JavaScript 插件需要引入 jQuery) -->
<script src="https://code.jquery.com/jquery.js"></script>
<!-- 包括所有已編譯的插件 -->
<script src="js/bootstrap.min.js"></script>

邏輯交互
身份認證
cookie中沒有手機號要彈窗,手機號不正確(11位數(shù)字)要提示錯誤:

選擇提交之后要能夠在cookie中看到:

目前為止detail.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<title>秒殺詳情頁</title>
<%--靜態(tài)包含:會合并過來放到這,和當前文件一起作為整個輸出--%>
<%@include file="common/head.jsp"%>
<link rel="stylesheet">
<%--<input type="hidden" id="basePath" value="${basePath}">--%>
<div class="container">
<div class="panel panel-default text-center">
<h1>
<div class="panel-heading">${seckill.name}</div>
</h1>
</div>
<div class="panel-body">
<h2 class="text-danger">
<!-- 顯示time圖標 -->
<span class="glyphicon glyphicon-time"></span>
<!-- 展示倒計時 -->
<span class="glyphicon" id="seckillBox"></span>
</h2>
</div>
</div>
<!-- 登錄彈出層,輸入電話 bootstrap里面的-->
<div id="killPhoneModal" class="modal fade bs-example-modal-lg">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title text-center">
<span class="glyphicon glyphicon-phone"></span>秒殺電話:
</h3>
</div>
<div class="modal-body">
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" name="killphone" id="killphoneKey" placeholder="填手機號^O^" class="form-control">
</div>
</div>
</div>
<div class="modal-footer">
<span id="killphoneMessage" class="glyphicon"></span>
<button type="button" id="killPhoneBtn" class="btn btn-success">
<span class="glyphicon glyphicon-phone"></span> Submit
</button>
</div>
</div>
</div>
</div>
<!-- jQuery文件。務必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script>
<!-- jQuery cookie操作插件 -->
<script src="http://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<!-- jQery countDonw倒計時插件 -->
<script src="http://cdn.bootcss.com/jquery.countdown/2.1.0/jquery.countdown.min.js"></script>
<%--開始寫交互邏輯--%>
<script src="/resources/script/seckill.js" type="text/javascript"></script>
<script type="text/javascript">
$(function () {
seckill.detail.init({
seckillId: ${seckill.seckillId},
startTime: ${seckill.startTime.time}, // 轉化為毫秒,方便比較
endTime: ${seckill.endTime.time},
});
});
</script>
我們的邏輯主要寫在另外的js文件中:
seckill.js
// 存放主要交互邏輯js
// javascript 模塊化
var seckill={
// 封裝秒殺相關ajax的URL
URL:{
},
// 驗證手機號
validatePhone: function (phone) {
if(phone && phone.length==11 && !isNaN(phone)) {
return true;
} else {
return false;
}
},
// 詳情頁秒殺邏輯
detail: {
// 詳情頁初始化
init: function (params) {
// 手機驗證和登錄,計時交互
// 規(guī)劃交互流程
// 在cookie中查找手機號
var killPhone = $.cookie('killPhone');
var startTime = params['startTime'];
var endTime = params['endTime'];
var seckillId = params['seckillId'];
// 驗證手機號
if(!seckill.validatePhone(killPhone)) {
// 綁定手機號,獲取彈窗輸入手機號的div id
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show: true, //顯示彈出層
backdrop: 'static',//禁止位置關閉
keyboard: false, //關閉鍵盤事件
});
$('#killPhoneBtn').click(function () {
var inputPhone = $('#killphoneKey').val();
// 輸入格式什么的ok了就刷新頁面
if(seckill.validatePhone(inputPhone)) {
// 將電話寫入cookie
$.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'});
window.location.reload();
} else {
// 更好的方式是把字符串寫入字典再用
$('#killphoneMessage').hide().html('<label class="label label-danger">手機號格式錯誤</label>').show(500);
}
});
}
// 已經(jīng)登錄
}
}
}
計時面板
在登錄完成后,處理計時操作:
// 已經(jīng)登錄
// 計時交互
$.get(seckill.URL.now(),{},function (result) {
if(result && result['success']) {
var nowTime = result['data'];
// 寫到函數(shù)里處理
seckill.countdown(seckillId,nowTime,startTime,endTime);
} else {
console.log('result: '+result);
}
});
在countdown函數(shù)里,有三個判斷,未開始、已經(jīng)開始、結束。
URL:{
now: function () {
return '/seckill/time/now';
}
},
handleSeckill: function () {
// 處理秒殺邏輯
},
countdown: function (seckillId,nowTime,startTime,endTime) {
var seckillBox = $('#seckillBox');
if(nowTime>endTime) {
seckillBox.html('秒殺結束!');
} else if(nowTime<starttime) {="" 秒殺未開始,計時="" var="" killtime="new" date(starttime="" +="" 1000);="" seckillbox.countdown(killtime,function="" (event)="" 控制時間格式="" format="event.strftime('秒殺開始倒計時:%D天" %h時="" %m分="" %s秒');="" seckillbox.html(format);="" 時間完成后回調(diào)事件="" }).on('finish.countdown',="" function="" ()="" 獲取秒殺地址,控制顯示邏輯,執(zhí)行秒殺="" seckill.handleseckill();="" })="" }="" else="" 秒殺開始="" },="" ```="" 總體就是一個顯示操作,用了jquery的countdown倒計時插件。="" <img="" src="https://gitee.com/hqinglau/img/raw/master/img/20211006194407.png" alt="image-20211006194407145" style="zoom:67%;">
### 秒殺交互
秒殺之前:

詳情頁:
<img src="https://gitee.com/hqinglau/img/raw/master/img/20211006201149.png" alt="image-20211006201149488" style="zoom:80%;">
點擊開始秒殺:
<img src="https://gitee.com/hqinglau/img/raw/master/img/20211006202320.png" alt="image-20211006202320137" style="zoom:80%;">
列表頁刷新:

運行時發(fā)現(xiàn)controller忘了寫`@ResponseBody`了,這里返回的不是jsp是json,需要加上。
```java
@ResponseBody
public SeckillResult<seckillexecution> execute(
@PathVariable("seckillId") Long seckillId,
// required = false表示cookie邏輯由我們程序處理,springmvc不要報錯
@CookieValue(value = "killPhone",required = false) Long userPhone,
@PathVariable("md5") String md5)
在seckill.js中,補全秒殺邏輯:
// 封裝秒殺相關ajax的URL
URL:{
now: function () {
return '/seckill/time/now';
},
exposer: function(seckillId) {
return '/seckill/'+seckillId+'/exposer';
},
execution: function (seckillId,md5) {
return '/seckill/'+seckillId+'/'+md5+'/execution';
}
},
// id和顯示計時的那個模塊
handleSeckill: function (seckillId,node) {
// 處理秒殺邏輯
// 在計時的地方顯示一個秒殺按鈕
node.hide()
.html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>');
// 獲取秒殺地址
$.post(seckill.URL.exposer(),{seckillId},function (result) {
if(result && result['success']) {
var exposer = result['data'];
if(exposer['exposed']) {
// 如果開啟了秒殺
// 獲取秒殺地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId,md5);
console.log("killurl: "+killUrl);
// click永遠綁定,one只綁定一次
$('#killBtn').one('click',function () {
// 執(zhí)行秒殺請求操作
// 先禁用按鈕
$(this).addClass('disabled');
// 發(fā)送秒殺請求
$.post(killUrl,{},function (result) {
if(result) {
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
// 顯示秒殺結果
if(result['success']) {
node.html('<span class="label label-success">'+stateInfo+'</span>');
} else {
node.html('<span class="label label-danger">'+stateInfo+'</span>');
}
}
console.log(result);
})
});
node.show();
} else {
// 未開始秒殺,這里是因為本機顯示時間和服務器時間不一致
// 可能瀏覽器認為開始了,服務器其實還沒開始
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
// 重新進入倒計時邏輯
seckill.countdown(seckillId,now,start,end);
}
} else {
console.log('result='+result);
}
})
},
秒殺成功后再次進行秒殺則不成功:

輸出:

在庫存不夠時也返回秒殺結束:


至此,功能方面已經(jīng)實現(xiàn)了,后面還剩下優(yōu)化部分。
總結
本篇文章就到這里了,希望能夠給你帶來幫助,也希望您能夠多多關注腳本之家的更多內(nèi)容!
相關文章
SpringBoot實現(xiàn)Server-Sent Events(SSE)的使用完整指南
使用SpringBoot實現(xiàn)Server-Sent Events(SSE)可以有效處理實時數(shù)據(jù)推送需求,具有單向通信、輕量級和高實時性等優(yōu)勢,本文詳細介紹了在SpringBoot中創(chuàng)建SSE端點的步驟,并通過代碼示例展示了客戶端如何接收數(shù)據(jù),適用于實時通知、數(shù)據(jù)展示和在線聊天等場景2024-09-09
SpringBoot 統(tǒng)一請求返回的實現(xiàn)
這篇文章主要介紹了SpringBoot 統(tǒng)一請求返回的實現(xiàn),文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2020-07-07
Java實現(xiàn)數(shù)據(jù)脫敏(Desensitization)的操作指南
數(shù)據(jù)脫敏是指通過對敏感數(shù)據(jù)進行部分或完全隱藏處理,保護敏感信息在存儲和使用過程中的安全性,常見的應用場景包括日志記錄、接口返回、報表展示、數(shù)據(jù)分析等,本文給大家介紹了Java實現(xiàn)數(shù)據(jù)脫敏(Desensitization)的操作指南,需要的朋友可以參考下2025-02-02

