1 需求分析和技术难点:
(1) 分析:
秒杀的时候:减少库存和购买记录明细两个事件保持在同一个事物中。
使用联合查询避免同一用户多次秒杀同一商品(利用在插入购物明细表中的秒杀id和用户的唯一标识来避免)。
(2) 秒杀难点:事务和行级锁的处理
|
|
|
(3) 实现那些秒杀系统(以天猫的秒杀系统为例)
|
|
(4) 我们如何实现秒杀功能?
① 秒杀接口暴漏
② 执行秒杀
③ 相关查询
下面我们以主要代码实现秒杀系统:
2.数据库设计和DAO层
(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) Dao层和对应的实体
① 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; -
} -
@Override -
public String toString() { -
return "Seckill{" + -
"seckillId=" + seckillId + -
", name='" + name + '\'' + -
", number=" + number + -
", startTime=" + startTime + -
", endTime=" + endTime + -
", createTime=" + createTime + -
'}'; -
} -
}
② 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; -
} -
@Override -
public String toString() { -
return "SuccessKilled{" + -
"seckillId=" + seckillId + -
", userPhone=" + userPhone + -
", state=" + state + -
", createTime=" + createTime + -
", seckill=" + seckill + -
'}'; -
} -
}
③ 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); -
}
④ 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); -
}
⑤ mybatis配置文件:
-
<?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的getGeneratekeys获取自增主键值 --> -
<setting name="useGeneratedKeys" value="true"/> -
<!--使用列别名替换列名 默认值为true -
select 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>
⑥ 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="com.force4us.dao.SeckillDao"> -
<update id="reduceNumber"> -
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 * -
FROM seckill -
WHERE seckill_id = #{seckillId} -
</select> -
<select id="queryAll" resultType="Seckill"> -
SELECT * -
FROM seckill -
ORDER BY create_time DESC -
limit #{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>
⑦ 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="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语句--> -
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>
⑧ 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层
① 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; -
}
② 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()); -
@Autowired -
private SeckillDao seckillDao; -
@Autowired -
private SuccessKilledDao successKilledDao; -
@Autowired -
private 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。访问redi -
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()); -
} -
//秒杀开启,返回秒杀商品的id、用给接口加密的md5 -
String 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) { -
//没有更新库存记录,说明秒杀结束 rollback -
throw new SeckillCloseException("seckill 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()); -
} -
} -
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); -
} -
} -
}
③ 异常的处理:
a.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); -
} -
}
b. 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); -
} -
}
c. 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); -
} -
}
④ 枚举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; -
} -
}
⑤ 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
(1) 详情页流程逻辑逻辑
(2) 配置web.xml[html] view plain copy
- <code class="language-html"><?xml 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$
- -->
- <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">
- <!--用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.xml
- Mybites -> 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></code>
(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) 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,AnnotationMethodHandlerAdapter -
b.默认提供一系列的功能:数据绑定,数字和日期的[email protected],@DateTimeFormat -
c: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相关的controller--> -
<context:component-scan base-package="com.force4us.web"/> -
</beans>
(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 { -
@Autowired -
private 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"}) -
@ResponseBody -
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId){ -
SeckillResult<Exposer> result; -
try { -
Exposer exposer = seckillService.exportSeckillUrl(seckillId); -
result = new SeckillResult<Exposer>(true,exposer); -
} catch (Exception e) { -
e.printStackTrace(); -
result = new SeckillResult<Exposer>(false,e.getMessage()); -
} -
return result; -
} -
@RequestMapping(value="/{seckillId}/{md5}/execution", method = RequestMethod.POST, -
produces = {"application/json;charset=UTF-8"}) -
@ResponseBody -
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId, -
@PathVariable("md5") String md5, -
@CookieValue(value="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) -
@ResponseBody -
public SeckillResult<Long> time(){ -
Date now = new Date(); -
return new SeckillResult<Long>(true,now.getTime()); -
} -
@RequestMapping("/test") -
public String test(){ -
return "helloworld"; -
} -
}
(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>
(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>
(8) seckill.js
-
//存放主要交互逻辑的js代码 -
// javascript 模块化(package.类.方法) -
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'; -
} -
}, -
//验证手机号 -
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,缓存,数据库的并发等,所以需要进行优化。
(1) 使用Redis优化:具体代码看上面。
(2) 调整业务逻辑:先进行insert,插入购买明细,然后进行减少库存数量,具体代码看上面。
(3) 调用存储过程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) -
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 ; -
SET fadeResult = -1; --重复秒杀 -
ELSEIF(insert_count < 0) THEN -
ROLLBACK ; -
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) THEN -
ROLLBACK ; -
SET fadeResult = 0; --库存没有了,代表秒杀已经关闭 -
ELSEIF (insert_count < 0) THEN -
ROLLBACK ; -
SET fadeResult = -2; --内部错误 -
ELSE -
COMMIT ; --秒杀成功,事务提交 -
SET fadeResult = 1; --秒杀成功返回值为1 -
END IF; -
END IF; -
END -
$$ -
DELIMITER ; -
SET @fadeResult = -3; -
-- 执行存储过程 -
CALL excuteSeckill(1003,18810464493,NOW(),@fadeResult); -
-- 获取结果 -
SELECT @fadeResult; -
--存储过程 -
-- 1、存储过程优化:事务行级锁持有的时间 -
-- 2、不要过度依赖存储过程
6.系统部署:
PS:若想通过源码更好的理解Java实现高并发秒杀,请:https://github.com/luomingkui/seckill