首页 > 运维 > linux运维 > 正文

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

看不見的法師
发布: 2025-07-09 12:22:01
原创
310人浏览过

在进行前端交互设计和开发高并发秒杀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">@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">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属性中。

基于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。

在页面中,不要写成,这样会导致后边的js加载不了,应写成

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

4.3 测试页面

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

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,文中内容可能已过时,请谨慎使用。

以上就是Java高并发秒杀API(三)之Web层的详细内容,更多请关注php中文网其它相关文章!

最佳 Windows 性能的顶级免费优化软件
最佳 Windows 性能的顶级免费优化软件

每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。

下载
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习
PHP中文网抖音号
发现有趣的

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