java高併發秒殺項目之Dao

Java高併發秒殺APi之業務分析與DAO層代碼編寫

具體可以參考github

Maven創建項目seckill

mvn archetype:generate -DgroupId=cn.codingxiaxw.seckill -DartifactId=seckill -Dpackage=cn.codingxiaxw.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp

打開WEB-INF下的web.xml,它默認爲我們創建servlet版本爲2.3,需要修改它的根標籤爲:

<web-app xmlns="http://java.sun.com/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         version="3.0"
         metadata-complete="true">
<!--用maven創建的web-app需要修改servlet的版本爲3.0-->


</web-app>

項目架構如下:

在這裏插入圖片描述

pom.xml如下:

<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/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cn.czy.seckill</groupId>
  <artifactId>seckill</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>seckill Maven Webapp</name>
  <url>http://maven.apache.org</url>
  <dependencies>
    <dependency>
      <!--3.0的junit是使用編程的方式來進行測試,而junit4是使用註解的方式來運行junit-->
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>
    <!--補全項目依賴-->
    <!--1.日誌 java日誌有:slf4j,log4j,logback,common-logging
        slf4j:是規範/接口
        日誌實現:log4j,logback,common-logging
        使用:slf4j+logback
    -->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-api</artifactId>
      <version>1.7.12</version>
    </dependency>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-core</artifactId>
      <version>1.1.1</version>
    </dependency>
    <!--實現slf4j接口並整合-->
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>1.1.1</version>
    </dependency>
    <!--1.數據庫相關依賴-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.35</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>c3p0</groupId>
      <artifactId>c3p0</artifactId>
      <version>0.9.1.1</version>
    </dependency>
    <!--2.dao框架:MyBatis依賴-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.3.0</version>
    </dependency>
    <!--mybatis自身實現的spring整合依賴-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.2.3</version>
    </dependency>
      <!--3.Servlet web相關依賴-->
      <dependency>
          <groupId>taglibs</groupId>
          <artifactId>standard</artifactId>
          <version>1.1.2</version>
      </dependency>
      <dependency>
          <groupId>jstl</groupId>
          <artifactId>jstl</artifactId>
          <version>1.2</version>
      </dependency>
      <dependency>
          <groupId>com.fasterxml.jackson.core</groupId>
          <artifactId>jackson-databind</artifactId>
          <version>2.5.4</version>
      </dependency>
      <dependency>
          <groupId>javax.servlet</groupId>
          <artifactId>javax.servlet-api</artifactId>
          <version>3.1.0</version>
      </dependency>
    <!--4:spring依賴-->
    <!--1)spring核心依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-core</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-beans</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <!--2)spring dao層依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-tx</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <!--3)springweb相關依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-web</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
    <!--4)spring test相關依賴-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>4.1.7.RELEASE</version>
    </dependency>
  </dependencies>
  <build>
    <finalName>seckill</finalName>
  </build>
</project>

這樣項目也就基本構建好了。

業務分析

爲什麼我們的系統需要事務?看如下這些故障:1.若是用戶成功秒殺商品我們記錄了其購買明細卻沒有減庫存。導致商品的超賣。2.減了庫存卻沒有記錄用戶的購買明細。導致商品的少賣。對於上述兩個故障,若是沒有事務的支持,損失最大的無疑是我們的用戶和商家。在MySQL中,它內置的事務機制,可以準確的幫我們完成減庫存和記錄用戶購買明細的過程。

MySQL實現秒殺的難點分析:當用戶A秒殺id爲10的商品時,此時MySQL需要進行的操作是:1.開啓事務。2.更新商品的庫存信息。3.添加用戶的購買明細,包括用戶秒殺的商品id以及唯一標識用戶身份的信息如電話號碼等。4.提交事務。若此時有另一個用戶B也在秒殺這件id爲10的商品,他就需要等待,等待到用戶A成功秒殺到這件商品然後MySQL成功的提交了事務他才能拿到這個id爲10的商品的鎖從而進行秒殺,而同一時間是不可能只有用戶B在等待,肯定是有很多很多的用戶都在等待拿到這個行級鎖。秒殺的難點就在這裏,如何高效的處理這些競爭?如何高效的完成事務?在後面第4個模塊如何進行高併發的優化爲大家講解。

實現功能:

1.秒殺接口的暴露。

2.執行秒殺的操作。

3.相關查詢,比如說列表查詢,詳情頁查詢。

建數據庫時的問題

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='秒殺庫存表';

When I try to create the table with the previous query, this is the error I get:

ERROR 1067 (42000): Invalid default value for ‘end_time’

答案:

In MySQL, the TIMESTAMP data type differs in nonstandard ways from other data types:

  • TIMESTAMP columns not explicitly declared with the NULL attribute are assigned the NOT NULL attribute. (Columns of other data types, if not explicitly declared as NOT NULL, permit NULL values.) Setting such a column to NULL sets it to the current timestamp.
  • The first TIMESTAMP column in a table, if not declared with the NULL attribute or an explicit DEFAULT or ON UPDATE clause, is automatically assigned the DEFAULT CURRENT_TIMESTAMP and ON UPDATE CURRENT_TIMESTAMP attributes.
  • TIMESTAMP columns following the first one, if not declared with the NULL attribute or an explicit DEFAULT clause, are automatically assigned DEFAULT ‘0000-00-00 00:00:00’ (the “zero” timestamp). For inserted rows that specify no explicit value for such a column, the column is assigned ‘0000-00-00 00:00:00’ and no warning occurs.

Those nonstandard behaviors remain the default for TIMESTAMP but as of MySQL 5.6.6 are deprecated and this warning appears at startup:

[Warning] TIMESTAMP with implicit DEFAULT value is deprecated.
Please use --explicit_defaults_for_timestamp server option 
(see documentation for more details).

在mysql高版本會出現這樣的問題,在命令行加上這句話就可以了

set @@session.explicit_defaults_for_timestamp=on;

建完該表後,就set @@session.explicit_defaults_for_timestamp=off;再建下一個表就好了。

在MySQL 5.7版本之前,且在MySQL 5.6.6版本之後(explicit_defaults_for_timestamp參數在MySQL 5.6.6開始加入)的版本中,如果沒有設置explicit_defaults_for_timestamp=1的情況下:

1)在默認情況下,如果TIMESTAMP列沒有顯示的指明null屬性,那麼該列會被自動加上not null屬性(而其他類型的列如果沒有被顯示的指定not null,那麼是允許null值的),如果往這個列中插入null值,會自動的設置該列的值爲current timestamp值。

2)表中的第一個TIMESTAMP列,如果沒有指定null屬性或者沒有指定默認值,也沒有指定ON UPDATE語句。那麼該列會自動被加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP屬性。

3)第一個TIMESTAMP列之後的其他的TIMESTAMP類型的列,如果沒有指定null屬性,也沒有指定默認值,那麼該列會被自動加上DEFAULT ‘0000-00-00 00:00:00’屬性。如果insert語句中沒有爲該列指定值,那麼該列中插入’0000-00-00 00:00:00’,並且沒有warning。

如果我們在啓動的時候在配置文件中指定了explicit_defaults_for_timestamp=1,MySQL會按照如下的方式處理TIMESTAMP列:

1)此時如果TIMESTAMP列沒有顯示的指定not null屬性,那麼默認的該列可以爲null,此時向該列中插入null值時,會直接記錄null,而不是current timestamp。

2)不會自動的爲表中的第一個TIMESTAMP列加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP屬性,除非你在建表的時候顯示的指明。

3)如果TIMESTAMP列被加上了not null屬性,並且沒有指定默認值。這時如果向表中插入記錄,但是沒有給該TIMESTAMP列指定值的時候,如果strict sql_mode被指定了,那麼會直接報錯。如果strict sql_mode沒有被指定,那麼會向該列中插入’0000-00-00 00:00:00’並且產生一個warning。

Dao層設計開發

創建數據庫

-- 數據庫初始化腳本
-- 創建數據庫
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,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('800元秒殺ipad',200,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('6600元秒殺mac book pro',300,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
  ('7000元秒殺iMac',400,'2018-05-01 00:00:00','2018-05-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='秒殺成功明細表';

--連接數據庫控制檯
mysql -uroot -p
-- SHOW CREATE TABLE seckill;#顯示錶的創建信息

創建實體類

package org.seckill.entity;

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 +
                '}';
    }
}
package org.seckill.entity;

import java.util.Date;

public class SuccessKilled {
    private long seckillId;

    private long userPhone;

    private short state;

    private Date createTime;

    private Seckill seckill;

    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 long getSeckillId() {
        return seckillId;
    }

    public void setSeckill(Seckill seckill) {
        this.seckill = seckill;
    }

    @Override
    public String toString() {
        return "SuccessKilled{" +
                "seckillId=" + seckillId +
                ", userPhone=" + userPhone +
                ", state=" + state +
                ", createTime=" + createTime +
                '}';
    }
}

創建dao層的接口

package org.seckill.dao;

import org.seckill.entity.Seckill;

import java.util.Date;
import java.util.List;

public interface SeckillDao {
    /**
     * 減庫存
     * @param seckillId
     * @param killTime
     * @return 如果影響行數>1,表示更新庫存的記錄行數
     */
    int reduceNumber(long seckillId, Date killTime);

    /**
     * 根據id查詢秒殺的商品信息
     * @param seckillId
     * @return
     */
    Seckill queryById(long seckillId);

    /**
     * 根據偏移量查詢秒殺商品列表
     * @param off
     * @param limit
     * @return
     */
    List<Seckill> queryAll(int off, int limit);
}
package org.seckill.dao;

import org.seckill.entity.SuccessKilled;

public interface SuccessKilledDao {
    /**
     * 插入購買明細,可過濾重複
     * @param seckillId
     * @param userPhone
     * @return插入的行數
     */
    int insertSuccessKilled(long seckillId,long userPhone);


    /**
     * 根據秒殺商品的id查詢明細SuccessKilled對象(該對象攜帶了Seckill秒殺產品對象)
     * @param seckillId
     * @return
     */
    SuccessKilled queryByIdWithSeckill(long seckillId, long userPhone);
}

配置mybatis

在resources包下創建MyBatis全局配置文件mybatis-config.xml文件

<?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>

創建mapperxml映射文件

在resources下創建mapper包,在包下創建對應Dao接口的xml映射文件。SeckillDao.xml和SuccessKilledDao.xml

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SeckillDao">
    <!--目的:爲dao接口方法提供sql語句配置
    即針對dao接口中的方法編寫我們的sql語句-->
    <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>

</mapper>

spring配置文件

<?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"
       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.配置數據庫相關參數-->
    <context:property-placeholder location="classpath:jdbc.properties"/>

    <!--2.數據庫連接池-->
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <!--配置連接池屬性-->
        <property name="driverClass" value="${jdbc.driver}" />

        <!-- 基本屬性 url、user、password -->
        <property name="jdbcUrl" value="${jdbc.url}" />
        <property name="user" value="${jdbc.username}" />
        <property name="password" value="${jdbc.password}" />

        <!--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">
        <!--往下才是mybatis和spring真正整合的配置-->
        <!--注入數據庫連接池-->
        <property name="dataSource" ref="dataSource"/>
        <!--配置mybatis全局配置文件:mybatis-config.xml-->
        <property name="configLocation" value="classpath:mybatis-config.xml"/>
        <!--掃描entity包,使用別名,多個用;隔開-->
        <property name="typeAliasesPackage" value="org.seckill.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="org.seckill.dao"/>
    </bean>
</beans>

需要我們在resources包下創建jdbc.properties用於配置數據庫的連接信息,內容如下:

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=newpass

利用junit4單元測試進行測試

首先測試SeckillDao.java,利用IDEA快捷鍵shift+command+T對SeckillDao.java進行測試,然後IDEA會自動在test包的java包下爲我們生成對SeckillDao.java中所有方法的測試類SeckillDaoTest.java,內容如下:

public class SeckillDaoTest {
    @Test
    public void reduceNumber() throws Exception {

    }

    @Test
    public void queryById() throws Exception {

    }

    @Test
    public void queryAll() throws Exception {

    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章