高并發(fā)秒殺API之業(yè)務分析與DAO

1.秒殺業(yè)務的分析

一般的秒殺系統(tǒng)會存在商家,庫存,用戶三個實體,商家添加調(diào)整庫存,庫存用于發(fā)貨和核賬,庫存用戶秒殺或者預售,用戶的付款,退貨也會影響到庫存集體如下圖:


這里寫圖片描述

也就是秒殺業(yè)務的核心就是庫存的處理。
庫存業(yè)務分析:首先用戶秒殺成功要相應的減去庫存已經(jīng)記錄購買的明細,這兩項操作組成了一個完整的事務。如下圖:


這里寫圖片描述

2.難點分析的分析

主要的難點問題就是競爭多個用戶同時秒殺一種商品。對于mysql 來說競爭反應到背后的技術就是事務和行級鎖。
1.事務工作機制
首先是 開啟事務 start transaction
update 庫存數(shù)量 (競爭出現(xiàn)的地方)
insert 購買明細
commit 事務提交
2 行級鎖

當一個用戶執(zhí)行減庫存的操作時,其他用戶執(zhí)行該項操作時為等待狀態(tài)如下圖
這里寫圖片描述

秒殺的難點在于如何高效的處理競爭具體的解決方法會在單寫一遍博客進行解釋。接下來通過一個項目主要實現(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')
  1. 秒殺成功明細表下面給出建表語句

-- 秒殺成功明細表
-- 用戶登錄認證相關的信息
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  &lt;=#{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

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容