美文网首页代码改变世界学习
JAVA高并发秒杀API项目的学习笔记

JAVA高并发秒杀API项目的学习笔记

作者: 叶子的翅膀 | 来源:发表于2016-07-03 23:46 被阅读10073次

    一步一步的搭建JAVA WEB项目,采用Maven构建,基于MYBatis+Spring+Spring MVC+Bootstrap技术的秒杀项目
    学习的视频:http://www.imooc.com/learn/587


    创建Maven项目

    • 创建目录,执行Maven命令
     mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=local
    

    问题:Maven命令执行到Generating Project in Batch mode 卡住,参考链接

    • 将项目导入到IDEA工具中
    • 修改项目配置
      1. 修改web.xml中的servlet版本,默认是2.3,其不支持JSP的EL表达式。从Tomcat中的示例的web.xml中拷贝3.0的版本配置到项目中
      2. 补全目录。项目的main目录下创建java目录,在src目录下创建test目录,test目录下创建java和sources目录
      3. 打开pom.xml,进行依赖的配置
        • 单元测试依赖:Junit4
        • 日志依赖:slf4j+logback。(lf4j是规范/接口,log4j,common-logging,logback是日志的实现)
        • 数据库依赖:mysql-connector-java、c3p0
        • DAO框架:mybatis依赖:mybatis
        • Servlet web相关依赖:standard、jstl、jackson-databind、servlet-api
        • Spring依赖:spring-core、spring-beans、spring-context、spring-jdbc、spring-tx、spring-web、spring-webmvc、spring-test
          <dependencies>
            <!---3.0使用编程方式,4.0使用注解方式-->
            <dependency>
              <groupId>junit</groupId>
              <artifactId>junit</artifactId>
              <version>4.11</version>
              <scope>test</scope>
            </dependency>
      
             <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>
              <!--数据库相关依赖-->
              <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.2</version>
              </dependency>
              <!--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>
              <!--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>
      
              <!--Spring依赖-->
              <!--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>
              <!--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>
              <!--spring的WEB层依赖-->
              <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>
              <!--spring的单元测试依赖-->
              <dependency>
                  <groupId>org.springframework</groupId>
                  <artifactId>spring-test</artifactId>
                  <version>4.1.7.RELEASE</version>
              </dependency>
          </dependencies>
      

    数据库的设计

    • 在项目main目录下创建sql目录,新建 schema.sql,作为数据库的创建脚本
    • 脚本代码如下:
          -- 数据库初始化脚本
      
          -- 创建数据库
          CREATE DATABASE seckill;
      
          -- 使用数据库
          use seckill;
      
          -- 创建秒杀库存表:使用InnoDB引擎,其支持事务。主键自增设置为从1000开始,字符格式设置为UTF8
          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='秒杀库存表';
      
          -- 秒杀成功明细表
          CREATE TABLE success_killed(
            seckill_id bigint NOT NULL COMMENT '秒杀商品id',
            user_phone int NOT NULL COMMENT '用户手机号',
            state tinyint NOT NULL  COMMENT '状态标示:-1指无效,0指成功,1指已付款',
            create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
            PRIMARY KEY (seckill_id,user_phone),
            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-06-28 00:00:00','2016-06-29 00:00:00'),
          ('500元秒杀iphone5',200,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
          ('200元秒杀小米4',300,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
          ('100元秒杀红米note',400,'2016-06-28 00:00:00','2016-06-29 00:00:00');
      
      
          -- show create table seckill;
          -- 为什么手写DDL,记录每次上线的DDL修改
      

    DAO实体和接口

    • 创建实体包org.seckill.entity

    • 创建DAO包org.seckill.dao

    • 创建SecKill实体类,生成getter和setter,重写toString

          private long secKillId; 
          private String name; 
          private int number; 
          private Date startTime; 
          private Date endTime; 
          private Date createTime; 
      
    • 创建SuccessKilled实体类,生成getter和setter,重写toString

          private long secKillId; 
          private long userPhone; 
          private short state; 
          private Date createTime; 
      
    • 创建DAO接口SecKillDao,添加减库存,根据ID查询秒杀对象,查询秒杀商品列表方法

          /**
           * 减库存
           * @param secKillId
           * @param killTime
           * @return如果影响行数大于1,表示更新的记录行数
           */
          int reduceNumber(long secKillId,Date killTime);
      
          /**
           * 根据id查询秒杀对象
           * @param secKillId
           * @return
           */
          SecKill queryById(long secKillId);
      
          /**
           * 根据偏移量查询秒杀商品列表
           * @param offset
           * @param limit
           * @return
           */
          List<SecKill> queryAll(int offset,int limit);
      
    • 创建DAO接口SuccessKilledDao,添加插入购买明细,根据ID查询购买明细实体的方法

          /**
           * 插入购买明细,可过滤重复
           * @param secKillId
           * @param userPhone
           * @return插入的行数
           */
          int inertSuccessKilled(long secKillId,long userPhone);
      
          /**
           *根据ID查询SuccessKilled并携带秒杀产品对象实体
           * @param secKillId
           * @return
           */
          SuccessKilled queryByIdWithSecKill(long secKillId);
      
    • 基于MyBaits实现DAO接口

      1. 创建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的getGeneratedKeys获取数据库自增主键值-->
               <setting name="useGenerateKeys" value="true"/>
      
               <!--使用列别名替换列名 默认为true-->
               <setting name="useColumnLabel" value="true"/>
      
               <!--开启驼峰命名转换-->
               <setting name="mapUnderscoreCamelCase" value="true"
           </settings>
       </configuration>
      
      1. 创建mapper文件夹,用于存储mybatis映射文件
      2. 创建SecKilledDao.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="org.seckill.dao.SecKillDao">
            <!--为DAO接口方法提供sql语句配置-->
           <update id="reduceNumber">
               <!--具体的sql-->
               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 seckill_id,name,number,start_time,end_time,create_time
             from seckill
             where seckill_id = #{secKillId}
           </select>
      
           <select id="queryAll" resultType="SecKill">
             select seckill_id,name,number,start_time,end_time,create_time
             from seckill
             order by create_time desc
             limit #{offset},#{limit}
           </select>
       </mapper>
      
      1. 创建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="org.seckill.dao.SuccessKilledDao">
          <!--为DAO接口方法提供sql语句配置-->
          <insert id="insertSuccessKilled">
             <!--ignore忽略主键冲突-->
             insert ignore into success_killed(seckill_id,user_phone)
             values (#{secKilled},#{userPhone})
          </insert>
      
          <select id="queryByIdWithSecKill" resultType="SuccessKilled">
            <!--根据id查询seccessKilled并携带seckill实体 如何告诉mybatis把结果映射到successkilled同时映射到seckill属性 mybatis可以自由控制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.crate_time"
            from success_killed sk
            inner join seckill s on sk.seckill_id = s.seckill_id
            where sk.seckill_id = #{secKillId}
          </select>
      </mapper> 
      
    • mybatis整合spring

      1. 创建spring文件,用于存储spring配置文件
      2. 创建spring-dao.xml配置文件
      3. 创建jdbc.properties配置文件,用于存储数据库相关信息
      ``` 
        driver=com.mysql.jdbc.Driver
        url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf-8
        username=root
        password=purple
      ```
      
      1. 在spring-dao.xml配置文件中进行四个步骤的配置
        • 配置数据库相关参数
        • 配置数据库连接池
        • 配置sqlSessionFactory对象
        • 配置扫描dao接口包,动态实现 dao接口,并注入到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-3.1.xsd">
                 <!--配置整合mybatis过程-->
                 <!--配置数据库相关参数-->
                 <context:property-placeholder location="classpath:jdbc.properties"/>
      
                 <!--数据库连接池-->
                 <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
                     <property name="driverClass" value="${driver" />
                     <property name="jdbcUrl" value="${url}"/>
                     <property name="user" value="${username}"/>
                     <property name="password" value="${password}"/>
      
                     <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>
      
                 <!--配置sqlSessionFactory对象-->
                 <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
                     <!--注入数据库连接池-->
                     <property name="dataSource" value="dataSource"/>
                     <!--配置mybatis全局配置文件-->
                     <property name="configLocation" value="classpath:mybatis-config.xml"/>
                     <!--扫描entity包,使用别名-->
                     <property name="typeAliasesPackage" value="org.seckill.entity"/>
                     <!--扫描sql配置文件-->
                     <property name="mapperLocations" value="mapper/*.xml"/>
                 </bean>
      
                 <!--配置扫描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>
      
    • Junit4与Spring进行整合,进行Junit4单元测试

      1. 创建SecKillDao的单元测试类
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration("classpath:spring/spring-dao.xml")
      public class SecKillDaoTest {
          //注入DAO实现类依赖
          @Resource
          private SecKillDao secKillDao;
      
          @Test
          public void testReduceNumber() throws Exception {
              Date killTime = new Date();
              int result = secKillDao.reduceNumber(1000L,killTime);
              System.out.println(result);
      
          }
      
          @Test
          public void testQueryById() throws Exception {
              long id = 1000;
              SecKill secKill = secKillDao.queryById(id);
              System.out.println(secKill.getName());
          }
      
          @Test
          public void testQueryAll() throws Exception {
              List<SecKill> secKillList = secKillDao.queryAll(0,1000);
      
              for(SecKill row : secKillList){
                  System.out.println(row.toString());
              }
          }
      }
      
      1. 创建SuccessKilledDao的单元测试类
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration("classpath:spring/spring-dao.xml")
      public class SuccessKilledDaoTest {
          @Resource
          private SuccessKilledDao successKilledDao;
      
          @Test
          public void testInertSuccessKilled() throws Exception {
              int result = successKilledDao.insertSuccessKilled(1000L,28059830451L);
              System.out.println(result);
      
          }
      
          @Test
          public void testQueryByIdWithSecKill() throws Exception {
              SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(1000L,2147483647L);
              System.out.println(successKilled.toString());
          }
      } 
      
      1. 学习点
        • 单元测试类可以利用IDEA的快捷键,直接在要测试的类中进行代码的生成
        • mybatis的传参,需要在DAO接口方法的形参中使用@Param注解进行指明

    业务层设计

    • 秒杀业务接口设计
      1. 创建业务包service

      2. 创建数据传输实体包dto

      3. 创建异常包exception

      4. 创建dto实体

        • 创建暴露秒杀地址DTO:Exposer
        public class Exposer {
            /**
             * 是否开启秒杀
             */
            private boolean exposed;
        
            /**
             * 秒杀ID
             */
            private long secKillId;
        
            /**
             * 一种加密措施
             */
            private String md5;
        
            /**
             *系统当前时间(毫秒值)
             */
            private long now;
        
            private long start;
        
            private long end;
        
            public Exposer(boolean exposed, String md5, long secKillId) {
                this.exposed = exposed;
                this.md5 = md5;
                this.secKillId = secKillId;
            }
        
            public Exposer(boolean exposed, long now, long start, long end) {
                this.exposed = exposed;
                this.now = now;
                this.start = start;
                this.end = end;
            }
        
            public Exposer(boolean exposed, long secKillId) {
                this.exposed = exposed;
                this.secKillId = secKillId;
            }
        
            public boolean isExposed() {
                return exposed;
            }
        
            public void setExposed(boolean exposed) {
                this.exposed = exposed;
            }
        
            public long getSecKillId() {
                return secKillId;
            }
        
            public void setSecKillId(long secKillId) {
                this.secKillId = secKillId;
                this.secKillId = secKillId;
            }
        
            public String getMd5() {
                return md5;
            }
        
            public void setMd5(String md5) {
                this.md5 = md5;
            }
        
            public long getNow() {
                return now;
            }
        
            public void setNow(long now) {
                this.now = now;
            }
        
            public long getStart() {
                return start;
            }
        
            public void setStart(long start) {
                this.start = start;
            }
        
            public long getEnd() {
                return end;
            }
        
            public void setEnd(long end) {
                this.end = end;
            }
        }
        
        
        • 创建封装秒杀执行后结果DTO:SecKillExecution
        public class SecKillExecution {
        
            private long secKillId;
        
            /**
             * 秒杀执行结果状态
             */
            private int state;
        
            /**
             * 状态表示
             */
            private String stateInfo;
        
            private SuccessKilled successKilled;
        
            public SecKillExecution(long secKillId, int state, String stateInfo, SuccessKilled successKilled) {
                this.secKillId = secKillId;
                this.state = state;
                this.stateInfo = stateInfo;
                this.successKilled = successKilled;
            }
        
            public SecKillExecution(long secKillId, int state, String stateInfo) {
                this.secKillId = secKillId;
                this.state = state;
                this.stateInfo = stateInfo;
            }
        
            public long getSecKillId() {
                return secKillId;
            }
        
            public void setSecKillId(long secKillId) {
                this.secKillId = secKillId;
            }
        
            public int getState() {
                return state;
            }
        
            public void setState(int state) {
                this.state = state;
            }
        
            public String getStateInfo() {
                return stateInfo;
            }
        
            public void setStateInfo(String stateInfo) {
                this.stateInfo = stateInfo;
            }
        
            public SuccessKilled getSuccessKilled() {
                return successKilled;
            }
        
            public void setSuccessKilled(SuccessKilled successKilled) {
                this.successKilled = successKilled;
            }
        }
        
      5. 创建异常类

        • 创建业务相关异常:SecKillException
        public class SecKillException extends RuntimeException{
            public SecKillException(String message) {
                super(message);
            }
        
            public SecKillException(String message, Throwable cause) {
                super(message, cause);
            }
        }
        
        • 创建重复秒杀异常类:RepeatKillException
        public class RepeatKillException extends SecKillException{
            public RepeatKillException(String message, Throwable cause) {
                super(message, cause);
            }
        
            public RepeatKillException(String message) {
                super(message);
            }
        }
        
        • 创建秒杀关闭异常类:SecKillCloseExce
          ption
        public class SecKillCloseException extends SecKillException{
            public SecKillCloseException(String message) {
                super(message);
            }
        
            public SecKillCloseException(String message, Throwable cause) {
                super(message, cause);
            }
        }
        
        
      6. 创建SecKillService业务接口:SecKillService

        • 创建查询所有的秒杀记录方法:getSecKillList
        • 创建查询单个秒杀记录方法:getById
        • 创建秒杀开启时输出秒杀接口地址方法:exportSecKillUrl
        • 创建执行秒杀操作方法:executeSecKill
         public interface SecKillService {
             /**
              * 查询所有的秒杀记录
              * @return
              */
              List<SecKill> getSecKillList();
        
             /**
              * 查询单个秒杀记录
              * @param secKillId
              * @return
              */
             SecKill getById(long secKillId);
        
             /**
              * 秒杀开启时输出秒杀接口地址
              * 否则输出系统时间和秒杀时间
              * 防止用户猜测出秒杀地址的规律
              * @param secKillId
              */
             Exposer exportSecKillUrl(long secKillId);
        
             /**
              *执行秒杀操作
              * @param secKillId
              * @param userPhone
              * @param md5
              */
             SecKillExecution executeSecKill(long secKillId,long userPhone,String md5) throws SecKillException,RepeatKillException,SecKillCloseException;
         }
        
      7. 业务接口设计的学习点

        • 站在使用者的角度进行设计接口,不要冗余设计
        • 方法定义粒度,目的明确。非常友好的让使用者调用接口
        • 参数要简炼
        • 返回类型要清晰
    • 秒杀业务接口实现

      1. 新建enums枚举包,将数据字典放到枚举中
      2. 在枚举包下创建秒杀状态枚举:SecKillStatEnum
      public enum SecKillStatEnum {
          SUCCESS(1,"秒杀成功"),
          END(0,"秒杀结束"),
          REPEAT(-1,"重复秒杀"),
          INNER_ERROR(-2,"系统异常"),
          DATA_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;
      
          }
      }
      
      1. 在service包下新建impl包
      2. 创建SecKillServiceImpl实现类,实现SecKillService接口方法
       public class SecKillServiceImpl implements SecKillService{
           private Logger logger = LoggerFactory.getLogger(SecKillService.class);
      
           private SecKillDao secKillDao;
           private SuccessKilledDao successKilledDao;
      
           //混淆字符,用于混淆MD5
           private final String salt = "sdlkjs#$#$dfowierlkjafdmv232k3j@@##$";
      
           @Override
           public List<SecKill> getSecKillList() {
               return secKillDao.queryAll(0,4);
           }
      
           @Override
           public SecKill getById(long secKillId) {
               return secKillDao.queryById(secKillId);
           }
      
           @Override
           public Exposer exportSecKillUrl(long secKillId) {
               SecKill secKill = secKillDao.queryById(secKillId);
      
               if(null == secKill){
                   return  new Exposer(false,secKillId);
               }
      
               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());
               }
      
               //转化特定字符串的过程,不可逆
               String md5 = getMD5(secKillId);
      
               return new Exposer(true,md5,secKillId);
      
           }
      
           @Override
           public SecKillExecution executeSecKill(long secKillId, long userPhone, String md5)
                   throws SecKillException, RepeatKillException, SecKillCloseException {
               if(null == md5 || md5.equals(getMD5(secKillId))){
                   throw new SecKillException("seckill datarewirte");
               }
      
              try{
                  //执行秒杀逻辑,减库存,记录购买行为
                  Date nowTime = new Date();
                  //减库存
                  int updateCount = secKillDao.reduceNumber(secKillId,nowTime);
      
                  if(updateCount <= 0){
                      //没有更新到记录,秒杀结束
                      throw new SecKillCloseException("seckill is Closed");
                  }else{
                      //记录购买行为
                      int insertCount = successKilledDao.insertSuccessKilled(secKillId,userPhone);
      
                      //唯一:secKillId,userPhone
                      if(insertCount <= 0){
                          //重复秒杀
                          throw new RepeatKillException("seckill repeated");
                      }else{
                          //秒杀成功
                          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());
              }
           }
      
           /**
            * 生成MD5
            * @param secKillId
            * @return
            */
           private String getMD5(long secKillId){
               String base = secKillId + "/" + salt;
               String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
               return md5;
           }
       }
      
    • 基于Spring托管Service实现类

      1. 创建Spring的service配置spring-service.xml,进行service包下的注解类型的扫描配置
      <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-3.1.xsd">
             <!--扫描service包下所有注解的类型-->
             <context:component-scan base-package="org.seckill.service"/>
      
      </beans> 
      
      1. 在service实现类中添加上@Service的注解,在类中的dao对象添加上@Autowired的注解
    • 配置并使用Spring声明式事务

      1. 在spring-service.xml中添加上配置事务管理器
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <!--注入数据库连接池-->
           <property name="dataSource" ref="dataSource"/>
      </bean>
      
      1. 在spring-service.xml中添加上配置基于注解的声明式事务
      <tx:annotation-driven transaction-manager="transactionManager"/> 
      
      1. 在业务类的executeSecKill方法中添加上@Transactional事务注解
      2. 学习点:使用注解控制事务方法的优点
      • 开发团队达到一致约定,明确标注事务方法的编程风格
      • 保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求,或者剥离到事务方法外部
      • 不是所有的方法都需要事务,如只有一条修改操作,只读操作就不需要事务控制
    • Service集成测试

      1. 添加上logback的日志配置文件logback.xml
       <?xml version="1.0" encoding="UTF-8"?>
      
       <configuration debug="true">
           <!-- ch.qos.logback.core.ConsoleAppender 控制台输出 -->
           <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
               <encoder>
                   <pattern>[%-5level] %d{HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n</pattern>
               </encoder>
      
           </appender>
      
           <!-- 日志级别 -->
           <root>
               <level value="debug" />
               <appender-ref ref="STDOUT" />
           </root>
      
       </configuration>   
      
      1. 使用IDEA为SecKillService业务接口创建单元测试类SecKillServiceTest
      2. 编写单元测试方法
      @RunWith(SpringJUnit4ClassRunner.class)
      @ContextConfiguration({"classpath:spring/spring-dao.xml","classpath:spring/spring-service.xml"})
      public class SecKillServiceTest {
          private final Logger logger = LoggerFactory.getLogger(this.getClass());
          @Autowired
          private SecKillService secKillService;
          @Test
          public void testGetSecKillList() throws Exception {
              List<SecKill> list = secKillService.getSecKillList();
              logger.info("list={}",list);
          }
      
          @Test
          public void testGetById() throws Exception {
              SecKill secKill = secKillService.getById(1000L);
              logger.info("secKill:{}",secKill);
      
          }
      
          /**
           * 测试完整业务,注意集成测试代码完整逻辑,注意可重复执行
           * @throws Exception
           */
          @Test
          public void testSecKillLogic() throws Exception {
              long id = 1000L;
              Exposer exposer = secKillService.exportSecKillUrl(id);
      
              if(exposer.isExposed()){
                  logger.info("exposer={}",exposer);
                  long phone = 18059830432L;
                  SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,exposer.getMd5());
                  logger.info("secKillExecution:{}",secKillExecution);
              }else{
                  //秒杀未开始
                  logger.warn("exposer={}",exposer);
              }
          }
      
          @Test
          public void testExportSecKillUrl() throws Exception {
              long id = 1000L;
              Exposer exposer = secKillService.exportSecKillUrl(id);
              logger.info("exposer={}",exposer);
      
          }
      
          @Test
          public void testExecuteSecKill() throws Exception {
              long id = 1000L;
              long phone = 18059830452L;
              String md5 = "f1974250b060f51c4a8e48df67232d53";
      
              SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,md5);
      
              logger.info("secKillExecution:{}",secKillExecution);
      
          }
      } 
      
      1. 单元测试的学习点
        • 集成测试的业务逻辑的完整性
        • 注意测试的可重复执行

    WEB层设计

    • 设计Restful接口

    • SpringMVC整合Spring

      1. 在web.xml中配置DispatcherServlet
      2. 创建web包
      3. 创建spring-web.xml配置文件
      4. 在spring-web.xml进行SpringMVC的配置
        • 开启SpringMVC注解模式
        • servlet-mapping映射路径
        • 配置jsp显示viewResolver
        • 扫描web相关的bean
        <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:conext="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-->
               <!--开启SpringMVC注解模式-->
               <!--简化配置
                1、自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
                2、提供一系列功能:数据绑定,数字和日期的转化@NumberFormat,@DataTimeFormat
                    xml,json默认读写支持
               -->
                <mvc:annotation-driven/>
        
                <!--servlet-mapping映射路径-->
                <!--静态资源默认servlet配置
                    1、加入对静态资源的处理:js,css,img
                    2、允许使用/做整体映射
                -->
                <mvc:default-servlet-handler/>
        
                <!--配置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>
        
                <!--扫描web相关的bean-->
                <conext:component-scan base-package="org.seckill.web"/>
        
        </beans> 
        
    • 实现秒杀相关的Restful接口

      1. 创建控制类SecKillController,实现获取列表,获取单条数据,获取系统时间,获取秒杀地址,秒杀的方法
       @Controller
       @RequestMapping("/seckill/")//模块/资源
       public class SecKillController {
           private final Logger logger = LoggerFactory.getLogger(this.getClass());
           @Autowired
           private SecKillService secKillService;
      
          @RequestMapping(name="/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 "redirect:/seckill/list";
               }
      
               model.addAttribute("secKill",secKill);
               return "detail";
           }
      
           @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 = null;
      
               try{
                   Exposer exposer = secKillService.exportSecKillUrl(secKillId);
                   result = new SecKillResult<Exposer>(true,exposer);
      
               }catch(Exception e){
                   logger.error(e.getMessage(),e);
                   result = new SecKillResult<Exposer>(false,e.getMessage());
               }
      
               return result;
           }
      
           @RequestMapping(value="/{secKillId}/{md5}/execution",
           method = RequestMethod.POST,
           produces = {"application/json;charset=utf-8"})
           public SecKillResult<SecKillExecution> excute(@PathVariable("secKillId") Long secKillId,
                                                         @PathVariable("md5") String md5,
                                                  @CookieValue(value="killPhone",required = false) Long userPhone){
               //springmvc valid
               if(userPhone == null){
                   return new SecKillResult<SecKillExecution>(false,"未注册");
               }
      
               SecKillResult<SecKillExecution> result = null;
      
               try{
                   SecKillExecution secKillExecution = secKillService.executeSecKill(secKillId,userPhone,md5);
                   result = new SecKillResult<SecKillExecution>(true,secKillExecution);
      
               }catch(RepeatKillException e){
                   SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.REPEAT);
                   result = new SecKillResult<SecKillExecution>(false,secKillExecution);
      
               }catch(SecKillCloseException e){
                   SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.END);
                   result = new SecKillResult<SecKillExecution>(false,secKillExecution);
      
               }catch(Exception e){
                   logger.error(e.getMessage(),e);
                   SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.INNER_ERROR);
                   result = new SecKillResult<SecKillExecution>(false,secKillExecution);
               }
      
               return result;
           }
      
           @RequestMapping(value="/time/now",method=RequestMethod.GET)
           public SecKillResult<Long> time(){
               Date now = new Date();
               return new SecKillResult<Long>(true,now.getTime());
      
           }
       }
      
    • 基于Bootstrap开发页面结构
      1. 创建jsp文件夹,创建common/header.jsp,common/tag.jsp,list.jsp,detail.jsp,并引入bootstrap框架,jquery、cookie、countdown插件,可以从百度和bootcss的CDN中引入插件。

      2. 创建js文件seckill.js,进行登录、计时的交互逻辑的编码,并在详细页面中引入

       var seckill = {
           //封装秒杀相关ajax的url
           URL: {
             now: function(){
                 return '/seckill/time/now';
             },
             exposer: function(id){
                 return '/seckill/' + id + '/exposer';
             },
             execution : function(id,md5){
                 return '/seckill/' + id + '/' + md5 + '/execution';
             }
           },
           //处理秒杀逻辑
           handleSecKillKill: 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 killUrl =  seckill.URL.execution(secKillId,exposer.md5);
                           console.log('killUrl:',killUrl);
                           //绑定一次点击事件
                           $('#killBtn').one('click',function(){
                                //执行秒杀请求
                               $(this).addClass('disabled');
                               $.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{
                           //未开启秒杀
                           //重新计算计时逻辑
                           seckill.countdown(secKillId,exposer.now,exposer.start,exposer.end);
                       }
      
                   }else{
                       console.error('result:',result);
                   }
               });
           },
           //计时
           countdown: function(secKillId,nowTime,startTime,endTime){
               var $secKillBox = $('#seckill-box');
      
               if(nowTime > endTime){
                   $secKillBox.html('秒杀结束');
               }else if(nowTime < startTime){
                   $secKillBox.html('秒杀未开始');
                   var killTime = new Date(startTime + 1000);
      
                   $secKillBox.countdown(killTime,function(event){
                       var format = event.strftime('秒杀倒计时:%D天 %H时 %M分 %S秒');
                       $secKillBox.html(format);
                   }).on('finish.countdown',function(){
                       //获取秒杀地址,控制实现逻辑,执行秒杀
                       seckill.handleSecKillKill(secKillId,$secKillBox);
                   });
               }else{
                   //秒杀开始
                   seckill.handleSecKillKill(secKillId,$secKillBox);
               }
      
      
           },
           //验证手机号
           validatePhone: function(phone){
               if(phone && phone.length == 11 && !isNaN(phone)){
                   return true;
               }else{
                   return false;
               }
           },
           //详情页秒杀逻辑
           detail: {
               //详情页初始化
               init: function(params){
                   //用户手机验证和登录,计时交互
                   //规划交互流程
                   //在cookie中查找手机号
                   var killPhone = $.cookie('killPhone'),
                       startTime = params.startTime,
                       endTime = params.endTime,
                       secKillId = params.secKillId;
      
                   //验证手机号
                   if(!seckill.validatePhone(killPhone)){
                       var killPhoneModal = $('#killPhoneModal');
      
                       killPhoneModal.modal({
                           show: true,
                           backdrop: 'static',//禁止位置关闭
                           keyboard: false//关闭键盘事件
                       });
      
                       $('#killPhoneBtn').click(function(){
                          var inputPhone = $('#killPhoneKey').val();
                           if(seckill.validatePhone(inputPhone)){
                               //电话写入cookie
                               $.cookie('killPhone',inputPhone,{expires:7,path: '/seckill'})
                               window.location.reload();
      
                           }else{
                               //正常下会有一个前端字典
                              $('#killPhoneMessage').hide().html('<label class="label label-danger">手机号码错误</label>').show(300);
                           }
                       });
                   }
      
                   //用户已经登录
                   //计时交互
                   $.get(seckill.URL.now(),function(result){
                       if(result && result.success){
                           var nowTime = result.data;
                           seckill.countdown(secKillId,nowTime,startTime,endTime);
      
                       }else{
                           consolw.error('result:',result);
                       }
                   });
               }
      
           }
      
       }
      
      1. 在detail.jsp页面中引入seckill.js文件,并进行初始化
      <script type="text/javascript">
          $(function(){
             //使用EL表达式传入参数
             seckill.detail.init({
               secKillId: ${secKill.secKillId},
               startTime: ${secKill.startTime.time},
               endTime: ${secKill.endTime.time}
             });
          });
      </script> 
      

    相关文章

      网友评论

        本文标题:JAVA高并发秒杀API项目的学习笔记

        本文链接:https://www.haomeiwen.com/subject/owecjttx.html