商城秒杀系统总结(Java)
本⽂写的较为零散,对没有基础的同学不太友好。
⼀、秒杀系统项⽬总结(基础版)
classpath
在.properties中时常需要读取资源,定位⽂件地址时经常⽤到classpath
类路径指的是src/main/java,或者是src/main/resource下的路径。例如:resource 下的 classpath:mapping/*.xml,经常⽤于Mybatis中配置mapping⽂件地址。Mybatis-generator
在写项⽬中可以利⽤mybatis-generator进⾏⼀些机械性⼯作(在pom中引⼊),这⾥将配置⽂件中的⼀部分进⾏展⽰:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE generatorConfiguration
PUBLIC "-////DTD MyBatis Generator Configuration 1.0//EN"
"/dtd/mybatis-generator-config_1_0.dtd">
<generatorConfiguration>
<context id="DB2Tables"    targetRuntime="MyBatis3">
<!--数据库链接地址账号密码-->
<jdbcConnection driverClass="sql.jdbc.Driver" connectionURL="jdbc:mysql://127.0.0.1:3306/库名" userId="sql_id" password="sql_pass word">
</jdbcConnection>
<!--⽣成DataObject类存放位置-->
<javaModelGenerator targetPackage="com.imooc.miaoshaproject.dataobject" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
<property name="trimStrings" value="true"/>
</javaModelGenerator>
<!--⽣成映射⽂件存放位置-->
<sqlMapGenerator targetPackage="mapping" targetProject="src/main/resources">
<property name="enableSubPackages" value="true"/>
</sqlMapGenerator>
<!--⽣成Dao类存放位置-->
<!-- 客户端代码,⽣成易于使⽤的针对Model对象和XML配置⽂件的代码
type="ANNOTATEDMAPPER",⽣成Java Model 和基于注解的Mapper对象
type="MIXEDMAPPER",⽣成基于注解的Java Model 和相应的Mapper对象
type="XMLMAPPER",⽣成SQLMap XML⽂件和独⽴的Mapper接⼝
-
->
<javaClientGenerator type="XMLMAPPER" targetPackage="com.imooc.miaoshaproject.dao" targetProject="src/main/java">
<property name="enableSubPackages" value="true"/>
</javaClientGenerator>
<!--⽣成对应表及类名-->
<!--
<table tableName="user_info"  domainObjectName="UserDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
<table tableName="user_password"  domainObjectName="UserPasswordDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
-->
<table tableName="promo"  domainObjectName="PromoDO" enableCountByExample="false"
enableUpdateByExample="false" enableDeleteByExample="false"
enableSelectByExample="false" selectByExampleQueryId="false"></table>
</context>
</generatorConfiguration>
在使⽤mybatis-generator之后要注意检查mapping中的⽂件,进⾏适当修改,⽐如Insert操作中声明⾃增和主键。
Spring异常拦截:
如果对Spring程序没有进⾏异常处理,则遇到特定的异常会⾃动映射为指定的HTTP状态码,部分如下:
表中的异常⼀般会由Spring⾃⾝抛出,作为DispatcherServlet处理过程中或执⾏校验时出现问题的结果。如果DispatcherServlet⽆法到适合处理请求的控制器⽅法,那么将会抛出NoSuchRequestHandlingMethodException异常,最终的结果就是产⽣404状态码的响应(Not Found)。
通过使⽤@ResponseStatus注解能将异常映射为特定的状态码:
//定义exceptionhandler解决未被controller层吸收的exception
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.OK)
@ResponseBody
public Object handlerException(HttpServletRequest request, Exception ex){
Map<String,Object> responseData = new HashMap<>();
if( ex instanceof BusinessException){
BusinessException businessException = (BusinessException)ex;
responseData.put("errCode",ErrCode());
responseData.put("errMsg",ErrMsg());
}else{
responseData.put("errCode", EmBusinessError.ErrCode());
responseData.put("errMsg",EmBusinessError.ErrMsg());
}
ate(responseData,"fail");
}
这⾥将响应200(OK)状态码,但是⼤多数时候,我们需要知道这个异常的具体信息,这就需要如上代码所⽰,加上 @ExceptionHandler(Exception.class),⼀旦捕捉到异常,则按handler流程运⾏。 如果需要⼀个contrller具有该异常处理,可以建⽴⼀个基类进⾏继承,不然需要每个controller都写⼀遍,这
种⽅式较为⿇烦。
⼀个Controller下多个@ExceptionHandler上的异常类型不能出现⼀样的,否则运⾏时抛异常.
@ControllerAdvice+@ExceptionHandler拦截异常并统⼀处理
@ExceptionHandler的作⽤主要在于声明⼀个或多个类型的异常,当符合条件的Controller抛出这些异常之后将会对这些异常进⾏捕获,然后按照其标注的⽅法的逻辑进⾏处理,从⽽改变返回的视图信息。
@ControllerAdvice
public class GlobalExceptionHandler{
@ExceptionHandler(Exception.class)
@ResponseBody
public CommonReturnType doError(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Exception ex) {
ex.printStackTrace();
Map<String,Object> responseData = new HashMap<>();
if( ex instanceof BusinessException){
BusinessException businessException = (BusinessException)ex;
responseData.put("errCode",ErrCode());  //⾃定义的异常类
responseData.put("errMsg",ErrMsg());
}else if(ex instanceof ServletRequestBindingException){
responseData.put("errCode",EmBusinessError.ErrCode());
responseData.put("errMsg","url绑定路由问题");
}else if(ex instanceof NoHandlerFoundException){
responseData.put("errCode",EmBusinessError.ErrCode());  //⾃定义的枚举类
responseData.put("errMsg","没有到对应的访问路径");
}else{
responseData.put("errCode", EmBusinessError.ErrCode());
responseData.put("errMsg",EmBusinessError.ErrMsg());
}
ate(responseData,"fail");
}
}
这样,当访问任何controller的时候,如果在该controller中抛出了Exception,那么理论上这⾥的异常捕获器就会捕获该异常,判断情况,然后返回我们定义的异常视图(默认的error视图)。
在数据库设计层⾯需要注意的有:例如商品价格属性在后台设置为BigDecimal,但是mysql中是没有这个关键字的,我们可以在表中设计为double属性,包括商品的DO对象也为double,但是在商品的mod
el对象中属性为BigDecimal,需要进⾏类型转换。不⽤double的原因为后端传送给前端后,可能会出现⼀些错误,例如1.9传过去之后可能为
建议将价格等对数位敏感的数据在后台处理为BigDecimal。
在数据结构设计层⾯建⽴了3种数据对象,视图层中的VO对象,这是为了将⽤户需要的数据进⾏呈现,避免将⼀些⽤户不需要感知的数据进⾏前后端交互。dao层的DO对象,这是为了和数据库真正进⾏交互。Service层的Model对象,这是为了后台整体逻辑统⼀,例如⽤户的资料和⽤户的密码在本项⽬中分两个表存,肯定有两个DO对象,⽽在后台设计时,每次都调⽤两个DO属性较为⿇烦,直接建⽴⼀个⽤户的逻辑对象,将⽤户相关的所有数据放在⼀个对象中,⽅便操作。
基础知识
前端
在编写前端页⾯的时候,通常使⽤⼀些框架,⽐如本项⽬使⽤的Metronic,之前也稍微⽤过element-ui这些,⼀般逻辑为:⾸先<head> </head>中引⼊样式和.js资源,然后在
<body> </body>中通过调⽤"class"即可直接完成页⾯的美化,在处理动态逻辑的时候,需要⽤ajax进⾏click等动作的判定,以及请求的发送。
对于前端我只了解⼀点点,可能说的不对,不过稍微理解概念后即可在模板上进⾏修修改改。
Java 8 stream api
在代码中经常使⽤.stream()有利于简化代码结构,效率⾼⼀点,举例:
//使⽤stream apiJ将list内的itemModel转化为ITEMVO;
List<ItemVO> itemVOList =  itemModelList.stream().map(itemModel -> {
ItemVO itemVO = vertVOFromModel(itemModel);
return itemVO;
}).List());
这⼀段即为将⼀个Model结构的list,利⽤stream api转成VO结构的list。
MD5加密
数据库中通常不存明⽂密码(防⽌数据库数据泄露,密码被公开),这时候我们需要⼀种加密⽅式,⼤多数采⽤MD5加密,在Java原⽣包中 MD5Encoder 只⽀持16位长度,这样的话不⽅便业务实现。
md5是不可逆的,也就是没有对应的算法,从⽣产的md5值逆向得到原始数据。但是如果使⽤暴⼒破解,那就另说了。
简单实现⽅式:
public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
//确定计算⽅法
MessageDigest md5 = Instance("MD5");
BASE64Encoder base64en = new BASE64Encoder();
/
/加密字符串
String newstr = de(md5.Bytes("utf-8")));
return newstr;
}
MD5的⼏个特点:
1.长度固定:
不管多长的字符串,加密后长度都是⼀样长 作⽤:⽅便平时信息的统计和管理
2.易计算:
字符串和⽂件加密的过程是容易的.
作⽤: 开发者很容易理解和做出加密⼯具
3.细微性
⼀个⽂件,不管多⼤,⼩到⼏k,⼤到⼏G,你只要改变⾥⾯某个字符,那么都会导致MD5值改变. 作⽤:很多软件和应⽤在⽹站提供下载资源,其中包含了对⽂件的MD5码,⽤户下载后只需要⽤⼯具测⼀下下载好的⽂件,通过对⽐就知道该⽂件是否有过更改变动.
4.不可逆性
你明明知道密⽂和加密⽅式,你却⽆法反向计算出原密码. 作⽤:基于这个特点,很多安全的加密⽅式都是⽤到.⼤⼤提⾼了数据的安全性
交易模型
交易模型流程:
//1.校验下单状态,下单的商品是否存在,⽤户是否合法,购买数量是否正确,校验活动信息
//2.落单减库存(下单时刻即减少库存,但是如果⽤户取消交易需要将库存还原,适⽤于后台备货⽐显⽰多的情况),还有⼀种交易减库存,这是只有当成功交易才会减少库存,适⽤于显⽰的库存为真实库存,会让⽤户有⼀定的交易紧迫感
//3.订单⼊库,⽣成交易流⽔号,订单号,加上商品的销量
//4.返回前端
设计订单号:(订单号显⽰是具有⼀定意义的,简单的⾃增ID⽆法满⾜需求)
设计订单号为16位:前8位为时间信息(年⽉⽇)⽅便在数据库数据量过⼤时候,可以删除⼏个⽉前的⽆⽤订单数据。中间6位为⾃增序列,如果每天的订单量超过6位数,则需要扩增。最后两位为分库分表位,区分在哪个库哪张表。这是订单号的⼀个简单设计。
秒杀环节的简单思考:
秒杀通常与商品活动挂钩,因此必然有⼀个活动开始时间,活动结束时间,以及活动开始倒计时,在增加秒杀活动的过程中,我们就需要对商品模型数据结构进⾏修改,可以增加⼀个促销模型属性,⽽促销模型进⾏分层设计,设计其service等等。在前端进⾏⼀定的页⾯修改,显⽰时间,显⽰促销价格等等。同时对订单模型进⾏修改,增加是否促销属性,如果促销,则订单⼊库时需要以促销价格⼊库,这些地⽅需要注意。
⾄于后端订单接⼝如何识别是否在活动呢?
//1.通过前端url上传过来秒杀活动id,然后下单接⼝内校验对应id是否属于对应商品且活动已开始
//2.直接在下单接⼝内判断对应的商品是否存在秒杀活动,若存在进⾏中的则以秒杀价格下单
显然,使⽤2的话,在⾮促销商品的下单环节会增加不必要的运⾏。
前端设计:
下单时,将promo_id传进去
jQuery(document).ready(function(){
$("#createorder").on("click",function(){
$.ajax({
type:"POST",
contentType:"application/x-www-form-urlencoded",
url:"localhost:8090/order/createorder",
data:{
"itemId":g_itemVO.id,
"amount":1,
"promoId":g_itemVO.promoId
},
xhrFields:{withCredentials:true},
success:function(data){
if(data.status == "success"){
alert("下单成功");
load();
}else{
alert("下单失败,原因为"+Msg);
if(Code == 20003){
window.location.href="login.html";
}
}
},
error:function(data){
alert("下单失败,原因为"+sponseText);
}
});
});
后台下单:
/
/封装下单请求
@RequestMapping(value = "/createorder",method = {RequestMethod.POST},consumes={CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType createOrder(@RequestParam(name="itemId")Integer itemId,
@RequestParam(name="amount")Integer amount,
@RequestParam(name="promoId",required = false)Integer promoId) throws BusinessException {
Boolean isLogin = (Boolean) Session().getAttribute("IS_LOGIN");unknown怎么处理
if(isLogin == null || !isLogin.booleanValue()){
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,"⽤户还未登陆,不能下单");
}
//获取⽤户的登陆信息
UserModel userModel = (Session().getAttribute("LOGIN_USER");
OrderModel orderModel = Id(),itemId,promoId,amount);
ate(null);
}
部署
本⼈是直接利⽤宝塔linux⾯板进⾏环境部署,在运⾏项⽬是采⽤外挂配置:
nohup java -jar "⽬标jar" --fig.additon-location=/外挂配置地址
//nohup可挂在后台运⾏jar包
并且外挂配置优先级⾼于默认配置
⼆、JMETER性能测试
JMETER实际上就是在本地开⼀个线程组,⾃⼰规定线程组的规模,向服务器发出HTTP请求,进⾏性能压测。⼀般需要配置HTTP请求,查看结果树,聚合报告这三项。
这是⼀个GET请求的⽰例,设置20个线程,ramp-up时间设为10秒,即jmeter⽤10秒启动20个线程并运⾏。(改动了线程组的设置)
观测结果,即平均58ms响应,90%的为64ms内响应,99%的为110ms内响应,TPS为2.1。
TPS 即Transactions Per Second的缩写,每秒处理的事务数⽬。⼀个事务是指⼀个客户机向服务器发送请求然后服务器做出反应的过程(完整处理,即客户端发起请求到得到响应)。客户机在发送请求时开始计时,收到服务器响应后结束计时,以此来计算使⽤的时间和完成的事务个数,最终利⽤这些信息作出的评估分。⼀个事务可能对应多个请求,可以参考下数据库的事务操作。
在服务器上查看tomcat当前维护的线程树:

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。