1 需求分析和技术难点:
(1) 分析:
秒杀的时候:减少库存和购买记录明细两个事件保持在同一个事物中。
使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。
(2) 秒杀难点:事务和行级锁的处理
(3) 实现那些秒杀系统(以天猫的秒杀系统为例)
(4) 我们如何实现秒杀功能?
① 秒杀接口暴漏
② 执行秒杀
③ 相关查询
下面我们以主要代码实现秒杀系统:
2.数据库设计和DAO层
(1) 数据库设计
-- 数据库初始化脚本-- 创建数据库CREATEDATABASEseckill;-- 使用数据库useseckill;CREATETABLEseckill(`seckill_id`BIGINTNOTNUllAUTO_INCREMENTCOMMENT'商品库存ID',`name`VARCHAR(120)NOTNULLCOMMENT'商品名称',`number`intNOTNULLCOMMENT'库存数量',`start_time`TIMESTAMPNOTNULLCOMMENT'秒杀开始时间',`end_time`TIMESTAMPNOTNULLCOMMENT'秒杀结束时间',`create_time`TIMESTAMPNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间', PRIMARYKEY(seckill_id),keyidx_start_time(start_time),keyidx_end_time(end_time),keyidx_create_time(create_time))ENGINE=INNODBAUTO_INCREMENT=1000DEFAULTCHARSET=utf8COMMENT='秒杀库存表';-- 初始化数据INSERTintoseckill(name,number,start_time,end_time)VALUES('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'), ('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'), ('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'), ('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00');-- 秒杀成功明细表-- 用户登录认证相关信息(简化为手机号)CREATETABLEsuccess_killed(`seckill_id`BIGINTNOTNULLCOMMENT'秒杀商品ID',`user_phone`BIGINTNOTNULLCOMMENT'用户手机号',`state`TINYINTNOTNULLDEFAULT-1COMMENT'状态标识:-1:无效 0:成功 1:已付款 2:已发货',`create_time`TIMESTAMPNOTNULLCOMMENT'创建时间', PRIMARYKEY(seckill_id,user_phone),/*联合主键*/KEYidx_create_time(create_time))ENGINE=INNODBDEFAULTCHARSET=utf8COMMENT='秒杀成功明细表';-- SHOW CREATE TABLE seckill;#显示表的创建信息
(2) Dao层和对应的实体
① Seckill.java
packagecom.force4us.entity;importorg.springframework.stereotype.Component;importjava.util.Date;publicclassSeckill{privatelongseckillId;privateString name;privateintnumber;privateDate startTime;privateDate endTime;privateDate createTime;publiclonggetSeckillId(){returnseckillId; }publicvoidsetSeckillId(longseckillId){this.seckillId = seckillId; }publicStringgetName(){returnname; }publicvoidsetName(String name){this.name = name; }publicintgetNumber(){returnnumber; }publicvoidsetNumber(intnumber){this.number = number; }publicDategetStartTime(){returnstartTime; }publicvoidsetStartTime(Date startTime){this.startTime = startTime; }publicDategetEndTime(){returnendTime; }publicvoidsetEndTime(Date endTime){this.endTime = endTime; }publicDategetCreateTime(){returncreateTime; }publicvoidsetCreateTime(Date createTime){this.createTime = createTime; }@OverridepublicStringtoString(){return"Seckill{"+"seckillId="+ seckillId +", name='"+ name +'\''+", number="+ number +", startTime="+ startTime +", endTime="+ endTime +", createTime="+ createTime +'}'; }}
② SuccessKilled.java
packagecom.force4us.entity;importorg.springframework.stereotype.Component;importjava.util.Date;publicclassSuccessKilled{privatelongseckillId;privatelonguserPhone;privateshortstate;privateDate createTime;privateSeckill seckill;publiclonggetSeckillId(){returnseckillId; }publicvoidsetSeckillId(longseckillId){this.seckillId = seckillId; }publiclonggetUserPhone(){returnuserPhone; }publicvoidsetUserPhone(longuserPhone){this.userPhone = userPhone; }publicshortgetState(){returnstate; }publicvoidsetState(shortstate){this.state = state; }publicDategetCreateTime(){returncreateTime; }publicvoidsetCreateTime(Date createTime){this.createTime = createTime; }publicSeckillgetSeckill(){returnseckill; }publicvoidsetSeckill(Seckill seckill){this.seckill = seckill; }@OverridepublicStringtoString(){return"SuccessKilled{"+"seckillId="+ seckillId +", userPhone="+ userPhone +", state="+ state +", createTime="+ createTime +", seckill="+ seckill +'}'; }}
③ SeckillDao
packagecom.force4us.dao;importcom.force4us.entity.Seckill;importorg.apache.ibatis.annotations.Param;importjava.util.Date;importjava.util.List;importjava.util.Map;publicinterfaceSeckillDao{/** * 减库存 *@paramseckillId *@paramkillTime *@return如果影响行数>1,表示更新库存的记录行数 */intreduceNumber(@Param("seckillId")longseckillId,@Param("killTime") Date killTime);/** * 根据id查询秒杀的商品信息 *@paramseckillId *@return*/Seckill queryById(@Param("seckillId")longseckillId);/** * 根据偏移量查询秒杀商品列表 *@paramoffset *@paramlimit *@return*/List queryAll(@Param("offset")intoffset,@Param("limit")intlimit);voidkillByProcedure(Map paramMap);}
④ SuccessKilledDao
packagecom.force4us.dao;importcom.force4us.entity.SuccessKilled;importorg.apache.ibatis.annotations.Param;publicinterfaceSuccessKilledDao{/**
* 插入购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return 插入的行数
*/intinsertSuccessKilled(@Param("seckillId") long seckillId,@Param("userPhone") long userPhone);/**
* 根据秒杀商品ID查询明细SuccessKilled对象, 携带了Seckill秒杀产品对象
* @param seckillId
* @param userPhone
* @return
*/SuccessKilledqueryByIdWithSeckill(@Param("seckillId") long ,@Param("userPhone") long userPhone);}
⑤ mybatis配置文件:
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
select name as title(实体中的属性名是title) form table;
开启后mybatis会自动帮我们把表中name的值赋到对应实体的title属性中
-->
⑥ SeckillDao.xml
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">UPDATE seckill SET number = number - 1 WHERE seckill_id = #{seckillId} AND start_time #{killTime} AND end_time >= #{killTime}AND number > 0SELECT * FROM seckill WHERE seckill_id = #{seckillId}SELECT * FROM seckill ORDER BY create_time DESC limit #{offset},#{limit}CALL excuteSeckill( #{seckillId, jdbcType=BIGINT, mode=IN},
#{phone, jdbcType=BIGINT, mode=IN},
#{killTime, jdbcType=TIMESTAMP, mode=IN},
#{result, jdbcType=INTEGER, mode=OUT})
⑦ SuccessKilledDao.xml
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">INSERT ignore INTO success_killed(seckill_id,user_phone,state) VALUES (#{seckillId},#{userPhone},0)SELECT sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id", s.name "seckill.name", s.number "seckill.number", s.start_time "seckill.start_time", s.end_time "seckill.end_time", s.create_time "seckill.create_time" FROM success_killed sk INNER JOIN seckill s ON sk.seckill_id = s.seckill_id WHERE sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}
⑧ Mybatis整合Service:spring-dao.xml
3 Service层
① SeckillService
packagecom.force4us.service;importcom.force4us.dto.Exposer;importcom.force4us.dto.SeckillExecution;importcom.force4us.entity.Seckill;importcom.force4us.exception.RepeatKillException;importcom.force4us.exception.SeckillCloseException;importcom.force4us.exception.SeckillException;importjava.util.List;/**业务接口:站在使用者(程序员)的角度设计接口
* 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好
* 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)
*/publicinterfaceSeckillService{/** * 查询全部秒杀记录 *@return*/ListgetSeckillList();/** * 查询单个秒杀记录 *@paramseckillId *@return*/SeckillgetById(longseckillId);/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间
*/ExposerexportSeckillUrl(longseckillId);/** * 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常 *@paramseckillId *@paramuserPhone *@parammd5 *@return*@throwsSeckillException *@throwsRepeatKillException *@throwsSeckillCloseException */SeckillExecutionexecuteSeckill(longseckillId,longuserPhone, String md5)throwsSeckillException, RepeatKillException, SeckillCloseException;SeckillExecutionexecuteSeckillProcedure(longseckillId,longuserPhone,String md5)throwsSeckillException,RepeatKillException,SeckillCloseException;}
② SeckillServiceImpl
packagecom.force4us.service.impl;importcom.force4us.dao.SeckillDao;importcom.force4us.dao.SuccessKilledDao;importcom.force4us.dao.cache.RedisDao;importcom.force4us.dto.Exposer;importcom.force4us.dto.SeckillExecution;importcom.force4us.entity.Seckill;importcom.force4us.entity.SuccessKilled;importcom.force4us.enums.SeckillStatEnum;importcom.force4us.exception.RepeatKillException;importcom.force4us.exception.SeckillCloseException;importcom.force4us.exception.SeckillException;importcom.force4us.service.SeckillService;importorg.apache.commons.collections4.MapUtils;importorg.slf4j.Logger;importorg.slf4j.LoggerFactory;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;importorg.springframework.transaction.annotation.Transactional;importorg.springframework.util.DigestUtils;importjavax.annotation.Resource;importjava.util.Date;importjava.util.HashMap;importjava.util.List;importjava.util.Map; @Servicepublicclass SeckillServiceImpl implements SeckillService {//日志对象privateLogger logger = LoggerFactory.getLogger(this.getClass()); @AutowiredprivateSeckillDao seckillDao; @AutowiredprivateSuccessKilledDao successKilledDao; @AutowiredprivateRedisDao redisDao;//加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好privatefinalStringsalt ="sadjgioqwelrhaljflutoiu293480523*&%*&*#";publicList getSeckillList() {returnseckillDao.queryAll(0,4); }publicSeckill getById(longseckillId) {returnseckillDao.queryById(seckillId); }publicExposer exportSeckillUrl(longseckillId) {//缓存优化//1。访问rediSeckill seckill = redisDao.getSeckill(seckillId);if(seckill ==null) {//2.访问数据库seckill = seckillDao.queryById(seckillId);if(seckill ==null) {//说明查不到这个秒杀产品的记录returnnewExposer(false, seckillId); }else{//3,放入redisredisDao.putSeckill(seckill); } } Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); Date nowTime =newDate();//若是秒杀未开启if(nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {returnnewExposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime()); }//秒杀开启,返回秒杀商品的id、用给接口加密的md5Stringmd5 = getMD5(seckillId);returnnewExposer(true, md5, seckillId); }privateStringgetMD5(longseckillId) {Stringbase = seckillId +"/"+ salt;Stringmd5 = DigestUtils.md5DigestAsHex(base.getBytes());returnmd5; } @Transactional/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/publicSeckillExecution executeSeckill(longseckillId,longuserPhone,Stringmd5)throwsSeckillException, RepeatKillException, SeckillCloseException {if(md5 ==null|| !md5.equals(getMD5(seckillId))) {thrownewSeckillException("seckill data rewrite"); }//执行秒杀逻辑:减库存+记录购买行为Date nowTime =newDate();try{//否则更新了库存,秒杀成功,增加明细intinsertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);//看是否该明细被重复插入,即用户是否重复秒杀if(insertCount <=0) {thrownewRepeatKillException("seckill repeated"); }else{//减库存,热点商品竞争,update方法会拿到行级锁intupdateCount = seckillDao.reduceNumber(seckillId, nowTime);if(updateCount <=0) {//没有更新库存记录,说明秒杀结束 rollbackthrownewSeckillCloseException("seckill is closed"); }else{//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commitSuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);returnnewSeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); } } }catch(SeckillCloseException e1) {throwe1; }catch(RepeatKillException e2) {throwe2; }catch(Exception e) { logger.error(e.getMessage(), e);//所有编译器异常,转化成运行期异常thrownewSeckillException("seckill inner error:"+ e.getMessage()); } }publicSeckillExecution executeSeckillProcedure(longseckillId,longuserPhone,Stringmd5)throwsSeckillException, RepeatKillException, SeckillCloseException {if(md5 ==null|| !md5.equals(getMD5(seckillId))) {returnnewSeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE); } Date time =newDate(); Mapmap=newHashMap();map.put("seckillId", seckillId);map.put("phone", userPhone);map.put("killTime", time);map.put("result",null);try{ seckillDao.killByProcedure(map);intresult = MapUtils.getInteger(map,"result",-2);if(result ==1) { SuccessKilled successKill = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);returnnewSeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKill); }else{returnnewSeckillExecution(seckillId, SeckillStatEnum.stateOf(result)); } }catch(Exception e) { logger.error(e.getMessage(), e);returnnewSeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR); } }}
③ 异常的处理:
a.SeckillCloseException
packagecom.force4us.exception; publicclassSeckillCloseExceptionextendsSeckillException{ publicSeckillCloseException(Stringmessage) {super(message); } publicSeckillCloseException(Stringmessage,Throwablecause) {super(message, cause); }}
b. SeckillException
packagecom.force4us.exception; publicclassRepeatKillExceptionextendsSeckillException{ publicRepeatKillException(Stringmessage) {super(message); } publicRepeatKillException(Stringmessage,Throwablecause) {super(message, cause); }}
c. RepeatKillException
packagecom.force4us.exception; publicclassSeckillExceptionextendsRuntimeException{ publicSeckillException(Stringmessage) {super(message); } publicSeckillException(Stringmessage,Throwablecause) {super(message, cause); }}
④ 枚举SeckillStatEnum
package com.force4us.enums;public enum SeckillStatEnum { SUCCESS(1,"秒杀成功"), END(0,"秒杀结束"), REPEAT_KILL(-1,"重复秒杀"), INNER_ERROR(-2,"系统异常"), DATE_REWRITE(-3,"数据篡改"); private intstate; private StringstateInfo; SeckillStatEnum(intstate, StringstateInfo){ this.state=state; this.stateInfo =stateInfo; } public int getState() { returnstate; } public String getStateInfo() { returnstateInfo; } public static SeckillStatEnumstateOf(int index){for(SeckillStatEnumstate: values()){ if(state.getState() == index){ returnstate; } } return null; }}
⑤ spring_spring.xml文件
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"
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/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
4.Web层,JSP页面和JS
(1) 详情页流程逻辑逻辑
(2) 配置web.xml
[html] view plain copy
class="language-html"> version="1.0" encoding="UTF-8"?>
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
- This is the Cocoon web-app configurations file
-
- $Id$
-->
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">
seckill-dispatcher
org.springframework.web.servlet.DispatcherServlet
配置SpringMVC 需要配置的文件
spring-dao.xml,spring-service.xml,spring-web.xml
Mybites -> spring -> springMvc
-->
contextConfigLocation
classpath:spring/spring-*.xml
seckill-dispatcher
/
(3) SeckillResult
packagecom.force4us.dto;//将所有的ajax请求返回类型,全部封装成json数据publicclassSeckillResult{privatebooleansuccess;privateT data;privateStringerror;publicSeckillResult(booleansuccess, T data){this.success = success;this.data = data; }publicSeckillResult(booleansuccess, Stringerror){this.success = success;this.error=error; }publicbooleanisSuccess(){returnsuccess; }publicvoidsetSuccess(booleansuccess){this.success = success; }publicTgetData(){returndata; }publicvoidsetData(T data){this.data = data; }publicStringgetError(){returnerror; }publicvoidsetError(Stringerror){this.error=error; }}
(4) spring-web.xml
a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
c:xml,json的默认读写支持-->
1).加入对静态资源处理:js,gif,png
2).允许使用 "/" 做整体映射
-->
(5) SeckillController中:
packagecom.force4us.web;importcom.force4us.dto.Exposer;importcom.force4us.dto.SeckillExecution;importcom.force4us.dto.SeckillResult;importcom.force4us.entity.Seckill;importcom.force4us.enums.SeckillStatEnum;importcom.force4us.exception.RepeatKillException;importcom.force4us.exception.SeckillCloseException;importcom.force4us.exception.SeckillException;importcom.force4us.service.SeckillService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Controller;importorg.springframework.test.annotation.Repeat;importorg.springframework.ui.Model;importorg.springframework.web.bind.annotation.*;importjava.util.Date;importjava.util.List;@Controller@RequestMapping("/seckill")publicclassSeckillController{@AutowiredprivateSeckillService seckillService;@RequestMapping(value ="/list",method= RequestMethod.GET)publicString list(Model model) { List list = seckillService.getSeckillList(); model.addAttribute("list",list);return"list"; }@RequestMapping(value ="/{seckillId}/detail",method = RequestMethod.GET)publicString detail(@PathVariable("seckillId")LongseckillId, 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.POST,produces = {"application/json;charset=UTF-8"})@ResponseBodypublicSeckillResult exposer(@PathVariable("seckillId")LongseckillId){ SeckillResult result;try{ Exposer exposer = seckillService.exportSeckillUrl(seckillId); result = new SeckillResult(true,exposer); }catch(Exception e) { e.printStackTrace(); result = new SeckillResult(false,e.getMessage()); }returnresult; }@RequestMapping(value="/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})@ResponseBodypublicSeckillResult execute(@PathVariable("seckillId")LongseckillId,@PathVariable("md5")String md5,@CookieValue(value="killPhone", required = false)Longphone){if(phone ==null){returnnew SeckillResult(false,"未注册"); } SeckillResult result;try{ SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone, md5);returnnew SeckillResult(true,execution); }catch(RepeatKillException e1) { SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);returnnew SeckillResult(true,execution); }catch(SeckillCloseException e2){ SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);returnnew SeckillResult(true,execution); }catch(Exception e){ SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);returnnew SeckillResult(true,execution); } }@RequestMapping(value ="/time/now", method = RequestMethod.GET)@ResponseBodypublicSeckillResult time(){ Date now = new Date();returnnew SeckillResult(true,now.getTime()); }@RequestMapping("/test")publicString test(){return"helloworld"; }}
(6) list.jsp
<%@pagecontentType="text/html;charset=UTF-8"language="java"%><%@includefile="common/tag.jsp"%>秒杀列表页<%@includefile="/WEB-INF/jsp/common/head.jsp"%>
秒杀列表
名称库存开始时间结束时间创建时间详情页${sk.name}${sk.number}详情(7) details.jsp
<%@pagecontentType="text/html;charset=UTF-8"language="java"%><%@includefile="common/tag.jsp"%>秒杀详情页<%@includefile="common/head.jsp"%>
${seckill.name}
<%--显示time图标--%><%--展示倒计时--%><%--登录弹出层输入电话--%>秒杀电话:<%--验证信息--%>Submit<%--jQueryCookie操作插件--%><%--jQuerycountDown倒计时插件--%>$(function(){seckill.detail.init({
seckillId:${seckill.seckillId}{seckill.startTime.time}{seckill.endTime.time}
(8) seckill.js
//存放主要交互逻辑的js代码// javascript 模块化(package.类.方法)varseckill = {//封装秒杀相关ajax的urlURL: {now:function(){return'/seckill/time/now'; },exposer:function(seckillId){return'/seckill/'+ seckillId +'/exposer'; },execution:function(seckillId, md5){return'/seckill/'+ seckillId +'/'+ md5 +'/execution'; } },//验证手机号validatePhone:function(phone){if(phone && phone.length ==11&& !isNaN(phone)){returntrue; }else{returnfalse; } },//详情页秒杀逻辑detail:{//详情页初始化init:function(params){//手机验证和登录,计时交互//规划我们的交互流程//在cookie中查找手机号varkillPhone = $.cookie('killPhone');//验证手机号if(!seckill.validatePhone(killPhone)){//绑定手机,控制输出varkillPhoneModal = $('#killPhoneModal'); killPhoneModal.modal({show:true,//显示弹出层backdrop:'static',//禁止位置关闭keyboard:false//关闭键盘事件}); $('#killPhoneBtn').click(function(){varinputPhone = $('#killPhoneKey').val();console.log("inputPhone"+ inputPhone);if(seckill.validatePhone(inputPhone)){//电话写入cookie,7天过期$.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'});//验证通过,刷新页面window.location.reload(); }else{ $('#killPhoneMessage').hide().html('手机号错误').show(300); } }); }//已经登录//计时交互varstartTime = params['startTime'];varendTime = params['endTime'];varseckillId = params['seckillId']; $.get(seckill.URL.now(), {},function(result){if(result && result['success']) {varnowTime = result['data'];//时间判断 计时交互seckill.countDown(seckillId, nowTime, startTime, endTime); }else{console.log('result: '+ result); alert('result: '+ result); } }); } },handlerSeckill:function(seckillId, node){//获取秒杀地址,控制显示器,执行秒杀node.hide().html('开始秒杀'); $.post(seckill.URL.exposer(seckillId), {},function(result){//在回调函数种执行交互流程if(result && result['success']) {varexposer = result['data'];if(exposer['exposed']) {//开启秒杀//获取秒杀地址varmd5 = exposer['md5'];varkillUrl = seckill.URL.execution(seckillId, md5);console.log("killUrl: "+ killUrl);//绑定一次点击事件$('#killBtn').one('click',function(){//执行秒杀请求//1.先禁用按钮$(this).addClass('disabled');//,<-$(this)===('#killBtn')->//2.发送秒杀请求执行秒杀$.post(killUrl, {},function(result){if(result && result['success']) {varkillResult = result['data'];varstate = killResult['state'];varstateInfo = killResult['stateInfo'];//显示秒杀结果node.html(''+ stateInfo +''); } }); }); node.show(); }else{//未开启秒杀(浏览器计时偏差)varnow = exposer['now'];varstart = exposer['start'];varend = exposer['end']; seckill.countDown(seckillId, now, start, end); } }else{console.log('result: '+ result); } }); },countDown:function(seckillId, nowTime, startTime, endTime){console.log(seckillId +'_'+ nowTime +'_'+ startTime +'_'+ endTime);varseckillBox = $('#seckill-box');if(nowTime > endTime) {//秒杀结束seckillBox.html('秒杀结束!'); }elseif(nowTime < startTime) {//秒杀未开始,计时事件绑定varkillTime =newDate(startTime +1000);//todo 防止时间偏移seckillBox.countdown(killTime,function(event){//时间格式varformat = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 '); seckillBox.html(format); }).on('finish.countdown',function(){//时间完成后回调事件//获取秒杀地址,控制现实逻辑,执行秒杀console.log('______fininsh.countdown'); seckill.handlerSeckill(seckillId, seckillBox); }); }else{//秒杀开始seckill.handlerSeckill(seckillId, seckillBox); } }}
5.优化:
由于减少库存和购买明细需要在同一事物当中,在次中间会出现网络延迟,GC,缓存,数据库的并发等,所以需要进行优化。
(1) 使用Redis优化:具体代码看上面。
(2) 调整业务逻辑:先进行insert,插入购买明细,然后进行减少库存数量,具体代码看上面。
(3) 调用存储过程seckill.sql
-- 秒杀执行存储过程DELIMITER $$ -- console ;转换为$$--定义存储参数--参数:in输入参数;out输出参数-- rowCount():返回上一条修改类型sql(delete,insert,update)的影响行数-- rowCount: 0:未修改数据 >0:表示修改的行数 <0:sql错误/未执行修改sqlCREATE PROCEDURE excuteSeckill(INfadeSeckillId INT,INfadeUserPhone VARCHAR (15),INfadeKillTime TIMESTAMP ,OUT fadeResult INT) BEGIN DECLARE insert_count INT DEFAULT 0; START TRANSACTION ; INSERT ignore success_kill(seckill_id,user_phone,status,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime); --先插入购买明细 SELECT ROW_COUNT() INTO insert_count;IF(insert_count = 0) THEN ROLLBACK ;SETfadeResult = -1; --重复秒杀 ELSEIF(insert_count < 0) THEN ROLLBACK ;SETfadeResult = -2; --内部错误ELSE--已经插入购买明细,接下来要减少库存 UPDATE seckillSETnumber = number -1 WHERE seckill_id = fadeSeckillIdANDstart_time < fadeKillTimeANDend_time > fadeKillTimeANDnumber > 0; SELECT ROW_COUNT() INTO insert_count;IF(insert_count = 0) THEN ROLLBACK ;SETfadeResult = 0; --库存没有了,代表秒杀已经关闭 ELSEIF (insert_count < 0) THEN ROLLBACK ;SETfadeResult = -2; --内部错误ELSECOMMIT ; --秒杀成功,事务提交SETfadeResult = 1; --秒杀成功返回值为1 ENDIF; ENDIF; END$$ DELIMITER ;SET@fadeResult = -3;-- 执行存储过程CALL excuteSeckill(1003,18810464493,NOW(),@fadeResult);-- 获取结果SELECT @fadeResult; --存储过程-- 1、存储过程优化:事务行级锁持有的时间-- 2、不要过度依赖存储过程
6.系统部署: