0

0

Java高并发秒杀API(三)之Web层

看不見的法師

看不見的法師

发布时间:2025-07-09 12:22:01

|

380人浏览过

|

来源于php中文网

原创

在进行前端交互设计和开发高并发秒杀api时,遵循restful规范、使用springmvc框架以及bootstrap和jquery是关键步骤。以下是详细的开发流程和注意事项。

Java高并发秒杀API(三)之Web层

前端页面流程

Java高并发秒杀API(三)之Web层

详情页流程逻辑

立即学习Java免费学习笔记(深入)”;

考虑到用户可能位于不同时区,且他们的系统时间可能不同,这一点在设计时需要特别注意。

Restful规范

Restful规范通过优雅的URI表达方式来组织资源路径:/模块/资源/{标识}/集合1/...

  • GET -> 查询操作
  • POST -> 添加/修改操作(用于非幂等操作)
  • PUT -> 修改操作(用于幂等操作)
  • DELETE -> 删除操作

在SpringMVC中,使用注解来映射HTTP方法:

@RequestMapping(value = "/path", method = RequestMethod.GET)
@RequestMapping(value = "/path", method = RequestMethod.POST)
@RequestMapping(value = "/path", method = RequestMethod.PUT)
@RequestMapping(value = "/path", method = RequestMethod.DELETE)

幂等性(idempotency)表示对同一URL的多个请求应返回相同的结果。在Restful规范中,GET、PUT、DELETE是幂等操作,而POST是非幂等操作。

POST和PUT都可用于创建和更新资源,区别在于前者用于非幂等操作,后者用于幂等操作。例如,使用POST方法请求创建资源,如果重复发送N次,将创建N个资源;使用GET方法请求创建资源,即使重复发送N次,也只会创建一个资源。

秒杀API的URL设计

Java高并发秒杀API(三)之Web层

注解映射技巧

Java高并发秒杀API(三)之Web层

整合配置SpringMVC框架

2.1 配置web.xml

<web-app metadata-complete="true" version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
    <servlet>
        <servlet-name>seckill-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <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>

Servlet版本为3.0,适用于Tomcat7.0版本。配置文件以spring-开头,可使用通配符*一次性加载所有配置文件。url-pattern设置为/,符合Restful规范;而在使用Struts框架时,通常配置为*.do,这是一种较为丑陋的表达方式。

2.2 在src/main/resources/spring包下建立spring-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd">
    <annotation-driven></annotation-driven>
    <default-servlet-handler></default-servlet-handler>
    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"></property>
        <property name="prefix" value="/WEB-INF/jsp/"></property>
        <property name="suffix" value=".jsp"></property>
    </bean>
    <component-scan base-package="com.lewis.web"></component-scan>
</beans>

Controller设计

Controller中的每个方法对应系统中的一个资源URL,应遵循Restful接口设计风格。

3.1 在java包下新建com.lewis.web包,在该包下新建SeckillController.java

@Controller
@RequestMapping("/seckill") // url:模块/资源/{}/细分
public class SeckillController {
    @Autowired
    private SeckillService seckillService;
<pre class="brush:php;toolbar:false;"><code>@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model) {
    // list.jsp+mode=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) {
        return "redirect:/seckill/list";
    }
    Seckill seckill = seckillService.getById(seckillId);
    if (seckill == null) {
        return "forward:/seckill/list";
    }
    model.addAttribute("seckill", seckill);
    return "detail";
}

// ajax, json暴露秒杀接口的方法
@RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.GET, 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) {
        e.printStackTrace();
        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 = "userPhone", required = false) Long userPhone) {
    if (userPhone == null) {
        return new SeckillResult<SeckillExecution>(false, "未注册");
    }
    try {
        SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
        return new SeckillResult<SeckillExecution>(true, execution);
    } catch (RepeatKillException e1) {
        SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
        return new SeckillResult<SeckillExecution>(true, execution);
    } catch (SeckillCloseException e2) {
        SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
        return new SeckillResult<SeckillExecution>(true, execution);
    } catch (Exception e) {
        SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
        return new SeckillResult<SeckillExecution>(true, execution);
    }
}

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

}

在处理Cookie时,如果找不到对应的Cookie会报错,因此设置required=false,将Cookie是否存在的逻辑判断放到代码中。

Service层中的抛出异常是为了让Spring能够回滚,Controller层中捕获异常是为了将异常转换为对应的Json供前台使用,缺一不可。

3.2 在dto包下新建一个SeckillResult

// 将所有的ajax请求返回类型,全部封装成json数据
public class SeckillResult<T> {
// 请求是否成功
private boolean success;
private T data;
private String error;</p><pre class="brush:php;toolbar:false;"><code>public SeckillResult(boolean success, T data) {
    this.success = success;
    this.data = data;
}

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

public boolean isSuccess() {
    return success;
}

public void setSuccess(boolean success) {
    this.success = success;
}

public T getData() {
    return data;
}

public void setData(T data) {
    this.data = data;
}

public String getError() {
    return error;
}

public void setError(String error) {
    this.error = error;
}

}

SeckillResult是一个VO类(View Object),属于DTO层,用于封装json结果,方便页面取值。将其设计成泛型,可以灵活地封装各种类型的对象。success属性指的是页面是否发送请求成功,而秒杀执行的结果则封装在data属性中。

MemFree
MemFree

MemFree - 来自知识库和互联网的混合AI搜索,更快获取准确答案

下载

基于Bootstrap开发页面

由于项目的前端页面都是由Bootstrap开发的,因此需要下载Bootstrap或使用在线CDN服务。Bootstrap依赖于jQuery,因此需要先引入jQuery。

4.1 在webapp下建立resources目录,接着建立script目录,建立seckill.js

// 存放主要交互逻辑的js代码
// javascript 模块化(package.类.方法)
var seckill = {
// 封装秒杀相关ajax的url
URL: {
now: function () {
return '/seckill/seckill/time/now';
},
exposer: function (seckillId) {
return '/seckill/seckill/' + seckillId + '/exposer';
},
execution: function (seckillId, md5) {
return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';
}
},
// 验证手机号
validatePhone: function (phone) {
if (phone && phone.length == 11 && !isNaN(phone)) {
return true; // 直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true
} else {
return false;
}
},
// 详情页秒杀逻辑
detail: {
// 详情页初始化
init: function (params) {
// 手机验证和登录,计时交互
// 规划我们的交互流程
// 在cookie中查找手机号
var userPhone = $.cookie('userPhone');
// 验证手机号
if (!seckill.validatePhone(userPhone)) {
// 绑定手机 控制输出
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show: true, // 显示弹出层
backdrop: 'static', // 禁止位置关闭
keyboard: false // 关闭键盘事件
});
$('#killPhoneBtn').click(function () {
var inputPhone = $('#killPhoneKey').val();
console.log("inputPhone: " + inputPhone);
if (seckill.validatePhone(inputPhone)) {
// 电话写入cookie(7天过期)
$.cookie('userPhone', inputPhone, { expires: 7, path: '/seckill' });
// 验证通过刷新页面
window.location.reload();
} else {
// todo 错误文案信息抽取到前端字典里
$('#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.countDown(seckillId, nowTime, startTime, endTime);
} else {
console.log('result: ' + result);
alert('result: ' + result);
}
});
}
},
handlerSeckill: function (seckillId, node) {
// 获取秒杀地址,控制显示器,执行秒杀
node.hide().html('开始秒杀');
$.get(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);
// 绑定一次点击事件
$('#killBtn').one('click', function () {
// 执行秒杀请求
// 1.先禁用按钮
$(this).addClass('disabled'); // ,
// 2.发送秒杀请求执行秒杀
$.post(killUrl, {}, function (result) {
if (result && result['success']) {
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
// 显示秒杀结果
node.html('' + stateInfo + '');
}
});
});
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);
}
});
},
countDown: function (seckillId, nowTime, startTime, endTime) {
console.log(seckillId + '<em>' + nowTime + '</em>' + 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.handlerSeckill(seckillId, seckillBox);
});
} else {
// 秒杀开始
seckill.handlerSeckill(seckillId, seckillBox);
}
}
};

使用Json来实现JavaScript模块化(类似于Java的package),避免将js代码混杂在一起,不利于维护和阅读。

由于Eclipse内嵌的Tomcat设置的原因,需要在URL的所有路径前加上/seckill(项目名)才能正常映射到Controller中对应的方法。

// 封装秒杀相关ajax的url
URL: {
now: function () {
return '/seckill/seckill/time/now';
},
exposer: function (seckillId) {
return '/seckill/seckill/' + seckillId + '/exposer';
},
execution: function (seckillId, md5) {
return '/seckill/seckill/' + seckillId + '/' + md5 + '/execution';
}
},

如果在测试页面时找不到路径,可以删除URL中的/seckill

4.2 编写页面

WEB-INF目录下新建一个jsp目录,用于存放jsp页面。为了减少工作量,将每个页面都会使用到的头部文件和标签库分离出来,放到common目录下,在jsp页面中静态包含这两个公共页面。

关于jsp页面,请从源码中拷贝。实际开发中,前端页面由前端工程师完成,但后端工程师也应了解jQuery和ajax。想要了解本项目的页面实现,请观看慕课网的Java高并发秒杀API之Web层。

静态包含会直接将页面包含进来,最终只生成一个Servlet;而动态包含会先将要包含进来的页面生成Servlet后再包含进来,最终会生成多个Servlet。

在页面中,不要写成<script src="js/seckill.js" type="text/javascript"></script>,这样会导致后边的js加载不了,应写成<script src="js/seckill.js"></script>

startTime是Date类型的,通过${startTime.time}来将Date转换成long类型的毫秒值。

4.3 测试页面

首先清理Maven项目,接着编译Maven项目(-X compile命令),然后启动Tomcat,在浏览器输入<a href="https://www.php.cn/link/5937bc13febda34938aa32a74ad94173">https://www.php.cn/link/5937bc13febda34938aa32a74ad94173</a>,成功进入秒杀商品页面;输入<a href="https://www.php.cn/link/ffad99a1f556e0e0595aec7b8060662d">https://www.php.cn/link/ffad99a1f556e0e0595aec7b8060662d</a>成功进入详情页面。

1. pom.xml

<dependency>
<groupId>org.webjars.bower</groupId>
<artifactId>jquery.countdown</artifactId>
<version>2.1.0</version>
</dependency>

2. 页面

<script src="js/jquery.countdown.min.js"></script>

关于显示NaN天 NaN时 NaN分 NaN秒的问题,原因是new Date(startTime + 1000)startTime被解释成一个字符串。

解决办法:

new Date(startTime - 0 + 1000);
new Date(Number(startTime) + 1000);

根据系统标准时间判断,如果在分布式环境下各机器时间不同步怎么办?同时发起的两次请求,可能一个活动开始,另一个提示没开始。后端服务器需要做NTP时间同步,如每5分钟与NTP服务同步保证时间误差在微妙级以下。时间同步在业务需要或者活性检查场景很常见(如hbase的RegionServer)。

如果判断逻辑都放到后端,遇到有刷子,后端处理这些请求扛不住了怎么办?可能活动没开始,服务器已经挂掉了。秒杀开启判断在前端和后端都有,后端的判断比较简单,取秒杀单做判断,这块的IO请求是DB主键查询很快,单DB就可以抗住几万QPS,后面也会加入redis缓存为DB减负。

负载均衡问题,比如根据地域在nginx哈希,怎样能较好的保证各机器秒杀成功的尽量分布均匀呢?负载均衡包括nginx入口端和后端upstream服务,在入口端一般采用智能DNS解析请求就近进入nginx服务器。后端upstream不建议采用一致性hash,防止请求不均匀。后端服务无状态可以简单使用轮训机制。nginx负载均衡本身过于简单,可以使用openresty自己实现或者nginx之后单独架设负载均衡服务如Netflix的Zuul等。

对于流量爆增造成的后端不可用情况,这门课程(Java高并发秒杀API)并没有做动态降级和弹性伸缩架构上的处理,后面受慕课邀请会做一个独立的实战课,讲解分布式架构,弹性容错,微服务相关的内容,到时会加入这方面的内容。

至此,关于Java高并发秒杀API的Web层的开发与测试已经完成,接下来进行对该秒杀系统进行高并发优化,详情可以参考下一篇文章。

上一篇文章: Java高并发秒杀API(二)之Service层

下一篇文章: Java高并发秒杀API(四)之高并发优化

警告

本文最后更新于 October 5, 2017,文中内容可能已过时,请谨慎使用。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
如何配置Tomcat环境变量
如何配置Tomcat环境变量

配置Tomcat环境变量需要在系统中添加CATALINA_HOME变量,并将Tomcat的安装路径添加到PATH变量中。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

117

2023.10.26

idea如何集成Tomcat
idea如何集成Tomcat

idea集成Tomcat的步骤:1、添加Tomcat服务器配置;2、配置项目部署;3、运行Tomcat服务器;4、访问项目;5、注意事项;6、关闭Tomcat服务器。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

173

2024.02.23

怎么查看Tomcat源代码
怎么查看Tomcat源代码

查看Tomcat源代码的步骤:1、下载Tomcat源代码;2、在IDEA中导入Tomcat源代码;3、查看源代码;4、理解Tomcat的工作原理;5、参与社区和贡献;6、注意事项;7、持续学习和更新;8、使用工具和插件。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

98

2024.02.23

常见的tomcat漏洞有哪些
常见的tomcat漏洞有哪些

常见的tomcat漏洞有:1、跨站脚本攻击;2、跨站请求伪造;3、目录遍历漏洞;4、缓冲区溢出漏洞;5、配置漏洞;6、第三方组件漏洞。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

170

2024.02.23

tomcat日志乱码怎么解决
tomcat日志乱码怎么解决

tomcat日志乱码的解决办法:1、修改tomcat的日志编码设置;2、检查ide的编码设置;3、检查操作系统的编码设置;4、使用过滤器处理日志;5、检查外部系统的编码设置;6、检查文件编码方式等等。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

158

2024.02.23

weblogic和tomcat有哪些区别
weblogic和tomcat有哪些区别

weblogic和tomcat的区别:1、功能;2、性能;3、规模;4、价格;5、安全性;6、配置和管理;7、社区支持;8、集成能力;9、升级和更新;10、可靠性。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

200

2024.02.23

tomcat和nginx有哪些区别
tomcat和nginx有哪些区别

tomcat和nginx的区别:1、应用领域;2、性能;3、功能;4、配置;5、安全性;6、扩展性;7、部署复杂性;8、社区支持;9、成本;10、日志管理。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

244

2024.02.23

tomcat启动闪退怎么解决
tomcat启动闪退怎么解决

tomcat启动闪退的解决办法:1、检查java环境;2、检查环境变量配置;3、检查端口被占用;4、检查配置文件编码;5、检查启动时需要的配置文件;6、检查相关文件是否丢失;7、检查防火墙和杀毒软件设置。本专题为大家提供相关的文章、下载、课程内容,供大家免费下载体验。

169

2024.02.23

Go高并发任务调度与Goroutine池化实践
Go高并发任务调度与Goroutine池化实践

本专题围绕 Go 语言在高并发任务处理场景中的实践展开,系统讲解 Goroutine 调度模型、Channel 通信机制以及并发控制策略。内容包括任务队列设计、Goroutine 池化管理、资源限制控制以及并发任务的性能优化方法。通过实际案例演示,帮助开发者构建稳定高效的 Go 并发任务处理系统,提高系统在高负载环境下的处理能力与稳定性。

4

2026.03.10

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
Rust 教程
Rust 教程

共28课时 | 6.8万人学习

国外Web开发全栈课程全集
国外Web开发全栈课程全集

共12课时 | 1万人学习

微信小程序开发之API篇
微信小程序开发之API篇

共15课时 | 1.3万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号