1、秒杀系统分析
1.1秒杀系统业务分析
1、秒杀系统的核心是对库存的处理,业务流程图如下所示
2、用户针对库存业务分析
1、减库存
2、记录购买明细(记录秒杀成功信息)
1)记录谁购买成功了
2)成功的时间/有效期
1.2 秒杀系统技术分析
1、为什么需要事务?
一旦用户秒杀成功系统需要做两步操作,减库存以及记录购买明细。利用数据库可以实现这操作的”事务”特性。如果没有控制事务,可能会发生如下情况:
1、减库存成功而记录购买明细失败,会导致少卖
2、记录购买明细成功而减库存失败,会导致超卖
2、数据落地方案选择
关于数据如何落地,有MySQL和NoSQL这两种方案选择。
1、MySQL是关系型数据库,它有多种存储引擎,只有InnoDB存储引擎支持事务。使用InnoDB存储引擎可以帮助我们完成减库存和记录购买明细的事务操作,InnoDB支持行级锁和表级锁,默认使用行级锁。
2、NoSQL是非关系型数据库,对于事务的支持做的不是很好,NoSQL更多的适用于高并发数据读写和海量数据存储等应用场景。
所以事务机制依然是目前可靠的落地方案。
3、MySQL实现秒杀的难点分析
当一个用户成功秒杀某件商品后,其他秒杀这件商品的用户只能等到,直到上一个用户成功提交事务或者回滚事务,他才能得到锁执行秒杀操作。锁的竞争,就是采用数据库方案的难点问题。
MySQL实现秒杀的机制是:事务+行级锁
1.3 秒杀系统优化分析
秒杀系统页面流程逻辑
高并发需要优化的地方
从上图可知,需要优化的有同一时间对详情页的高访问(这里包含了动态资源、静态资源,主要费时间的是资源的加载以及渲染)、获取系统时间(由于获取系统时间非常短,所以这个位置不需要优化)、地址暴露接口(动态资源)、执行秒杀操作(主要是对数据库的访问)。
高并发解决方案
对于固定资源的高速访问可以使用CDN智能选择离用户最近的网络节点,加速用户获取数据的系统,这个过程还可以使用nigix反向代理实现负载均衡,将访问流量分配到nigix集群中来分散压力。
对于同时某一个商品的高流量访问可以采用redis集群进行缓存和分解访问压力,redis每秒支持数十万访问,redis集群则可高达百万级。
记录行为消息(将购买明细写入消息队列,秒杀业务只需从消息队列中读取相关信息进行相应的库存操作,可以避免瞬间过大流量访问秒杀业务导致系统崩溃)可以采用分布式消息队列进行分流。
对于库存的操作,使用Mysql,通过它的事务机制可以避免少卖或超卖的发生,但它的瓶颈在于网络延迟(由于判断逻辑在客户端,当与Mysql服务器进行交互时存在较大的网络延迟,另一个则是事务锁机制的耗时以及客户端频繁的对象销毁创建造成的GC),为缓解这些问题,将判断逻辑放置在服务端并采用存储过程(存储过程是预编译的,编译一次执行多次,比单sql语句效率要高)则能极大的优化网络延迟问题。
mysql瓶颈分析
通过调整逻辑,可以减少延迟,如果先执行购买明细的记录则可避免重复秒杀sql的执行,并且插入购买明细可以并行执行,而对于先执行减库存操作,再执行购买明细的记录它是针对同一行数据的(mysql针对同一行数据的操作大概每秒大概一万多次,不同行数据操作则是十几万次),存在一个行级锁的问题,即若是多个用户在购买同一个商品(减库存操作就阻塞了)则存在两个网络延迟。
2、秒杀系统实现
2.1 DAO层实现
数据库脚本,创建秒杀商品表、秒杀成功明细表。
/*数据初始化脚本*/
---创建数据库
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=utf-8 comment='秒杀库存表';
---初始化数据
INSERT INTO
seckill(name,number,start_time,end_time)
VALUES
('1000元秒杀iphone6',100,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
('500元秒杀ipad2',200,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
('300元秒杀小米4',300,'2018-08-08 00:00:00','2018-08-09 00:00:00'),
('200元秒杀红米note',400,'2018-08-08 00:00:00','2018-08-09 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:已付款',
'create_time' TIMESTAMP NOT NULL comment '创建时间',
PRIMARY KEY (seckill_id,user_phone),/*联合主键*/
key idx_create_time(create_time) /*创建索引*/
)ENGINE=InnoDB DEFAULT charset=utf-8 comment='秒杀成功明细表';
整合Spring和MyBatis
在resources下创建jdbc.properties文件
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf8
username=root
password=root
在resources下创建mybatis-config.xml文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 配置全局属性-->
<settings>
<!-- 使用jdbc的getGeneratedKeys 获取数据库自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!-- 使用列别名替换列名 默认:true
select name as title from table
-->
<setting name="useColumnLabel" value="true" />
<!-- 开启驼峰命名转换:Table(create_time) -> Entity(createTime) -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
在resources目录下创建一个新的目录spring(存放所有spring相关的配置),spring目录下创建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: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/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- 配置整合mybatis过程-->
<!-- 1:配置数据库相关参数properties的属性:${url} -->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!-- 2:数据库连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性-->
<property name="driverClass" value="${driver}" />
<property name="jdbcUrl" value="${url}" />
<property name="user" value="${username}" />
<property name="password" value="${password}" />
<!-- c3p0连接池的私有属性-->
<property name="maxPoolSize" value="30" />
<property name="minPoolSize" value="10" />
<!-- 关闭连接后不自动commit-->
<property name="autoCommitOnClose" value="false" />
<!-- 获取连接超时时间-->
<property name="checkoutTimeout" value="1000" />
<!-- 当获取连接失败重试次数-->
<property name="acquireIncrement" 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.seckill.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.seckill.dao" />
</bean>
<!-- 5:RedisDao配置-->
<bean id="redisDao" class="com.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="localhost"/>
<constructor-arg index="1" value="6379" />
</bean>
</beans>
resources下创建mapper目录,mapper下专门存放dao映射文件。
SeckillDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.ctgu.seckill.dao.SeckillDao">
<update id="reduceNumber">
/*具体的sql*/
update
seckill
set
number=number-1
where seckill_id=#{seckillId}
and start_time<![CDATA[ <= ]]>#{killTime}
and end_time>=#{killTime}
and number >0;
</update>
<select id="queryById" resultType="Seckill" parameterType="long">
select Seckill_id,name,number,start_time,end_time,create_time
from seckill
where seckill_id=#{seckillId}
</select>
<select id="queryAll" resultType="Seckill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
<!--mybatis调用存储过程-->
<select id="killByprocedure" statementType="CALLABLE">
call execute_seckill(
#{seckillId,jdbcType=BIGINT,mode=IN},
#{phone,jdbcType=BIGINT,mode=IN},
#{killTime,jdbcType=TIMESTAMP,mode=IN},
#{result,jdbcType=INTEGER,mode=OUT},
)
</select>
</mapper>
SuccessKilledDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="cn.ctgu.seckill.dao.SuccessKilledDao">
<insert id="insertSuccessKilled">
/*主键冲突,报错,采用忽略*/
insert ignore into success_killed(seckill_id,user_phone)
values(#{seckillId},#{userPhone})
</insert>
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
/*根据id查询successKilled并携带seckill实体
如果告诉mybatis把结果映射到successKilled同时映射seckill属性
*/
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}
</select>
</mapper>
SuccessKilledDao.java
package cn.ctgu.seckill.dao;
import cn.ctgu.seckill.domain.SuccessKilled;
import org.apache.ibatis.annotations.Param;
public interface SuccessKilledDao {
//插入购买明细,可过滤重复
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone")long userPhone);
//根据id查询successKilled并携带秒杀产品对象实体
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId,@Param("userPhone")long userPhone);
}
SeckillDao.java
package cn.ctgu.seckill.dao;
import cn.ctgu.seckill.domain.Seckill;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
import java.util.Map;
public interface SeckillDao {
//减库存
int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime);
//通过id查询商品秒杀列表
Seckill queryById(long seckillId);
//根据偏移量查询商品秒杀列表
List<Seckill> queryAll(@Param("offset") int offet, @Param("limit")int limit);
//使用存储过程实现秒杀
void killByProcedure(Map<String,Object> paramMap);
}
RedisDao.java
package cn.ctgu.seckill.dao.cache;
import cn.ctgu.seckill.domain.Seckill;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisDao {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
private final JedisPool jedisPool;
private RuntimeSchema<Seckill>schema=RuntimeSchema.createFrom(Seckill.class);
public RedisDao(String ip,int port){
jedisPool=new JedisPool(ip,port);
}
public Seckill getSeckill(long seckillId){
//redis操作逻辑
try{
Jedis jedis=jedisPool.getResource();
try {
String key="seckill:"+seckillId;
//并没有实现内部序列化操作
//get->byte[]->反序列化->Object([Seckill])
//采用自定义序列化
//protosbuff:pojo
byte[]bytes=jedis.get(key.getBytes());
//缓存重获取到
if(bytes!=null){
//反序列化需要先创建一个空对象用于存储
Seckill seckill=schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
//seckill被反序列化
return seckill;
}
}finally {
jedis.close();
}
}catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
public String putSeckill(Seckill seckill){
//set Object(Seckill)->序列化->byte[]
try{
Jedis jedis=jedisPool.getResource();
try{
String key="seckill:"+seckill.getSeckillId();
byte[]bytes=ProtostuffIOUtil.toByteArray(seckill,schema,
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
//超时缓存一个小时
int timeout=60*60;//一个小时
String result=jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally {
jedis.close();
}
}catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
}
2.2 Service层实现
配置事务,resources/spring下创建spring-service.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.seckill.service" />
<!-- 配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池-->
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 配置基于注解的声明式事务
默认使用注解来管理事务行为
-->
<tx:annotation-driven transaction-manager="transactionManager" />
</beans>
SeckillService.java
package cn.ctgu.seckill.service;
import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.exception.SeckillException;
import java.util.List;
/*
*
* 业务接口:站在使用者的角度去设计接口
* 三个方面:方法定义粒度,参数,返回类型(return 类型/异常)
*
* */
public interface SeckillService {
/*
*
* 查询所有秒杀记录
* */
List<Seckill> getSeckillList();
/*
*
* 查询单个秒杀记录
*
* */
Seckill getById(long seckillId);
/*
*
* 秒杀开启时输出秒杀接口地址
* 否则输出系统时间和秒杀时间
* */
Exposer exportSeckillUrl(long seckillId);
//执行秒杀操作
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
//执行秒杀操作,存储过程实现
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
}
SeckillServiceImpl.java
package cn.ctgu.seckill.service.impl;
import cn.ctgu.seckill.dao.SeckillDao;
import cn.ctgu.seckill.dao.SuccessKilledDao;
import cn.ctgu.seckill.dao.cache.RedisDao;
import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.domain.SuccessKilled;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.exception.SeckillException;
import cn.ctgu.seckill.service.SeckillService;
import org.apache.commons.collections.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 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());
//注入service依赖
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
@Autowired
private RedisDao redisDao;
//md5盐值字符串,用于混淆md5
private final String slat="faldksjfhaehfasje";
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0,4);
}
public Seckill getById(long seckillId) {
return seckillDao.queryById(seckillId);
}
public Exposer exportSeckillUrl(long seckillId) {
//缓存优化:超时的基础上维护一致性
/*
*1、访问redis
* */
Seckill seckill=redisDao.getSeckill(seckillId);
if(seckill==null){
//2、访问数据库
seckill=seckillDao.queryById(seckillId);
if(seckill==null){
return new Exposer(false,seckillId);
}else{
//3、放入redis
redisDao.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());
}
String md5=getMD5(seckillId);
return new Exposer(true,md5,seckillId);
}
private String getMD5(long seckillId){
String base=seckillId+"/"+slat;
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);
//唯一:seckillId,userPhone
if (insertCount <= 0) {
//重复秒杀
throw new RepeatKillException("seckilled repeat");
} else {
//减库存,热点商品的竞争
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//没有更新到记录,秒杀结束,rollback
throw new SeckillCloseException("seckilled is closed");
} else {
//秒杀成功,commit
SuccessKilled 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());
}
}
@Override
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
if(md5==null||!md5.equals(getMD5(seckillId))){
return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
}
Date killTime=new Date();
Map<String,Object> map=new HashMap<String,Object>();
map.put("seckillId",seckillId);
map.put("phone",userPhone);
map.put("killTime",killTime);
map.put("result",null);
//执行存储过程,result被赋值
try{
seckillDao.killByProcedure(map);
//获取result
int result= MapUtils.getInteger(map,"reuslt",-2);
if(result==1){
SuccessKilled sk=successKilledDao.
queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
}else{
return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
}
}catch (Exception e){
logger.error(e.getMessage(),e);
return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
}
}
}
2.3 Web层实现
整合SpringMVC ,配置web.xml
<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/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1"
metadata-complete="true">
<!-- 修改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.xml
MyBatis -> 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>
resources/spring下创建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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
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">
<!-- 配置SpringMVC-->
<!-- 1.开启SpringMVC注解模式-->
<!-- 简化配置
1)自动注册DefaultAnnotationHandlerMapping, AnnotationMethodHandlerAdapter
2)提供一系列功能:数据绑定,数字和日期的format @NumberFormat,@DateTimeFormat,xml, json默认读写支持。
-->
<mvc:annotation-driven />
<!-- 2.静态资源默认servlet配置
1)加入对静态资源的处理:js,gif,png
2)允许使用 "/" 做整体映射
-->
<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相关的bean-->
<context:component-scan base-package="com.seckill.web" />
</beans>
接口设计,秒杀功能的流程:秒杀接口暴露 -> 执行秒杀 -> 相关查询,秒杀API的URL设计如下:
SeckillController.java
package cn.ctgu.seckill.web;
import cn.ctgu.seckill.domain.Seckill;
import cn.ctgu.seckill.dto.Exposer;
import cn.ctgu.seckill.dto.SeckillExecution;
import cn.ctgu.seckill.dto.SeckillResult;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import cn.ctgu.seckill.exception.RepeatKillException;
import cn.ctgu.seckill.exception.SeckillCloseException;
import cn.ctgu.seckill.service.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import java.util.Date;
import java.util.List;
@Controller
@RequestMapping("/seckill")//url:模块/资源/{id}/细分/seckill/list
public class SeckillController {
private final Logger logger= LoggerFactory.getLogger(this.getClass());
@Autowired
private SeckillService seckillService;
@RequestMapping(value="list",method= RequestMethod.GET)
public String list(Model model){
//获取列表页
List<Seckill> list=seckillService.getSeckillList();
model.addAttribute("list",list);
//list.jsp+model=ModelAndView
return "list";//返回的是/WEB-INF/jsp/"list".jsp
}
@RequestMapping(value="/{seckillId}/detail",method=RequestMethod.GET)
public String detail(@PathVariable("seckill")Long seckiiId,Model model){
if(seckiiId==null){
return "redirect:/seckill/list";
}
Seckill seckill=seckillService.getById(seckiiId);
if(seckill==null){
return "forward:/seckill/list";
}
model.addAttribute("seckill",seckill);
return "detail";
}
//返回ajax json
@RequestMapping(value="/{seckillId}/exporder",
method = RequestMethod.GET,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer>exporser(Long seckillId) {
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}
return result;
}
//执行秒杀
@ResponseBody
@RequestMapping(value="/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
public 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 e){
SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true,execution);
}catch (SeckillCloseException e){
SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(true,execution);
}catch (Exception e){
logger.error(e.getMessage(),e);
SeckillExecution execution = new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true,execution);
}
}
//返回当前时间
@ResponseBody
@RequestMapping(value="/time/now",method=RequestMethod.GET)
public SeckillResult<Long>time(){
Date now=new Date();
return new SeckillResult<Long>(true,now.getTime());
}
}
其他代码
domain层
Seckill.java
package cn.ctgu.seckill.domain;
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;
}
@Override
public String toString() {
return "Seckill{" +
"seckillId=" + seckillId +
", name='" + name + '\'' +
", number=" + number +
", startTime=" + startTime +
", endTime=" + endTime +
", createTime=" + createTime +
'}';
}
}
SuccessKilled.java
package cn.ctgu.seckill.domain;
import java.util.Date;
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
//多对一
private Seckill seckill;
public Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
this.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;
}
}
dto层
Exposer.java
package cn.ctgu.seckill.dto;
/*
*
* 暴露秒杀地址DTO
*
* */
public class Exposer {
//是否开启秒杀
private boolean exposed;
//一种机密措施
private String md5;
//id
private long seckillId;
//系统当前时间(毫秒)
private long now;
//开启时间
private long start;
//结束时间
private long end;
public Exposer(boolean exposed, String md5, long seckillId) {
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
public Exposer(boolean exposed,long seckillId, long now, long start, long end) {
this.exposed = exposed;
this.seckillId=seckillId;
this.now = now;
this.start = start;
this.end = end;
}
public Exposer(boolean exposed, long seckillId) {
this.exposed = exposed;
this.seckillId = seckillId;
}
public boolean isExposed() {
return exposed;
}
public void setExposed(boolean exposed) {
this.exposed = exposed;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public long getNow() {
return now;
}
public void setNow(long now) {
this.now = now;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
}
SeckillExecution.java
package cn.ctgu.seckill.dto;
import cn.ctgu.seckill.domain.SuccessKilled;
import cn.ctgu.seckill.enums.SeckillStatEnum;
import com.sun.net.httpserver.Authenticator;
/*
*
* 封装秒杀执行结果
*
* */
public class SeckillExecution {
private long seckillId;
//秒杀执行结果状态
private int state;
//状态表示
private String stateInfo;
//秒杀成功对象
private SuccessKilled successKilled;
public SeckillExecution(long seckillId, SeckillStatEnum statEnum,int state, String stateInfo, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateInfo();
this.successKilled = successKilled;
}
public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled stateInfo) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateInfo();
}
public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getStateInfo() {
return stateInfo;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
public SuccessKilled getSuccessKilled() {
return successKilled;
}
public void setSuccessKilled(SuccessKilled successKilled) {
this.successKilled = successKilled;
}
}
SeckillResult.java
package cn.ctgu.seckill.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;
}
}
enums层
SeckillStatEnum.java
package cn.ctgu.seckill.enums;
/*
*
* 使用枚举表述常量数据字段
*
* */
public enum SeckillStatEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_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;
}
}
Exception层
RepeatKillExecption.java
package cn.ctgu.seckill.exception;
/*
*
* 重复秒杀异常(运行时期异常)
*
* */
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
SeckillCloseExecption.java
package cn.ctgu.seckill.exception;
/*
*
* 秒杀关闭异常
*
* */
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
SeckillExecption.java
package cn.ctgu.seckill.exception;
/*
*
* 秒杀相关的业务异常
*
* */
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
2.4 前端页面
引入Bootstrap,通过cdn的方式引入Bootstrap相关的文件
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
倒计时插件
<!-- jQuery countDown倒计时插件-->
<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.js"></script>
cookie插件
<!-- jQuery cookie插件-->
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
list.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 引入jstl-->
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html>
<head>
<title>秒杀列表页</title>
<%@include file="common/head.jsp" %>
</head>
<body>
<!-- 页面显示部分-->
<div class="container">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h1>秒杀列表</h1>
</div>
<div class="panel-body">
<table class="table table-hover">
<thead>
<tr>
<td>名称</td>
<td>库存</td>
<td>开始时间</td>
<td>结束时间</td>
<td>创建时间</td>
<td>详情页</td>
</tr>
</thead>
<tbody>
<c:forEach var="sk" items="${list}">
<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">link</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
</html>
detail.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!-- 引入jstl-->
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html>
<head>
<title>秒杀详情页</title>
<%@include file="common/head.jsp" %>
</head>
<body>
<!-- 页面显示部分-->
<div class="container">
<div class="panel panel-default text-center">
<div class="panel-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="填写手机号" 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文件。务必在bootstrap.min.js 之前引入 -->
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<!-- 使用CDN 获取公共js http://www.bootcdn.cn/ -->
<!-- jQuery cookie插件-->
<script src="https://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.js"></script>
<!-- jQuery countDown倒计时插件-->
<script src="https://cdn.bootcss.com/jquery.countdown/2.2.0/jquery.countdown.js"></script>
<!-- 开始编写交互逻辑-->
<script type="text/javascript" src="/resources/script/seckill.js"></script>
<script type="text/javascript">
$(function () {
//使用EL表达式传入参数
seckill.detail.init({
seckillId: ${seckill.seckillId},
startTime: ${seckill.startTime.time}, //毫秒
endTime: ${seckill.endTime.time}
});
});
</script>
</html>
seckill.js
//存放主要交互逻辑js代码
//javascript 模块化
var seckill = {
//封装秒杀相关ajax的URL
URL : {
now : function () {
return '/seckill/time/now';
},
exposer: function (seckillId) {
return '/seckill/' + seckillId + "/exposer";
},
execution:function (seckillId,md5) {
return '/seckill/' + seckillId + '/' + md5 + "/execution";
}
},
//处理秒杀逻辑
handleSeckill : 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'];
console.log(exposer);
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'];
//3.显示秒杀结果
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,start,end);
}
}else{
console.log('result:' + result);
}
});
},
//验证手机号
validatePhone:function (phone) {
if(phone && phone.length == 11 && !isNaN(phone)){
return true;
}else{
return false;
}
},
countdown:function (seckillId,nowTime,startTime,endTime) {
var seckillBox = $("#seckill-box");
//时间判断
if(nowTime > endTime){
//秒杀结束
seckillBox.html("秒杀结束!");
}else if(nowTime < startTime){
//秒杀未开始,计时事件绑定
var killTime = new Date(startTime + 1000);
console.log('nowTime:' + nowTime + ' ,startTime:' + startTime + ' ,endTime:'+endTime);
seckillBox.countdown(killTime,function (event) {
//时间格式
var format = event.strftime('秒杀计时: %D天 %H时 %M分 %S秒');
seckillBox.html(format);
//时间完成后回调时间
}).on('finish.countdown',function () {
//获取秒杀地址,控制实现逻辑,执行秒杀
seckill.handleSeckill(seckillId,seckillBox);
});
}else{
//秒杀开始
console.log('已开始');
seckill.handleSeckill(seckillId,seckillBox);
}
},
//详情页秒杀逻辑
detail: {
//详情页初始化
init : function (params) {
//手机验证和登录,计时交互 - 规划交互流程
//在cookie中查找手机号
var killPhone = $.cookie('killPhone');
//验证手机号,判断用户是否登录
if (!seckill.validatePhone(killPhone)) {
//绑定phone
var killPhoneModal = $("#killPhoneModal");
//显示弹出层
killPhoneModal.modal({
show: true, //显示弹出层
backdrop: 'static', //禁止位置关闭
keyboard: false //关闭键盘事件
});
$("#killPhoneBtn").click(function () {
var inputPhone = $("#killPhoneKey").val();
if (seckill.validatePhone(inputPhone)) {
//电话写入cookie
$.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);
}
})
}
}
}
3、系统演示
秒杀列表页面
输入手机号
系统没有做登录页面,此处使用本地cookie作为替代方案。如果cookie中没有手机号,则在弹出页面填写正确手机号就可以进入秒杀页面。
秒杀成功页面
秒杀未开始页面
秒杀还未开始时通过插件显示倒计时,倒计时时间以服务器时间为准。
资源地址
GitHub代码:秒杀系统代码