1.秒殺業(yè)務的分析
一般的秒殺系統(tǒng)會存在商家,庫存,用戶三個實體,商家添加調(diào)整庫存,庫存用于發(fā)貨和核賬,庫存用戶秒殺或者預售,用戶的付款,退貨也會影響到庫存集體如下圖:
也就是秒殺業(yè)務的核心就是庫存的處理。
庫存業(yè)務分析:首先用戶秒殺成功要相應的減去庫存已經(jīng)記錄購買的明細,這兩項操作組成了一個完整的事務。如下圖:
2.難點分析的分析
主要的難點問題就是競爭多個用戶同時秒殺一種商品。對于mysql 來說競爭反應到背后的技術就是事務和行級鎖。
1.事務工作機制
首先是 開啟事務 start transaction
update 庫存數(shù)量 (競爭出現(xiàn)的地方)
insert 購買明細
commit 事務提交
2 行級鎖
秒殺的難點在于如何高效的處理競爭具體的解決方法會在單寫一遍博客進行解釋。接下來通過一個項目主要實現(xiàn)一下如下的秒殺功能。
3.設計數(shù)據(jù)庫
因為主要只實現(xiàn)秒殺相關的功能這里只設置兩張表。
1.秒殺庫存表下面給出建表語句。
-- 創(chuàng)建秒殺庫存表
CREATE TABLE seckill(
`seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品庫存id',
`name` VARCHAR(120) NOT NULL COMMENT '商品名稱',
`number` INT NOT NULL COMMENT '庫存數(shù)量',
`start_time` TIMESTAMP NOT NULL COMMENT '秒殺開始時間',
`end_time` TIMESTAMP NOT NULL COMMENT '秒殺結束時間',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒殺創(chuàng)建時間',
PRIMARY KEY (`seckill_id`),
/*創(chuàng)建時間索引是為了以后時間查詢的業(yè)務提供方便*/
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='秒殺庫存表'
-- 初始化數(shù)據(jù)
INSERT INTO
seckill(name, number, start_time, end_time)
VALUES
('1000元秒殺iphone6', 100, '2015-11-01 00:00:00', '2018-11-02 00:00:00'),
('500元秒殺ipad2', 200, '2015-11-01 00:00:00', '2018-11-02 00:00:00'),
('300元秒殺小米4', 300, '2015-11-01 00:00:00', '2018-11-02 00:00:00'),
('200元秒殺紅米note', 400, '2015-11-01 00:00:00', '2018-11-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 '狀態(tài)信息:-1無效,0成功,1已付款,2已發(fā)貨',
`create_time` TIMESTAMP NOT NULL COMMENT '創(chuàng)建時間',
PRIMARY KEY (`seckill_id`, `user_phone`),/*聯(lián)合主鍵*/
KEY `idx_create_time` (`create_time`)
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒殺庫存表'
4.DAO編碼
1.創(chuàng)建工程
首先創(chuàng)建一個maven工程seckill工程目錄如下
2.添加依賴
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.wen</groupId>
<artifactId>seckill</artifactId>
<version>0.0.1-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.2.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<thymeleaf.version> 3.0.2.RELEASE </thymeleaf.version>
<thymeleaf-layout-dialect.version> 2.1.1 </thymeleaf-layout-dialect.version>
<tomcat.version>7.0.69</tomcat.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- log4j2 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- web組件支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- thymeleaf模板支持 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- mybatis支持 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.0</version>
</dependency>
<!--pagehelper -->
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!-- mysql連接池 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
</dependency>
<!-- Apache公共類庫 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.4</version>
</dependency>
<!-- google guava公共類庫 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>21.0</version>
</dependency>
<!--熱部署-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<!-- 測試依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--添加切面支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.31</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!--開發(fā)中使用devtools 打包忽略-->
<excludeDevtools>false</excludeDevtools>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
<finalName>seckill</finalName>
</build>
</project>
3工程配置
#數(shù)據(jù)庫連接配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/seckill
username: root
password: 123456
driver-class-name: com.mysql.jdbc.Driver
thymeleaf:
mode: HTML5
#字符集和json格式工具
http:
encoding:
charset: utf-8
converters:
preferred-json-mapper: fastjson
multipart:
max-file-size: 10MB
application:
name: seckill
#mynatis配置
mybatis:
type-aliases-package: com.wen.seckill.model
#mapper加載路徑
mapper-locations: classpath:mapper/*.xml
#myatbis配置文件
config-location: classpath:mybatis-conf.xml
#加載log4j2
logging:
config: classpath:log4j2.xml
level: debug
file:
server:
session-timeout : 3600
port: 80
日志配置文件
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<properties>
<!-- 文件輸出格式 -->
<property name="PATTERN">%d{yyyy-MM-dd HH:mm:ss.SSS} |-%-5level [%thread] %c [%L] -| %msg%n</property>
</properties>
<appenders>
<Console name="Console" target="system_out">
<PatternLayout pattern="${PATTERN}" />
</Console>
</appenders>
<!--配置mybatis日志-->
<loggers>
<logger name="log4j.logger.org.mybatis" level="debug" additivity="false">
<appender-ref ref="Console"/>
</logger>
<logger name="log4j.logger.java.sql" level="debug" additivity="false">
<appender-ref ref="Console"/>
</logger>
<logger name="com.wen.seckill.dao" level="debug" />
<root level="info">
<appenderref ref="Console" />
</root>
</loggers>
</configuration>
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>
<!--設置mybatis日志類型-->
<settings>
<setting name="logImpl" value="LOG4J2"/>
<!--配置的緩存的全局開關。-->
<setting name="cacheEnabled" value="true"/>
<!--延遲加載的全局開關。當開啟時,所有關聯(lián)對象都會延遲加載。 特定關聯(lián)關系中可通過設置fetchType屬性來覆蓋該項的開關狀態(tài)。-->
<setting name="lazyLoadingEnabled" value="true"/>
<!--當沒有為參數(shù)提供特定的 JDBC 類型時,為空值指定 JDBC 類型。 某些驅動需要指定列的 JDBC 類型,多數(shù)情況直接用一般類型即可,比如 NULL、VARCHAR 或 OTHER。-->
<setting name="jdbcTypeForNull" value="NULL"/>
<setting name="useGeneratedKeys" value="true"/>
<setting name="useColumnLabel" value="true"/>
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
4 dao層實體編寫
根據(jù)表結構創(chuàng)建實體
庫存表
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 + "]";
}
}
秒殺記錄表
import java.util.Date;
/**
* 成功秒殺實體
*
*/
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date creteTime;
// 多對一的復合屬性
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 getCreteTime() {
return creteTime;
}
public void setCreteTime(Date creteTime) {
this.creteTime = creteTime;
}
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
+ ", creteTime=" + creteTime + "]";
}
}
4 dao層借口編寫
實體類接口
主要需要的功能有減庫存,秒殺列表,根據(jù)id 檢索商品信息
import java.util.Date;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.wen.seckill.model.Seckill;
@Mapper
public interface SeckillDao {
/**
* 減庫存
* @param seckillId
* @param killTime
* @return 如果影響的行數(shù)大于1 則表示更新庫存成功
*/
int reduceNumber(@Param("seckillId")long seckillId,@Param("killTime")Date killTime);
/**
* 根據(jù)id 查詢秒殺對象
* @param seckillId
* @return
*/
Seckill queryById(@Param("seckillId")long seckillId);
/**
* 獲取秒殺列表
*/
List<Seckill> queryAll();
}
為接口編寫相應的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.wen.seckill.dao.SeckillDao">
<!-- 減少庫存操作 -->
<update id="reduceNumber">
update
seckill
set number=number-1
where seckill_id=#{seckillId}
AND start_time <=#{killTime}
and end_time>=#{killTime}
and number>0
</update>
<!-- 根據(jù)id 查詢 -->
<select id="queryById" resultType="Seckill" parameterType="long">
select seckill_id,name,number,start_time,end_time,create_time from seckill
where seckill_id=#{seckillId}
</select>
<!-- 根據(jù)id 查詢 -->
<select id="queryAll" resultType="Seckill" >
select seckill_id,name,number,start_time,end_time,create_time from seckill
</select>
</mapper>
秒殺接口主要需要兩個功能 1插入秒殺記錄 2秒殺記錄檢索
import java.util.Date;
/**
* 成功秒殺實體
*
*/
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date creteTime;
// 多對一的復合屬性
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 getCreteTime() {
return creteTime;
}
public void setCreteTime(Date creteTime) {
this.creteTime = creteTime;
}
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
+ ", creteTime=" + creteTime + "]";
}
}
為接口編寫相應的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.wen.seckill.dao.SuccessKilledDao">
<!-- 秒殺成功插入 -->
<insert id="insertSuccessKilled">
<!-- 主鍵沖突報錯 -->
insert ignore into success_killed(seckill_id,user_phone) values(#{seckillId},#{userPhone})
</insert>
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!-- 根據(jù)id 查詢 successkidded 并攜帶Seckill 實體 -->
<!-- 根據(jù) mybatis 將結果映射到SuccessKilled 同時映射 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 as sk
inner join seckill as s on sk.seckill_id=s.seckill_id
where sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}
</select>
</mapper>
5.單元測試
編寫完相應的代碼后自然要編寫單元測試,測試相應的代碼的正確性。
首先編寫一個公用的單元測試類引入相應的測試注解配置
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes=App.class)
@WebAppConfiguration
public class BaseTest {
}
編寫秒殺庫存dao的單元測試給出測試數(shù)據(jù)測試秒殺庫存dao中的三個方法。
public class SeckillDaoTest extends BaseTest {
//注入Dao實現(xiàn)類依賴
@Resource
private SeckillDao seckillDao;
@Test
public void testQueryById() {
long id = 1000;
try {
Seckill seckill = seckillDao.queryById(id);
System.out.println(seckill.getName());
System.out.println(seckill);
}catch(Exception e) {
e.printStackTrace();
}
}
@Test
public void testReduceNumber() throws Exception {
Date killTime = new Date();
int updateCount = seckillDao.reduceNumber(1000L, killTime);
System.out.println("updateCount=" + updateCount);
}
@Test
public void testQueryAll() throws Exception {
List<Seckill> seckills = seckillDao.queryAll();
for (Seckill seckill : seckills) {
System.out.println(seckill);
}
}
}
啟動junit 查看測試結果。
編寫秒殺記錄dao的單元測試給出測試數(shù)據(jù)測試秒殺記錄dao中的二個方法。
public class SuccessKilledDaoTest extends BaseTest {
@Resource
private SuccessKilledDao successKilledDao;
@Test
public void testInsertSuccessKilled() throws Exception {
long id = 1001;
long phone = 13631231234L;
int insertCount = successKilledDao.insertSuccessKilled(id, phone);
System.out.println("insertCount=" + insertCount);
}
@Test
public void testQueryByIdWithSeckill() throws Exception {
long id = 1001;
long phone = 13631231234L;
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone);
System.out.println(successKilled);
System.out.println(successKilled.getSeckill());
}
}
啟動junit 查看測試結果。到此dao 層就算完成了 下一遍將接受service 層實現(xiàn)以及測試。
源碼地址 :https://github.com/haha174/seckill.git
文章地址: http://www.haha174.top/article/details/256198
教程地址:http://www.imooc.com/learn/587