1 需求分析和技术难点:

1.1 分析:

秒杀的时候:减少库存和购买记录明细两个事件保持在同一个事物中。

使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。

1.2 秒杀难点:事务和行级锁的处理

1.3 实现那些秒杀系统(以天猫的秒杀系统为例)

1.4 我们如何实现秒杀功能?

• 秒杀接口暴漏

• 执行秒杀

• 相关查询

下面我们以主要代码实现秒杀系统:

2.数据库设计和DAO层

2.1 数据库设计

-- 数据库初始化脚本-- 创建数据库
CREATE DATABASE seckill;
-- 使用数据库
use seckill;
CREATE TABLE seckill(`seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID',`name` VARCHAR(120) NOT NULL COMMENT '商品名称',`number` int NOT NULL COMMENT '库存数量',`start_time` TIMESTAMP  NOT NULL COMMENT '秒杀开始时间',`end_time`   TIMESTAMP   NOT NULL COMMENT '秒杀结束时间',`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (seckill_id),key idx_start_time(start_time),key idx_end_time(end_time),key idx_create_time(create_time)
)ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';-- 初始化数据
INSERT into seckill(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');-- 秒杀成功明细表
-- 用户登录认证相关信息(简化为手机号)
CREATE TABLE success_killed(`seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',`user_phone` BIGINT NOT NULL COMMENT '用户手机号',`state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',PRIMARY KEY(seckill_id,user_phone),/*联合主键*/KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';-- SHOW CREATE TABLE seckill;#显示表的创建信息

2.2 Dao层和对应的实体

2.2.1 Seckill.java

package com.force4us.entity;
import org.springframework.stereotype.Component;
import java.util.Date;public class Seckill {private long seckillId;private String name;private int number;private Date startTime;private Date endTime;private Date createTime;public long getSeckillId() {return seckillId;}public void setSeckillId(long seckillId) {this.seckillId = seckillId;}public String getName() {return name;}public void setName(String name) {this.name = name;}public int getNumber() {return number;}public void setNumber(int number) {this.number = number;}public Date getStartTime() {return startTime;}public void setStartTime(Date startTime) {this.startTime = startTime;}public Date getEndTime() {return endTime;}public void setEndTime(Date endTime) {this.endTime = endTime;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}@Overridepublic String toString() {return "Seckill{" +"seckillId=" + seckillId +", name='" + name + '\'' +", number=" + number +", startTime=" + startTime +", endTime=" + endTime +", createTime=" + createTime +'}';}
}

2.2.2 SuccessKilled.java

package com.force4us.entity;
import org.springframework.stereotype.Component;
import java.util.Date;
public class SuccessKilled {private long seckillId;private long userPhone;private short state;private Date createTime;private Seckill seckill;public long getSeckillId() {return seckillId;}public void setSeckillId(long seckillId) {this.seckillId = seckillId;}public long getUserPhone() {return userPhone;}public void setUserPhone(long userPhone) {this.userPhone = userPhone;}public short getState() {return state;}public void setState(short state) {this.state = state;}public Date getCreateTime() {return createTime;}public void setCreateTime(Date createTime) {this.createTime = createTime;}public Seckill getSeckill() {return seckill;}public void setSeckill(Seckill seckill) {this.seckill = seckill;}@Overridepublic String toString() {return "SuccessKilled{" +"seckillId=" + seckillId +", userPhone=" + userPhone +", state=" + state +", createTime=" + createTime +", seckill=" + seckill +'}';}
}

2.2.3 SeckillDao

package com.force4us.dao;
import com.force4us.entity.Seckill;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;public interface SeckillDao {/*** 减库存* @param seckillId* @param killTime* @return 如果影响行数>1,表示更新库存的记录行数*/int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);/*** 根据id查询秒杀的商品信息* @param seckillId* @return*/Seckill queryById(@Param("seckillId") long seckillId);/*** 根据偏移量查询秒杀商品列表* @param offset* @param limit* @return*/List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);void killByProcedure(Map<String,Object> paramMap);
}

2.2.4 SuccessKilledDao

package com.force4us.dao;
import com.force4us.entity.SuccessKilled;
import org.apache.ibatis.annotations.Param;
public interface SuccessKilledDao {/*** 插入购买明细,可过滤重复* @param seckillId* @param userPhone* @return 插入的行数*/int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);/*** 根据秒杀商品ID查询明细SuccessKilled对象, 携带了Seckill秒杀产品对象* @param seckillId* @param userPhone* @return*/SuccessKilled queryByIdWithSeckill(@Param("seckillId") long , @Param("userPhone") long userPhone);
}

2.2.5 mybatis配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configurationPUBLIC "-//mybatis.org//DTD Config 3.0//EN""http://mybatis.org/dtd/mybatis-3-config.dtd"><configuration><!-- 配置全局属性 --><settings><!-- 使用jdbc的getGeneratekeys获取自增主键值 --><setting name="useGeneratedKeys" value="true"/><!--使用列别名替换列名  默认值为trueselect name as title(实体中的属性名是title) form table;开启后mybatis会自动帮我们把表中name的值赋到对应实体的title属性中--><setting name="useColumnLabel" value="true"/><!--开启驼峰命名转换Table:create_time到 Entity(createTime)--><setting name="mapUnderscoreToCamelCase" value="true"/></settings></configuration>

2.2.6 SeckillDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.force4us.dao.SeckillDao"><update id="reduceNumber">UPDATE seckillSET number = number - 1WHERE seckill_id = #{seckillId}AND start_time <![CDATA[ <= ]]> #{killTime}AND end_time >= #{killTime}AND number > 0</update><select id="queryById" resultType="Seckill" parameterType="long">SELECT *FROM seckillWHERE seckill_id = #{seckillId}</select><select id="queryAll" resultType="Seckill">SELECT *FROM seckillORDER BY create_time DESClimit #{offset},#{limit}</select><select id="killByProcedure" statementType="CALLABLE">CALL excuteSeckill(#{seckillId, jdbcType=BIGINT, mode=IN},#{phone, jdbcType=BIGINT, mode=IN},#{killTime, jdbcType=TIMESTAMP, mode=IN},#{result, jdbcType=INTEGER, mode=OUT})</select>
</mapper>

2.2.7 SuccessKilledDao.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.force4us.dao.SuccessKilledDao"><insert id="insertSuccessKilled"><!--当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore-->INSERT ignore INTO success_killed(seckill_id,user_phone,state)VALUES (#{seckillId},#{userPhone},0)</insert><select id="queryByIdWithSeckill" resultType="SuccessKilled"><!--根据seckillId查询SuccessKilled对象,并携带Seckill对象--><!--如何告诉mybatis把结果映射到SuccessKill属性同时映射到Seckill属性--><!--可以自由控制SQL语句-->SELECTsk.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 skINNER JOIN seckill s ON sk.seckill_id = s.seckill_idWHERE sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}</select>
</mapper>

2.2.8 Mybatis整合Service:spring-dao.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:contex="http://www.springframework.org/schema/context"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"><!-- 配置整合mybatis过程--><!-- 1、配置数据库相关参数--><contex:property-placeholder location="classpath:jdbc.properties"/><!-- 2、配置数据库连接池--><bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"><!-- 配置链接属性--><property name="driverClass" value="${jdbc.driver}"/><property name="user" value="${jdbc.username}"/><property name="password" value="${jdbc.password}"/><property name="jdbcUrl" value="${jdbc.url}"/><!-- 配置c3p0私有属性--><property name="maxPoolSize" value="30"/><property name="minPoolSize" value="10"/><!--关闭连接后不自动commit--><property name="autoCommitOnClose" value="false"/><!--获取连接超时时间--><property name="checkoutTimeout" value="1000"/><!--当获取连接失败重试次数--><property name="acquireRetryAttempts" value="2"/></bean><!-- 3、配置sqlSessionFactory对象--><bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"><!--注入数据库连接池--><property name="dataSource" ref="dataSource"/><!-- 配置mybatis全局配置文件:mybatis-config.xml--><property name="configLocation" value="classpath:mybatis-config.xml"/><!-- 扫描entity包,使用别名,多个用;隔开--><property name="typeAliasesPackage" value="com.force4us.entity"/><!-- 扫描sql配置文件:mapper需要的xml文件--><property name="mapperLocations" value="classpath:mapper/*.xml"/></bean><!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器--><bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"><!-- 注入sqlSessionFactory--><property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/><!--给出需要扫描的Dao接口--><property name="basePackage" value="com.force4us.dao"/></bean><!--redisDao--><bean id="redisDao" class="com.force4us.dao.cache.RedisDao"><constructor-arg index="0" value="localhost"/><constructor-arg index="1" value="6379"/></bean>
</beans>

3.Service层

3.1 SeckillService

package com.force4us.service;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.entity.Seckill;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import java.util.List;/**业务接口:站在使用者(程序员)的角度设计接口* 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好* 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)*/
public interface SeckillService {/*** 查询全部秒杀记录* @return*/List<Seckill> getSeckillList();/*** 查询单个秒杀记录* @param seckillId* @return*/Seckill getById(long seckillId);/*** 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间*/Exposer exportSeckillUrl(long seckillId);/***  执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常* @param seckillId* @param userPhone* @param md5* @return* @throws SeckillException* @throws RepeatKillException* @throws SeckillCloseException*/SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)throws SeckillException, RepeatKillException, SeckillCloseException;SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;
}

3.2 SeckillServiceImpl

package com.force4us.service.impl;
import com.force4us.dao.SeckillDao;
import com.force4us.dao.SuccessKilledDao;
import com.force4us.dao.cache.RedisDao;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.entity.Seckill;
import com.force4us.entity.SuccessKilled;
import com.force4us.enums.SeckillStatEnum;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import com.force4us.service.SeckillService;
import org.apache.commons.collections4.MapUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import javax.annotation.Resource;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Service
public class SeckillServiceImpl implements SeckillService {//日志对象private Logger logger = LoggerFactory.getLogger(this.getClass());@Autowiredprivate SeckillDao seckillDao;@Autowiredprivate SuccessKilledDao successKilledDao;@Autowiredprivate RedisDao redisDao;//加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好private final String salt = "sadjgioqwelrhaljflutoiu293480523*&%*&*#";public List<Seckill> getSeckillList() {return seckillDao.queryAll(0, 4);}public Seckill getById(long seckillId) {return seckillDao.queryById(seckillId);}public Exposer exportSeckillUrl(long seckillId) {//缓存优化//1。访问rediSeckill seckill = redisDao.getSeckill(seckillId);if (seckill == null) {//2.访问数据库seckill = seckillDao.queryById(seckillId);if (seckill == null) {//说明查不到这个秒杀产品的记录return new Exposer(false, seckillId);} else {//3,放入redisredisDao.putSeckill(seckill);}}Date startTime = seckill.getStartTime();Date endTime = seckill.getEndTime();Date nowTime = new Date();//若是秒杀未开启if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());}//秒杀开启,返回秒杀商品的id、用给接口加密的md5String md5 = getMD5(seckillId);return new Exposer(true, md5, seckillId);}private String getMD5(long seckillId) {String base = seckillId + "/" + salt;String md5 = DigestUtils.md5DigestAsHex(base.getBytes());return md5;}@Transactional/*** 使用注解控制事务方法的优点:* 1.开发团队达成一致约定,明确标注事务方法的编程风格* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制*/public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {if (md5 == null || !md5.equals(getMD5(seckillId))) {throw new SeckillException("seckill data rewrite");}//执行秒杀逻辑:减库存+记录购买行为Date nowTime = new Date();try {//否则更新了库存,秒杀成功,增加明细int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);//看是否该明细被重复插入,即用户是否重复秒杀if (insertCount <= 0) {throw new RepeatKillException("seckill repeated");} else {//减库存,热点商品竞争,update方法会拿到行级锁int updateCount = seckillDao.reduceNumber(seckillId, nowTime);if (updateCount <= 0) {//没有更新库存记录,说明秒杀结束 rollbackthrow new SeckillCloseException("seckill is closed");} else {//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commitSuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);}}} catch (SeckillCloseException e1) {throw e1;} catch (RepeatKillException e2) {throw e2;} catch (Exception e) {logger.error(e.getMessage(), e);//所有编译器异常,转化成运行期异常throw new SeckillException("seckill inner error:" + e.getMessage());}}public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {if (md5 == null || !md5.equals(getMD5(seckillId))) {return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);}Date time = new Date();Map<String, Object> map = new HashMap<String, Object>();map.put("seckillId", seckillId);map.put("phone", userPhone);map.put("killTime", time);map.put("result", null);try {seckillDao.killByProcedure(map);int result = MapUtils.getInteger(map, "result", -2);if (result == 1) {SuccessKilled successKill = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKill);} else {return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));}} catch (Exception e) {logger.error(e.getMessage(), e);return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);}}
}

3.3 异常的处理:

3.3.1 SeckillCloseException

package com.force4us.exception;public class SeckillCloseException extends SeckillException{public SeckillCloseException(String message) {super(message);}public SeckillCloseException(String message, Throwable cause) {super(message, cause);}
}

3.3.2 SeckillException

package com.force4us.exception;public class RepeatKillException extends SeckillException{public RepeatKillException(String message) {super(message);}public RepeatKillException(String message, Throwable cause) {super(message, cause);}
}

3.3.3 RepeatKillException

package com.force4us.exception;public class SeckillException extends RuntimeException{public SeckillException(String message) {super(message);}public SeckillException(String message, Throwable cause) {super(message, cause);}
}

3.3.4 枚举SeckillStatEnum

package com.force4us.enums;
public enum SeckillStatEnum {SUCCESS(1,"秒杀成功"),END(0,"秒杀结束"),REPEAT_KILL(-1,"重复秒杀"),INNER_ERROR(-2,"系统异常"),DATE_REWRITE(-3,"数据篡改");private int state;private String stateInfo;SeckillStatEnum(int state, String stateInfo){this.state = state;this.stateInfo = stateInfo;}public int getState() {return state;}public String getStateInfo() {return stateInfo;}public static SeckillStatEnum stateOf(int index){for(SeckillStatEnum state : values()){if(state.getState() == index){return state;}}return null;}
}

3.3.5 spring_spring.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"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"><!-- 扫描service包下所有使用注解的类型--><context:component-scan base-package="com.force4us.service"/><!-- 配置事务管理器 --><bean id="transactionManager"class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><!-- 注入数据库连接池 --><property name="dataSource" ref="dataSource" /></bean><!-- 配置基于注解的声明式事务 --><tx:annotation-driven transaction-manager="transactionManager" />
</beans>

4.Web层,JSP页面和JS

4.1 详情页流程逻辑逻辑

4.2 配置web.xml

<?xml version="1.0" encoding="UTF-8"?>
<!--Licensed to the Apache Software Foundation (ASF) under one or morecontributor license agreements.  See the NOTICE file distributed withthis 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 withthe License.  You may obtain a copy of the License athttp://www.apache.org/licenses/LICENSE-2.0Unless required by applicable law or agreed to in writing, softwaredistributed 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 andlimitations under the License.
--><!--- This is the Cocoon web-app configurations file-- $Id$-->
<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/javaeehttp://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"version="3.1"metadata-complete="true"><!--用maven创建的web-app需要修改servlet的版本为3.1--><!--配置DispatcherServlet--><servlet><servlet-name>seckill-dispatcher</servlet-name><servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class><!--配置SpringMVC 需要配置的文件spring-dao.xml,spring-service.xml,spring-web.xmlMybites -> spring -> springMvc--><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>

4.3 SeckillResult

package com.force4us.dto;
//将所有的ajax请求返回类型,全部封装成json数据
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;}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;}
}

4.4 spring-web.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!--配置spring mvc--><!--1,开启springmvc注解模式a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapterb.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormatc:xml,json的默认读写支持--><mvc:annotation-driven/><!--2.静态资源默认servlet配置--><!--1).加入对静态资源处理:js,gif,png2).允许使用 "/" 做整体映射--><mvc:default-servlet-handler/><!--3:配置JSP 显示ViewResolver--><bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"><property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/><property name="prefix" value="/WEB-INF/jsp/"/><property name="suffix" value=".jsp"/></bean><!--4:扫描web相关的controller--><context:component-scan base-package="com.force4us.web"/>
</beans>

4.5 SeckillController中:

package com.force4us.web;
import com.force4us.dto.Exposer;
import com.force4us.dto.SeckillExecution;
import com.force4us.dto.SeckillResult;
import com.force4us.entity.Seckill;
import com.force4us.enums.SeckillStatEnum;
import com.force4us.exception.RepeatKillException;
import com.force4us.exception.SeckillCloseException;
import com.force4us.exception.SeckillException;
import com.force4us.service.SeckillService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.test.annotation.Repeat;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;@Controller
@RequestMapping("/seckill")
public class SeckillController {@Autowiredprivate SeckillService seckillService;@RequestMapping(value = "/list",method= RequestMethod.GET)public String list(Model model) {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.POST,produces = {"application/json;charset=UTF-8"})@ResponseBodypublic 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"})@ResponseBodypublic SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,@PathVariable("md5") String md5,@CookieValue(value="killPhone", required = false) Long phone){if(phone == null){return new SeckillResult<SeckillExecution>(false,"未注册");}SeckillResult<SeckillExecution> result;try {SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId,phone, 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)@ResponseBodypublic SeckillResult<Long> time(){Date now = new Date();return new SeckillResult<Long>(true,now.getTime());}@RequestMapping("/test")public String test(){return "helloworld";}
}

4.6 list.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp"%>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --><title>秒杀列表页</title><%@include file="/WEB-INF/jsp/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 items="${list}" var="sk"><tr><td>${sk.name}</td><td>${sk.number}</td><td><fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss" /></td><td><fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss" /></td><td><fmt: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 (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script><!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script></html>

4.7 details.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp"%>
<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1"><!-- 上述3个meta标签*必须*放在最前面,任何其他内容都*必须*跟随其后! --><title>秒杀详情页</title><%@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 class="panel-body"><h2 class="text-danger"><%--显示time图标--%><span class="glyphicon glyphicon-time"></span><%--展示倒计时--%><span class="glyphicon" id="seckill-box"></span></h2></div></div>
</div><%--登录弹出层 输入电话--%>
<div id="killPhoneModal" class="modal fade"><div class="modal-dialog"><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="填写手机号^o^" 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 (necessary for Bootstrap's JavaScript plugins) -->
<script src="https://cdn.bootcss.com/jquery/1.12.4/jquery.min.js"></script><!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
<%--jQuery Cookie操作插件--%>
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<%--jQuery countDown倒计时插件--%>
<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.min.js"></script><script src="/resource/script/seckill.js" typ="text/javascript"></script><script type="text/javascript">$(function(){seckill.detail.init({seckillId:${seckill.seckillId},startTime:${seckill.startTime.time},endTime:${seckill.endTime.time}});})</script>
</html>

4.8 seckill.js

//存放主要交互逻辑的js代码
// javascript 模块化(package.类.方法)var seckill = {//封装秒杀相关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)){return true;}else{return false;}},//详情页秒杀逻辑detail:{//详情页初始化init:function (params) {//手机验证和登录,计时交互//规划我们的交互流程//在cookie中查找手机号var killPhone = $.cookie('killPhone');//验证手机号if(!seckill.validatePhone(killPhone)){//绑定手机,控制输出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('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.countDown(seckillId, nowTime, startTime, endTime);} else {console.log('result: ' + result);alert('result: ' + result);}});}},handlerSeckill: 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.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']) {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.countDown(seckillId, now, start, end);}} else {console.log('result: ' + result);}});},countDown: function (seckillId, nowTime, startTime, endTime) {console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime);var seckillBox = $('#seckill-box');if (nowTime > endTime) {//秒杀结束seckillBox.html('秒杀结束!');} else if (nowTime < startTime) {//秒杀未开始,计时事件绑定var killTime = new Date(startTime + 1000);//todo 防止时间偏移seckillBox.countdown(killTime, function (event) {//时间格式var format = 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,缓存,数据库的并发等,所以需要进行优化。

• 使用Redis优化:具体代码看上面

• 调整业务逻辑:先进行insert,插入购买明细,然后进行减少库存数量。

• 调用存储过程seckill.sql

-- 秒杀执行存储过程
DELIMITER $$ -- console ;转换为$$
--定义存储参数
--参数:in 输入参数;out输出参数
-- rowCount():返回上一条修改类型sql(delete,insert,update)的影响行数
-- rowCount: 0:未修改数据  >0:表示修改的行数 <0:sql错误/未执行修改sql
CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT)BEGINDECLARE 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) THENROLLBACK ;SET fadeResult = -1;  --重复秒杀ELSEIF(insert_count < 0) THENROLLBACK ;SET fadeResult = -2;  --内部错误ELSE  --已经插入购买明细,接下来要减少库存UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0;SELECT ROW_COUNT() INTO insert_count;IF (insert_count = 0)  THENROLLBACK ;SET fadeResult = 0;  --库存没有了,代表秒杀已经关闭ELSEIF (insert_count < 0) THENROLLBACK ;SET fadeResult = -2;  --内部错误ELSECOMMIT ;    --秒杀成功,事务提交SET  fadeResult = 1;  --秒杀成功返回值为1END IF;END IF;END
$$DELIMITER ;SET @fadeResult = -3;
-- 执行存储过程
CALL excuteSeckill(1003,18810464493,NOW(),@fadeResult);
-- 获取结果
SELECT @fadeResult;--存储过程
-- 1、存储过程优化:事务行级锁持有的时间
-- 2、不要过度依赖存储过程

6.系统部署:

Java 实现高并发秒杀相关推荐

  1. 【在线网课】Java高性能高并发秒杀系统方案优化实战

    java教程视频讲座简介: Java高性能高并发秒杀系统方案优化实战 Java秒杀系统方案优化 高性能高并发实战 以"秒杀"这一Java高性能高并发的试金石场景为例,带你通过一系列 ...

  2. Java解决高并发秒杀商品

    在看本文章之前,需要了解Spring boot搭建和使用 ,本篇文章核心问题是如何解决高并发问题. 开发环境:redis缓存4.0.1,Rabbitmq消息队列,Erlang(这个跟MQ环境有关,先安 ...

  3. SpringBoot实现Java高并发秒杀系统之DAO层开发(一)

    SpringBoot实现Java高并发秒杀系统之DAO层开发(一) 秒杀系统在如今电商项目中是很常见的,最近在学习电商项目时讲到了秒杀系统的实现,于是打算使用SpringBoot框架学习一下秒杀系统( ...

  4. java 秒杀 源码 下载_java高并发秒杀系统3-4节秒杀功能实现.mp4

    本Java商城秒杀系统视频教程目录如下:    java高并发秒杀系统1-1节java高并发商城秒杀优化学习指引.mp4 java高并发秒杀系统1-2节项目环境搭建(Eclipse)-节.mp4 ja ...

  5. java商品详情页设计_java高并发秒杀系统3-2节商品详情页上.mp4

    本Java商城秒杀系统视频教程目录如下:    java高并发秒杀系统1-1节java高并发商城秒杀优化学习指引.mp4 java高并发秒杀系统1-2节项目环境搭建(Eclipse)-节.mp4 ja ...

  6. SpringBoot实现Java高并发秒杀系统之Service层开发(二)

    继上一篇文章:SpringBoot实现Java高并发秒杀系统之DAO层开发 我们创建了SpringBoot项目并熟悉了秒杀系统的表设计,下面我们将讲解一下秒杀系统的核心部分:Service业务层的开发 ...

  7. Java高并发秒杀系统【观后总结】

    项目简介 在慕课网上发现了一个JavaWeb项目,内容讲的是高并发秒杀,觉得挺有意思的,就进去学习了一番. 记录在该项目 我结合其资料和观看视频的时候整理出从该项目学到了什么... 项目Dao层 日志 ...

  8. Java高并发秒杀API(四)之高并发优化

    Java高并发秒杀API(四)之高并发优化 1. 高并发优化分析 关于并发 并发性上不去是因为当多个线程同时访问一行数据时,产生了事务,因此产生写锁,每当一个获取了事务的线程把锁释放,另一个排队线程才 ...

  9. java高并发秒杀活动的各种简单实现

    最近遇到比较多数据不一致的问题,大多数都是因为并发请求时,没及时处理的原因,故用一个比较有代表性的业务场景[活动秒杀]来模拟一下这个这种高并发所产生的问题. 众所周知,电商系统的秒杀活动是高并发的很好 ...

最新文章

  1. 解决java.lang.UnsupportedClassVersionError: Bad version number in .class file问题
  2. esp8266烧写机智云固件方法
  3. markdown python整段话_(7)python少儿编程之基础语法(二)
  4. Android Activity为什么要细化出onCreate、onStart、onResume、onPause、onStop、onDesdroy这么多方法让应用去重载?
  5. c 语言栈,C语言栈
  6. MyEclipse10下开发第一个Hibernate小程序
  7. 系统梳理 Tensorflow、PyTorch 等深度学习框架,洞悉 AI 系统底层原理和算法
  8. SQL Server 更新数据表记录
  9. org.apache.commons.fileupload.DiskFileUpload1
  10. 4.10招商银行笔试编程题
  11. Struts2 DTD与XML文件编写
  12. Google maps及51ditu的图片切割及存储方法
  13. 上位机使用python/matlab通过网线VISA/SCPI编程远程控制旧版A.06.04.32的安捷伦agilent矢量网络分析仪(VNA)采集S21参数
  14. 【第三方登录】第三方登录 Part1 —— QQ登录(2016-09最新版)
  15. 网络安全自学入门:(超详细)从入门到精通学习路线规划,学完即可就业
  16. Ubuntu 20.04.2.0 LTS 更改默认关联视频播放器VLC的方法
  17. JES SPOOL UP TO 99%
  18. SEO链接为什么要用nofollow,nofollow属性的作用是什么,nofollow的用法
  19. 图的广度遍历(湖北汽车工业学院数据结构实验)
  20. AES解密,key长度不够16处理

热门文章

  1. serious game
  2. 姚洋:建议国家购买三四线空置房 再低价出售给进城农民
  3. 阿里安筱鹏:一文讲透数字化转型的本质!
  4. Chap和pap认证
  5. 英语口语测试对话软件,英语口语人机对话软件
  6. Java爬虫_资源网站爬取实战
  7. 亚运赛场阿联酋公主亲自上阵 爱骑超千万
  8. Centos7 添加新用户并赋予权限
  9. 1005【顺序结构】马克与爸爸的年龄问题
  10. 计算机学院年会,全国高等院校计算机基础教育研究会2019学术年会在我校成功举办...