基于SSM实现高并发秒杀API(Web层)

Web层

  1. 前端交互设计
  2. Restful
  3. Spring MVC
  4. Bootstrap+JQuery

前端交互设计

下面部分代码块需要使用有道云笔记的markdown语法来解析

1
2
3
4
5
6
7
graph TD
列表页-->详情页
详情页--> a{login}
a-->|no| 登陆操作
a-->|yes| 展示逻辑
登陆操作-->写入cookie
写入cookie-->展示逻辑

详情页

  1. 获取系统标准时间(统一的时间)
  2. 判断时间(开始时间,结束时间)
  3. 秒杀结束
  4. 秒杀未开始(倒计时)
  5. 秒杀结束后(秒杀地址获得)
  6. 执行秒杀
  7. 结果
1
2
3
4
5
6
7
8
graph LR
A[获取系统时间]-->B[判断时间]
B-->|结束| C[秒杀结束]
B-->|未开始| D[倒计时]
B-->|已开始| E[秒杀地址]
D-->E
E-->F[执行秒杀]
F-->G[结果]

设计Restful接口

1.优雅的url表达方式;2.表示资源的状态和状态的转移

例子

  1. GET seckill/list 优雅
  2. POST seckill/execute/{seckillId} 不优雅
  3. POST seckill/{seckillId}/exection 优雅
  4. GET seckill/delete/{id} 不优雅
  5. DELETEseckill/{id}/delete 优雅

何为优雅,何为不优雅

  1. GET 秒杀/列表
  2. POST 秒杀/执行(动词)/商品id
  3. POST 秒杀/商品id/执行(名词)
  4. GET 秒杀/删除/商品id
  5. DELETE 秒杀/商品/删除(名词)

可以看出Restful规范我们定义URL时,需要让URL表达出名词的意思,而不应该表示为动作.

Restful规范

  1. GET–>查询
  2. POST–>添加/修改
  3. put–>修改
  4. DELETE–>

POST与PUT的区别: POST非幂等操作,PUT幂等操作

幂等(PUT):一个幂等的操作典型如:把编号为5的记录的A字段设置为0,这种操作不管执行多少次都是幂等的。
非幂等(POST):一个非幂等的操作典型如:把编号为5的记录的A字段增加1,这种操作显然就不是幂等的。

URL设计

1
2
3
4
5
6
7
8
9
10
11
例子:
/模块/资源/标示/集合/....
/user/{uid}/friends --> 好友列表
/user/{uid}/followers --> 关注者列表

秒杀API的URL设计:
GET /seckill/list 秒杀列表
GET /seckill/{id}/detail 详情页
GET /seckill/time/now 系统时间
POST /seckill/{id}/exposer 暴露秒杀
POST /seckill/{id}/{md5}/execution

使用Spring MVC

Spring MVC执行流程

1
2
3
4
5
6
7
8
9
graph LR
A[用户]-->|1| B[DispatcherServlet]
B-->|2| C[DefaultAnnotionHandlerMapping]
B-->|3| D[DefaultAnnotionHandlerAdapter]
D-->|4| E[SeckillController]
D-->|5.ModelAndView<br>/seckill/list| B
B-->|6| F[InternalResourceViewResolver]
B-->|7.Model| G[list.jsp]
G-->A

HTTP请求地址映射原理

HTTP请求–>HandlerMapping–>Handler处理方法

@RequestMapping
1. 支持标准的URL
2. Ant风格URL(?匹配任意一个字符,*任意数量字符,**任意路径)
3. 带{XXX}占位符的URL

1
2
3
4
5
6
user/*/creation
user/aaa/creation,user/bbb/creation...
user/**/creation
user/creation,user/aaa/bbb/creation...
user/{userId}
user/123,user/abc...

请求方法细节

  • 请求参数绑定
  • 请求方法限制
  • 请求转发与重定向
  • 数据模型赋值
  • 返回json数据
  • cookie访问

代码实现

web.xml

加载两个配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?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">
<!--version = 3.1 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>mvc-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>mvc-dispatcher</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

spring配置

1
2
3
4
5
6
<context:component-scan base-package="com.yuda">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!--导入其他-->
<import resource="spring-dao.xml"/>
<import resource="spring-service.xml"/>

mvc 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!--开启注解模式-->
<!--1.自动注册DefaultAnnotationHandlerMapping和AnnotationHandlerAdapter-->
<!--2.提供一系列:数据绑定,数字和日期类format,@NumberFormat,@DataTimeFormat,xml,json,默认读写支持-->
<mvc:annotation-driven/>
<!--扫描Controller-->
<context:annotation-config/>
<context:component-scan base-package="com.yuda.web">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
<!-- servletMapping 路径为 "/" -->
<!-- 静态资源默认Servlet配置 -->
<!-- 1. 加入对静态资源的处理,js,png,css-->
<!-- 2. 运行使用'/'做映射 -->
<mvc:default-servlet-handler/>
<!--映射静态资源,和上面那个一样的作用,不过功能好像更加强大,可以映射classpath里面的静态资源-->
<mvc:resources mapping="/resources/**" location="/resources/"/>
<!--配置jsp,viewResolver-->
<bean id="internalResourceViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass"
value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/jsps/"/>
<property name="suffix" value=".jsp"/>
</bean>

SeckillController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
@Controller
@RequestMapping("/seckill")
public class SeckillController {

@Autowired
private SeckillService seckillService;

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

/**
* 列出商品
*
* @param model
* @return
*/
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model) {
//list.jsp + model = ModelAndView
//获取列表页
//TODO 这句话可能报错::查询异常
List<Seckill> seckills = seckillService.getSeckillList();
model.addAttribute("list", seckills);
return "list";
}

/**
* 详情页
*
* @param seckillId
* @param model
* @return
*/
@RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)
public String detail(@PathVariable(value = "seckillId") Long seckillId, Model model) {
loggger.debug("传入的ID:" + String.valueOf(seckillId));
//没传id
if (seckillId == null) {
return "redirect:/seckill/list";
}
Seckill seckill = seckillService.getById(seckillId);
//没找到相应商品,用户瞎传的id
if (seckill == null) {
return "forward:/list";
}
model.addAttribute("seckill", seckill);
return "detail";
}

/**
* ajax:json 获得ID
*
* @param seckillId
*/
@RequestMapping(value = "/{seckillId}/exposer",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId) {

SeckillResult<Exposer> result;

try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
loggger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}

return result;
}

@RequestMapping(value = "/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(
@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "killPhone", required = false) Long phone) {

//可以使用Spring的 Valid方式
if (phone == null) {
return new SeckillResult<SeckillExecution>(false, "未注册");
}

SeckillResult<SeckillExecution> result;

try {
SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
result = new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
result = new SeckillResult<SeckillExecution>(true, execution);
} catch (SeckillCloseException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
result = new SeckillResult<SeckillExecution>(true, execution);
} catch (Exception e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
result = new SeckillResult<SeckillExecution>(true, execution);
}
return result;
}

@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult<Long> time() {
Date date = new Date();
return new SeckillResult<Long>(true, date.getTime());
}
}

输出结果封装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class SeckillResult<T> {

private boolean success;

private T data;

private String error;

public SeckillResult(boolean success, T data) {
this.success = success;
this.data = data;
}

public SeckillResult(boolean success, String error) {
this.success = success;
this.error = error;
}
}

jsp编写

JQuery , Bootstrap , jstl , jquery.countdown , jquery.cookie , 模块方式编写JS

list.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html>
<head>
<title>秒杀列表</title>
<%-- 静态包含(一个Servlet) --%>
<%@include file="common/head.jsp" %>
</head>
<body>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h2>秒杀列表</h2>
</div>
<div class="panel-body">
<table class="table table-hover">
<thead>
<tr>
<th>名称</th>
<th>库存</th>
<th>开始时间</th>
<th>结束时间</th>
<th>创建时间</th>
<th>详情页</th>
</tr>
</thead>
<tbody>
<c:forEach var="sk" items="${list}">
<tr>
<td>${sk.name}</td>
<td>${sk.number}</td>
<td>
<fm:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<fm:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<fm:formatDate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss"/>
</td>
<td>
<a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">
详情页
</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</html>
detail.jsp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html>
<head>
<title>${seckill.name}</title>
<%-- 静态包含(一个Servlet) --%>
<%@include file="common/head.jsp" %>
</head>
<body>
<div class="container">
<div class="panel panel-default text-center">
<div class="pannel-heading">
<h1>${seckill.name}</h1>
</div>
</div>
<div class="panel-body">
<h2 class="text-danger text-center">
<%--time--%>
<span class="glyphicon glyphicon-time"></span>
<%--倒计时--%>
<span class="glyphicon" id="seckill-box"></span>
</h2>
<button class="btn btn-danger" id="clearPhone">重置电话号码</button>
</div>
</div>
<%--弹出层--%>
<div id="killPhoneModal" class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<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="填写手机号"
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>

</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<%--cookie--%>
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<%--countdown--%>
<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.js"></script>
<%--交互逻辑--%>
<script type="text/javascript" src="<c:url value="/resources/script/seckill.js"/>"></script>
<script type="text/javascript">
$(function () {
//使用EL表达式传入参数
//模块方式编写js
seckill.detail.init({
seckillId: ${seckill.seckillId},
startTime: ${seckill.startTime.time},//转换为毫秒
endTime: ${seckill.endTime.time}
});
});
</script>
</html>
抽离出head和tag
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!----------------------   head.jsp   ---------------------------------->
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 引入 Bootstrap -->
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">

<!-- HTML5 Shiv 和 Respond.js 用于让 IE8 支持 HTML5元素和媒体查询 -->
<!-- 注意: 如果通过 file:// 引入 Respond.js 文件,则该文件无法起效果 -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
<script src="https://oss.maxcdn.com/libs/respond.js/1.3.0/respond.min.js"></script>
<![endif]-->
<link href="https://cdn.bootcss.com/jquery-countdown/2.0.1/jquery.countdown.css" rel="stylesheet">
<!---------------------- tag.jsp ---------------------------------->
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@taglib prefix="fm" uri="http://java.sun.com/jsp/jstl/fmt" %>
seckill.js

重点(特别是时间显示的处理逻辑和秒杀操作的逻辑)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//模块化
//seckill.URL.XXX()
//seckill.detail.XXX()
var seckill = {
//所有的请求路径的抽离
URL: {
now: function () {
return "/seckill/time/now";
},
exposer: function (seckillId) {
return "/seckill/" + seckillId + "/exposer";
},
killUrl: function (seckillId, md5) {
return "/seckill/" + seckillId + "/" + md5 + "/execution";
}
},
//验证手机号方法抽离
validatePhone: function (phone) {
if (phone && phone.length == 11 && !isNaN(phone)) {
return true;
} else {
return false;
}
},
//秒杀操作方法抽离
handlerSeckillkill: 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.killUrl(seckillId, md5);
console.log("秒杀地址" + killUrl);
$("#killBtn").one('click', function () {
//绑定执行秒杀请求的操作
$(this).addClass('disabled');
//发送请求
$.post(killUrl, {}, function (result) {
//发送请求执行秒杀
//分析结果
if (result && result['success']) {
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
//显示秒杀结果
node.html("<span class='label label-success'>" + stateInfo + "</span>");
}

});
});
node.show();
} else {
//未开启
//如果用户开启计时面板,时间久了,各种机器的时间是有差异的
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
seckill.mycountdown(seckillId, now, start, end);
}
} else {
//失败
console.log(result['data']);
}
});
},
//倒计时时间显示,到时秒杀活动开启
mycountdown: function (seckillId, nowTime, startTime, endTime) {
var seckillBox = $("#seckill-box");
//时间判断
if (nowTime > endTime) {
//秒杀结束
seckillBox.html("秒杀结束");
} else if (nowTime < startTime) {
//秒杀未开始
var killTime = new Date(startTime + 1000);
seckillBox.countdown(killTime, function (event) {
//时间变化时候做日期输出
var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒');
seckillBox.html(format);
}).on('finish.countdown', function () {
//时间完成后回调事件,获取秒杀地址,控制显示逻辑,执行秒杀
//页面处于倒计时,然后时间到了
seckill.handlerSeckillkill(seckillId, seckillBox);
})
} else {
//秒杀开始
//时间本来就到了,用户进入秒杀页面
seckill.handlerSeckillkill(seckillId, seckillBox);
}
},
//详情页
detail: {
//详情页初始化
init: function (params) {
//手机验证相关操作,计时交互
//规划交互流程
//cookie中取得手机号
var killPhone = $.cookie('killPhone');
//验证手机号
if (!seckill.validatePhone(killPhone)) {
//绑定Phone
//控制输出
var killPhoneModal = $("#killPhoneModal");
killPhoneModal.modal({
show: true,//显示
backdrop: 'static',//禁止位置关闭
keyboard: false//关闭键盘事件
});
$("#killPhoneBtn").click(function () {
var inputPhone = $("#killPhoneKey").val();
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(300);
}
});
}
//已经登陆
var startTime = params['startTime'];
var endTime = params['endTime'];
var seckillId = params['seckillId'];

$.get(seckill.URL.now(), {}, function (result) {
if (result && result['success']) {
var nowTime = result['data'];
//时间判断,计时交互
seckill.mycountdown(seckillId, nowTime, startTime, endTime);
} else {
console.log('result:' + result);
}
});

$("#clearPhone").click(function () {
$.removeCookie("killPhone");
window.location.reload();
});
}
}
};