Java 實現高併發秒殺
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-Tc5IQib027qvyjS