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.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@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