项目作者: July077

项目描述 :
一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API
高级语言: Java
项目地址: git://github.com/July077/seckill.git


seckill

一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API

项目的来源

项目的来源于国内IT公开课平台,质量没的说,很适合学习一些技术的基础,这个项目是由四个系列的课程组成的,流程分为几个流程,很基础地教你接触到一个相对有技术含量的项目

  • Java高并发秒杀API之业务分析与DAO层
  • Java高并发秒杀API之web层
  • Java高并发秒杀API之Service层
  • Java高并发秒杀API之高并发优化

其实这几个流程也就是开发的流程,首先从DAO层开始开发,从后往前开发,开始Coding吧!

项目环境的搭建

  • 操作系统 : Ubuntu 17.04
  • IDE :IntelliJ IDEA 2016.2.5 x64 用Eclipse也一样的,工具时靠人用的
  • JDK : JDK1.8 建议使用JDK1.7以上版本,有许多语法糖用着挺舒服的
  • Web容器 : Tomcat 8.0
  • 数据库 :Mysql-5.6.17-WinX64 实验性的项目用Mysql就足够啦
  • 依赖管理工具 : Maven 管理jar包真的很方便

    这里列出的环境不是必须的,你喜欢用什么就用什么,这里只是给出参考,不过不同的版本可能会引起各种不同的问题就需要我们自己去发现以及排查,在这里使用Maven的话时方便我们管理JAR包,我们不用跑去各种开源框架的官网去下载一个又一个的JAR包,配置好了Maven后添加pom文件坐标就会从中央仓库下载JAR包,如果哪天替换版本也很方便


项目效果图

  • 秒杀商品列表

效果图

  • 秒杀结束提示界面

效果图

  • 开始秒杀提示界面

效果图

  • 重复秒杀提示界面

效果图

  • 秒杀秒杀成功提示界面

效果图


项目的运行

下载

Download Zip或者 git clone

  1. git clone https://github.com/Sunybyjava/seckill.git

导入到IDE

这里因为是使用IDEA创建的项目,所以使用IDEA直接打开是很方便的,提前是你要配置好maven的相关配置,以及项目JDK版本,
JDK版本必须在1.8以上,因为在项目中使用了Java8LocalDateTime以及LocalDate,所以低于这个版本编译会失败的

  • IDEA
    直接在主界面选择Open,然后找到项目所在路径,点击pom.xml打开就可以了
  • Eclipse
    这个项目是基于IDEA创建,我这里把项目转成了Eclipse的项目,如果你使用Eclipse的话也可以直接导入,只是步骤更繁琐一点,Eclipse导入步骤

项目编码

项目总结可能比较的长,密集恐惧症者请按小节进行阅读

这里按照上面几个流程走下去,你要有基本的Maven认识以及Java语法的一些概念,要不然可能不太理解

(一)Java高并发秒杀APi之业务分析与DAO层代码编写

构建项目的基本骨架

  • 首先我们要搭建出一个符合Maven约定的目录来,这里大致有两种方式,第一种:
    1. 第一种使用命令行手动构建一个maven结构的目录,当然我基本不会这样构建
      1. mvn archetype:generate -DgroupId=com.suny.seckill -DartifactId=seckill -Dpackage=com.suny.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp
      这里要注意的是使用archetype:generate进行创建,在Maven老版本中是使用archetype:create,现在这种方法已经被弃用了,所以使用命令行创建的话注意了,稍微解释下这段语句的意思,就是构建一个一个maven-archetype-webapp骨架的Webapp项目,然后groupIdcom.suny.seckill,artifactIdseckill,这里是Maven相关知识,可以按照自己的情况进行修改

2.第二种直接在IDE中进行创建,这里以IDEA为例

  • 点击左上角File>New>Project>Maven
  • 然后在里面勾选Create from archetype,然后再往下拉找到org.apache.cocoon:cocoon-22-archetype-webapp,选中它,注意要先勾选那个选项,否则选择不了,然后点击Next继续
    创建Maven项目
    +然后就填写你的Maven的那几个重要的坐标了,自己看着填吧
    填写Maven坐标
    +再就配置你的Maven的相关信息,默认应该是配置好的
    填写Maven在你本机的位置
    +之后就是点Finsh,到此不出意外的话就应该创建成功了

构建pom文件

项目基本的骨架我们就创建出来了,接下来我们要添加一些基本的JAR包的依赖,也就是在pom.xml中添加各种开源组件的三坐标了

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4. <groupId>com.suny.seckill</groupId>
  5. <artifactId>seckill</artifactId>
  6. <version>1.0-SNAPSHOT</version>
  7. <name>seckill Maven Webapp</name>
  8. <url>http://maven.apache.org</url>
  9. <dependencies>
  10. <!--junit测试-->
  11. <dependency>
  12. <groupId>junit</groupId>
  13. <artifactId>junit</artifactId>
  14. <version>4.12</version>
  15. <scope>test</scope>
  16. </dependency>
  17. <!--配置日志相关,日志门面使用slf4j,日志的具体实现由logback实现-->
  18. <dependency>
  19. <groupId>ch.qos.logback</groupId>
  20. <artifactId>logback-classic</artifactId>
  21. <version>1.1.7</version>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.slf4j</groupId>
  25. <artifactId>slf4j-api</artifactId>
  26. <version>1.7.21</version>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.apache.logging.log4j</groupId>
  30. <artifactId>log4j-core</artifactId>
  31. <version>2.6.1</version>
  32. </dependency>
  33. <!--数据库相关依赖-->
  34. <!--首先导入连接Mysql数据连接-->
  35. <dependency>
  36. <groupId>mysql</groupId>
  37. <artifactId>mysql-connector-java</artifactId>
  38. <version>5.1.39</version>
  39. </dependency>
  40. <!--导入数据库连接池-->
  41. <dependency>
  42. <groupId>c3p0</groupId>
  43. <artifactId>c3p0</artifactId>
  44. <version>0.9.1.2</version>
  45. </dependency>
  46. <!--导入mybatis依赖-->
  47. <dependency>
  48. <groupId>org.mybatis</groupId>
  49. <artifactId>mybatis</artifactId>
  50. <version>3.4.2</version>
  51. </dependency>
  52. <dependency>
  53. <groupId>org.mybatis</groupId>
  54. <artifactId>mybatis-spring</artifactId>
  55. <version>1.3.1</version>
  56. </dependency>
  57. <!--导入Servlet web相关的依赖-->
  58. <dependency>
  59. <groupId>taglibs</groupId>
  60. <artifactId>standard</artifactId>
  61. <version>1.1.2</version>
  62. </dependency>
  63. <dependency>
  64. <groupId>jstl</groupId>
  65. <artifactId>jstl</artifactId>
  66. <version>1.2</version>
  67. </dependency>
  68. <!--spring默认的json转换-->
  69. <dependency>
  70. <groupId>com.fasterxml.jackson.core</groupId>
  71. <artifactId>jackson-databind</artifactId>
  72. <version>2.8.5</version>
  73. </dependency>
  74. <dependency>
  75. <groupId>javax.servlet</groupId>
  76. <artifactId>javax.servlet-api</artifactId>
  77. <version>3.1.0</version>
  78. </dependency>
  79. <!--导入spring相关依赖-->
  80. <dependency>
  81. <groupId>org.springframework</groupId>
  82. <artifactId>spring-core</artifactId>
  83. <version>4.3.6.RELEASE</version>
  84. </dependency>
  85. <dependency>
  86. <groupId>org.springframework</groupId>
  87. <artifactId>spring-beans</artifactId>
  88. <version>4.3.6.RELEASE</version>
  89. </dependency>
  90. <dependency>
  91. <groupId>org.springframework</groupId>
  92. <artifactId>spring-context</artifactId>
  93. <version>4.3.6.RELEASE</version>
  94. </dependency>
  95. <dependency>
  96. <groupId>org.springframework</groupId>
  97. <artifactId>spring-jdbc</artifactId>
  98. <version>4.3.7.RELEASE</version>
  99. </dependency>
  100. <dependency>
  101. <groupId>org.springframework</groupId>
  102. <artifactId>spring-tx</artifactId>
  103. <version>4.3.6.RELEASE</version>
  104. </dependency>
  105. <dependency>
  106. <groupId>org.springframework</groupId>
  107. <artifactId>spring-web</artifactId>
  108. <version>4.3.6.RELEASE</version>
  109. </dependency>
  110. <dependency>
  111. <groupId>org.springframework</groupId>
  112. <artifactId>spring-webmvc</artifactId>
  113. <version>4.3.7.RELEASE</version>
  114. </dependency>
  115. <!--导入springTest-->
  116. <dependency>
  117. <groupId>org.springframework</groupId>
  118. <artifactId>spring-test</artifactId>
  119. <version>4.2.7.RELEASE</version>
  120. </dependency>
  121. </dependencies>
  122. <build>
  123. <finalName>seckill</finalName>
  124. </build>
  125. </project>

建立数据库

在根目录下有一个sql文件夹里面有一个sql数据库脚本,如果你不想自己手写的话就直接导入到你的数据库里面去吧,不过还是建议自己手写一遍加深印象

  1. -- 整个项目的数据库脚本
  2. -- 开始创建一个数据库
  3. CREATE DATABASE seckill;
  4. -- 使用数据库
  5. USE seckill;
  6. -- 创建秒杀库存表
  7. CREATE TABLE seckill(
  8. `seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存ID',
  9. `name` VARCHAR(120) NOT NULL COMMENT '商品名称',
  10. `number` INT NOT NULL COMMENT '库存数量',
  11. `start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '秒杀开启的时间',
  12. `end_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '秒杀结束的时间',
  13. `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '创建的时间',
  14. PRIMARY KEY (seckill_id),
  15. KEY idx_start_time(start_time),
  16. KEY idx_end_time(end_time),
  17. KEY idx_create_time(create_time)
  18. )ENGINE =InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
  19. -- 插入初始化数据
  20. insert into
  21. seckill(name,number,start_time,end_time)
  22. values
  23. ('1000元秒杀iphone6',100,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
  24. ('500元秒杀iPad2',200,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
  25. ('300元秒杀小米4',300,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
  26. ('200元秒杀红米note',400,'2016-5-22 00:00:00','2016-5-23 00:00:00');
  27. -- 秒杀成功明细表
  28. -- 用户登录相关信息
  29. create table success_killed(
  30. `seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
  31. `user_phone` BIGINT NOT NULL COMMENT '用户手机号',
  32. `state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标示:-1无效 0成功 1已付款',
  33. `create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
  34. PRIMARY KEY (seckill_id,user_phone), /*联合主键*/
  35. KEY idx_create_time(create_time)
  36. )ENGINE =InnDB DEFAULT CHARSET =utf8 COMMENT ='秒杀成功明细表'
  • 在建立数据库的,如果按照我这里的数据库脚本建立的话应该是没问题的,但是我按照视频里面的数据库脚本建表的话发生了一个错误
    sql报错
    这个报错看起来比较的诡异,我仔细检查sql也没有错误,它总提示我end_time要有一个默认的值,可我记得我以前就不会这样,然后视频里面也没有执行错误,然后我感觉可能时MySQL版本的差异,我查看了下我数据库版本,在登录Mysql控制台后输入指令,在控制台的我暂时知道的有两种方式:
    1. select version();
    2. select @@version;
    我的输出结果如下:
    Mysql版本
    其实登录进控制台就已经可以看到版本了,我的Mysql是5.7的,以前我用的时5.6的,然后去Google上搜索了下,找到了几个答案,参考链接:

然后网友评论里总结出来的几种解决办法,未经测试!:

  • 下次有问题一定要先看一下评论!!!create不了的同学,可以这样写:

    1. `start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀开始时间',
    2. `end_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀结束时间',
    3. `create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    • 关于timestamp的问题,需要先运行 set explicit_defaults_for_timestamp = 1,否则会报invalid default value错误
    • 还需要注意的是SQL版本的问题会导致视频中seckill表创建会出错。只要将create_time放在start_time和end_time之前是方便的解决方法。

    对比下我修改过后的跟视频里面的sql片段:
    sql对比
    我们可以看到在这三个字段有一个小差别,那就是给start_time,end_time,create_time三个字段都添加一个默认值,然后执行数据库语句就没问题了


这里我们需要修改下web.xml中的servlet版本为3.0

打开WEB-INF下的web.xml,修改为以下代码:

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

修改的原因有以下几点:

  • 高版本的Servlet支持更多的特性,更方便我们的Coding,特别是支持注解这一特性
  • Servlet2.3中新加入了Listener接口的实现,,我们可以使用Listener引入SpringContextLoaderListener

举个栗子:

  • Servlet2.3以前我们这样配置ContextLoaderListener:
    1. <servlet>
    2. <servlet-name>context</servlet-name>
    3. <servlet-class>org.springframework.context.ContextLoaderServlet</servlet-class>
    4. <load-on-startup>1</load-on-startup>
    5. </servlet>
    • Servlet2.3以后可以使用Listener配置,也就是我们项目中使用的方法
      1. <listener>
      2. <listener-class>org.springframework.context.ContextLoaderListener</listener-class>
      3. </listener>
      两种方法的效果都是一样的,主要不要同时使用,否则会报错的

建立实体类

  • 首先建立SuccessKilled 秒杀状态表
    ```java
    package com.suny.entity;

import java.io.Serializable;
import java.time.LocalDateTime;

public class SuccessKilled implements Serializable {
private static final long serialVersionUID = 1834437127882846202L;

  1. private long seckillId;
  2. /* 用户的手机号码*/
  3. private long userPhone;
  4. /* 秒杀的状态*/
  5. private short state;
  6. /* 创建时间*/
  7. private LocalDateTime createTime;
  8. /* 多对一,因为一件商品在库存中肯定有许多,对应的购买信息也有很多*/
  9. private Seckill seckill;
  10. public SuccessKilled() {
  11. }
  12. public SuccessKilled(long seckillId, long userPhone, short state, LocalDateTime createTime, Seckill seckill) {
  13. this.seckillId = seckillId;
  14. this.userPhone = userPhone;
  15. this.state = state;
  16. this.createTime = createTime;
  17. this.seckill = seckill;
  18. }
  19. public long getSeckillId() {
  20. return seckillId;
  21. }
  22. public void setSeckillId(long seckillId) {
  23. this.seckillId = seckillId;
  24. }
  25. public long getUserPhone() {
  26. return userPhone;
  27. }
  28. public void setUserPhone(long userPhone) {
  29. this.userPhone = userPhone;
  30. }
  31. public short getState() {
  32. return state;
  33. }
  34. public void setState(short state) {
  35. this.state = state;
  36. }
  37. public LocalDateTime getCreateTime() {
  38. return createTime;
  39. }
  40. public void setCreateTime(LocalDateTime createTime) {
  41. this.createTime = createTime;
  42. }
  43. public Seckill getSeckill() {
  44. return seckill;
  45. }
  46. public void setSeckill(Seckill seckill) {
  47. this.seckill = seckill;
  48. }
  49. @Override
  50. public String toString() {
  51. return "SuccessKilled{" +
  52. "主键ID=" + seckillId +
  53. ", 手机号码=" + userPhone +
  54. ", 秒杀状态=" + state +
  55. ", 创建时间=" + createTime +
  56. ", 秒杀的商品=" + seckill +
  57. '}';
  58. }

}

  1. - 再建立`Seckill` 秒杀商品信息
  2. ```java
  3. package com.suny.entity;
  4. import java.io.Serializable;
  5. import java.time.LocalDateTime;
  6. public class Seckill implements Serializable {
  7. private static final long serialVersionUID = 2912164127598660137L;
  8. /* 主键ID*/
  9. private long seckillId;
  10. /* 秒杀商品名字 */
  11. private String name;
  12. /* 秒杀的商品编号 */
  13. private int number;
  14. /* 开始秒杀的时间 */
  15. private LocalDateTime startTime;
  16. /* 结束秒杀的时间 */
  17. private LocalDateTime endTime;
  18. /* 创建的时间 */
  19. private LocalDateTime createTIme;
  20. public Seckill() {
  21. }
  22. public Seckill(long seckillId, String name, int number, LocalDateTime startTime, LocalDateTime endTime, LocalDateTime createTIme) {
  23. this.seckillId = seckillId;
  24. this.name = name;
  25. this.number = number;
  26. this.startTime = startTime;
  27. this.endTime = endTime;
  28. this.createTIme = createTIme;
  29. }
  30. public long getSeckillId() {
  31. return seckillId;
  32. }
  33. public void setSeckillId(long seckillId) {
  34. this.seckillId = seckillId;
  35. }
  36. public String getName() {
  37. return name;
  38. }
  39. public void setName(String name) {
  40. this.name = name;
  41. }
  42. public int getNumber() {
  43. return number;
  44. }
  45. public void setNumber(int number) {
  46. this.number = number;
  47. }
  48. public LocalDateTime getStartTime() {
  49. return startTime;
  50. }
  51. public void setStartTime(LocalDateTime startTime) {
  52. this.startTime = startTime;
  53. }
  54. public LocalDateTime getEndTime() {
  55. return endTime;
  56. }
  57. public void setEndTime(LocalDateTime endTime) {
  58. this.endTime = endTime;
  59. }
  60. public LocalDateTime getCreateTIme() {
  61. return createTIme;
  62. }
  63. public void setCreateTIme(LocalDateTime createTIme) {
  64. this.createTIme = createTIme;
  65. }
  66. @Override
  67. public String toString() {
  68. return "com.suny.entity.Seckill{" +
  69. "主键ID=" + seckillId +
  70. ", 秒杀商品='" + name + '\'' +
  71. ", 编号=" + number +
  72. ", 开始秒杀时间=" + startTime +
  73. ", 结束秒杀时间=" + endTime +
  74. ", 创建时间=" + createTIme +
  75. '}';
  76. }
  77. }

对实体类创建对应的mapper接口,也就是dao接口类

  • 首先创建SeckillMapper,在我这里位于com.suny.dao包下
    ```java
    package com.suny.dao;

import com.suny.entity.Seckill;
import org.apache.ibatis.annotations.Param;

import java.time.LocalDateTime;
import java.util.List;

public interface SeckillMapper {
/**

  1. * 根据传过来的<code>seckillId</code>去减少商品的库存.
  2. *
  3. * @param seckillId 秒杀商品ID
  4. * @param killTime 秒杀的精确时间
  5. * @return 如果秒杀成功就返回1,否则就返回0
  6. */
  7. int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") LocalDateTime killTime);
  8. /**
  9. * 根据传过来的<code>seckillId</code>去查询秒杀商品的详情.
  10. *
  11. * @param seckillId 秒杀商品ID
  12. * @return 对应商品ID的的数据
  13. */
  14. Seckill queryById(@Param("seckillId") long seckillId);
  15. /**
  16. * 根据一个偏移量去查询秒杀的商品列表.
  17. *
  18. * @param offset 偏移量
  19. * @param limit 限制查询的数据个数
  20. * @return 符合偏移量查出来的数据个数
  21. */
  22. List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

}

  1. - 再创建`SuccessKilledMapper`
  2. ```java
  3. package com.suny.dao;
  4. import com.suny.entity.SuccessKilled;
  5. import org.apache.ibatis.annotations.Param;
  6. public interface SuccessKilledMapper {
  7. /**
  8. * 插入一条详细的购买信息.
  9. *
  10. * @param seckillId 秒杀商品的ID
  11. * @param userPhone 购买用户的手机号码
  12. * @return 成功插入就返回1, 否则就返回0
  13. */
  14. int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
  15. /**
  16. * 根据秒杀商品的ID查询<code>SuccessKilled</code>的明细信息.
  17. *
  18. * @param seckillId 秒杀商品的ID
  19. * @param userPhone 购买用户的手机号码
  20. * @return 秒杀商品的明细信息
  21. */
  22. SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
  23. }

接下来书写xml配置文件

建立对应的mapper.xml

首先在src/main/resources建立com.suny.dao这个包,也就是对应mapper接口文件包一样的包名,这样符合Maven的约定,就是资源放置在Resource包下,Java包下则是放置java类文件,编译后最后还是会在同一个目录下.
建包

  • 首先建立SeckillMapper.xml
    ```xml
    <!DOCTYPE mapper

    1. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    2. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">


    UPDATE seckill
    SET number = number - 1
    WHERE seckill_id = #{seckillId}
    AND start_time
    <![CDATA[
    <=
    ]]>
    #{killTime}
    AND end_time >= #{killTime}
    AND number > 0
  1. <select id="queryAll" resultType="com.suny.entity.Seckill">
  2. SELECT
  3. *
  4. FROM seckill AS s
  5. ORDER BY create_time DESC
  6. LIMIT #{offset}, #{limit}
  7. </select>

  1. - 建立`SuccessKilledMapper.xml`
  2. ```xml
  3. <!DOCTYPE mapper
  4. PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
  5. "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
  6. <mapper namespace="com.suny.dao.SuccessKilledMapper">
  7. <!--添加主键冲突时忽略错误返回0-->
  8. <insert id="insertSuccessKilled">
  9. INSERT IGNORE INTO success_killed (seckill_id, user_phone, state)
  10. VALUES (#{seckillId}, #{userPhone}, 0)
  11. </insert>
  12. <!--根据seckillId查询SuccessKilled对象,并携带Seckill对象,告诉mybatis把映射结果映射到SuccessKill属性同时映射到Seckill属性-->
  13. <select id="queryByIdWithSeckill" resultType="com.suny.entity.SuccessKilled">
  14. SELECT
  15. sk.seckill_id,
  16. sk.user_phone,
  17. sk.create_time,
  18. sk.state,
  19. s.seckill_id "seckill.seckill_id",
  20. s.name "seckill.name",
  21. s.number "seckill",
  22. s.start_time "seckill.start_time",
  23. s.end_time "seckill.end_time",
  24. s.create_time "seckill.create_time"
  25. FROM success_killed sk
  26. INNER JOIN seckill s ON sk.seckill_id = s.seckill_id
  27. WHERE sk.seckill_id = #{seckillId}
  28. AND sk.user_phone= #{userPhone}
  29. </select>
  30. </mapper>
  • 建立Mybatis的配置文件mybatis-config.xml
    1. <?xml version="1.0" encoding="UTF-8" ?>
    2. <!DOCTYPE configuration PUBLIC
    3. "-//mybatis.org//DTD MyBatis Generator Configuration 3.0//EN"
    4. "http://mybatis.org/dtd/mybatis-3-config.dtd" >
    5. <configuration>
    6. <!--首先配置全局属性-->
    7. <settings>
    8. <!--开启自动填充主键功能,原理时通过jdbc的一个方法getGeneratekeys获取自增主键值-->
    9. <setting name="useGeneratedKeys" value="true"></setting>
    10. <!--使用别名替换列名,默认就是开启的-->
    11. <setting name="useColumnLabel" value="true"></setting>
    12. <!--开启驼峰命名的转换-->
    13. <setting name="mapUnderscoreToCamelCase" value="true"></setting>
    14. </settings>
    15. </configuration>
  • 然后建立连接数据库的配置文件jdbc.properties,这里的属性要根据自己的需要去进行修改,切勿直接复制使用
    1. jdbc.driver=com.mysql.jdbc.Driver
    2. jdbc.user=root
    3. jdbc.password=root
    4. jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
  • 建立Springdao的配置文件,在resources包下创建applicationContext-dao.xml
    ```xml

<?xml version=”1.0” encoding=”UTF-8”?>








































  1. - 基础的部分我们搭建完成了,然后要开始测试了
  2. `IDEA`里面有一个快速建立测试的快捷键`Ctrl+Shift+T`,在某个要测试的类里面按下这个快捷键就会出现`Create new Test`,然后选择你要测试的方法跟测试的工具就可以了,这里我们使用Junit作为测试
  3. + 建立`SeckillMapperTest`文件,代码如下
  4. ```java
  5. package com.suny.dao;
  6. import com.suny.entity.Seckill;
  7. import org.junit.Test;
  8. import org.junit.runner.RunWith;
  9. import org.springframework.test.context.ContextConfiguration;
  10. import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
  11. import javax.annotation.Resource;
  12. import java.time.LocalDateTime;
  13. import java.util.List;
  14. import static org.junit.Assert.*;
  15. @RunWith(SpringJUnit4ClassRunner.class)
  16. @ContextConfiguration({"classpath:spring/applicationContext-dao.xml"})
  17. public class SeckillMapperTest {
  18. @Resource
  19. private SeckillMapper seckillMapper;
  20. @Test
  21. public void reduceNumber() throws Exception {
  22. long seckillId=1000;
  23. LocalDateTime localDateTime=LocalDateTime.now();
  24. int i = seckillMapper.reduceNumber(seckillId, localDateTime);
  25. System.out.println(i);
  26. }
  27. @Test
  28. public void queryById() throws Exception {
  29. long seckillId = 1000;
  30. Seckill seckill = seckillMapper.queryById(seckillId);
  31. System.out.println(seckill.toString());
  32. }
  33. @Test
  34. public void queryAll() throws Exception {
  35. List<Seckill> seckills = seckillMapper.queryAll(0, 100);
  36. for (Seckill seckill : seckills) {
  37. System.out.println(seckill.toString());
  38. }
  39. }
  40. }

测试中可能会出现Mybatis参数绑定失败的错误,在mapper接口中的方法里面添加@Param的注解,显示的告诉mybatis参数的名称是什么,例如

  1. List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

2016-5-23 13:46:28


(二)Java高并发秒杀API之Service层

首先在编写Service层代码前,我们应该首先要知道这一层到底时干什么的,这里摘取来自ITEYE一位博主的原话

Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。

在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以Service层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用DAO层的代码,这里我们的DAo层就是连接数据库的那一层,调用关系可以这样表达:
View(页面)>Controller(控制层)>Service(业务逻辑)>Dao(数据访问)>Database(数据库)

  • 首先还是接口的设计,设计Service秒杀商品的接口 SeckillService
    首先在som.suny包下建立interfaces这个包,这个包里面存放Service相关的接口,然后建立SeckillService接口文件,代码如下:
    ```java

public interface SeckillService {

  1. /**
  2. * 查询全部的秒杀记录.
  3. * @return 数据库中所有的秒杀记录
  4. */
  5. List<Seckill> getSeckillList();
  6. /**
  7. * 查询单个秒杀记录
  8. * @param seckillId 秒杀记录的ID
  9. * @return 根据ID查询出来的记录信息
  10. */
  11. Seckill getById(long seckillId);
  12. /**
  13. * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
  14. * @param seckillId 秒杀商品Id
  15. * @return 根据对应的状态返回对应的状态实体
  16. */
  17. Exposer exportSeckillUrl(long seckillId);
  18. /**
  19. * 执行秒杀操作,有可能是失败的,失败我们就抛出异常
  20. * @param seckillId 秒杀的商品ID
  21. * @param userPhone 手机号码
  22. * @param md5 md5加密值
  23. * @return 根据不同的结果返回不同的实体信息
  24. */
  25. SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;
  1. 建立后接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回`json`数据的话,你是返回什么样的数据好?直接返回一个数字状态码或者时文字?这样设计肯定是不好的,所以我们应该向前段返回一个实体信息`json`,里面包含了一系列的信息,无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是`PO`了,所以我们建立一个`DTO`数据传输类,关于常见的几种对象我的解释如下:
  2. + PO: 也就是我们在为每一张数据库表写一个实体的类
  3. + VO, 对某个页面或者展现层所需要的数据,封装成一个实体类
  4. + BO, 就是业务对象,我也不是很了解
  5. + DTO, VO的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
  6. + POJO, 简单的无规则java对象
  7. `com.suny`下建立`dto`包,然后建立`Exposer`类,这个类是秒杀时数据库那边处理的结果的对象
  8. ```java
  9. public class Exposer {
  10. /*是否开启秒杀 */
  11. private boolean exposed;
  12. /* 对秒杀地址进行加密措施 */
  13. private String md5;
  14. /* id为seckillId的商品秒杀地址 */
  15. private long seckillId;
  16. /* 系统当前的时间 */
  17. private LocalDateTime now;
  18. /* 秒杀开启的时间 */
  19. private LocalDateTime start;
  20. /* 秒杀结束的时间 */
  21. private LocalDateTime end;
  22. public Exposer() {
  23. }
  24. public Exposer(boolean exposed, String md5, long seckillId) {
  25. this.exposed = exposed;
  26. this.md5 = md5;
  27. this.seckillId = seckillId;
  28. }
  29. public Exposer(boolean exposed, long seckillId, LocalDateTime now, LocalDateTime start, LocalDateTime end) {
  30. this.exposed = exposed;
  31. this.seckillId = seckillId;
  32. this.now = now;
  33. this.start = start;
  34. this.end = end;
  35. }
  36. public Exposer(boolean exposed, long seckillId) {
  37. this.exposed = exposed;
  38. this.seckillId = seckillId;
  39. }
  40. public boolean isExposed() {
  41. return exposed;
  42. }
  43. public void setExposed(boolean exposed) {
  44. this.exposed = exposed;
  45. }
  46. public String getMd5() {
  47. return md5;
  48. }
  49. public void setMd5(String md5) {
  50. this.md5 = md5;
  51. }
  52. public long getSeckillId() {
  53. return seckillId;
  54. }
  55. public void setSeckillId(long seckillId) {
  56. this.seckillId = seckillId;
  57. }
  58. public LocalDateTime getNow() {
  59. return now;
  60. }
  61. public void setNow(LocalDateTime now) {
  62. this.now = now;
  63. }
  64. public LocalDateTime getStart() {
  65. return start;
  66. }
  67. public void setStart(LocalDateTime start) {
  68. this.start = start;
  69. }
  70. public LocalDateTime getEnd() {
  71. return end;
  72. }
  73. public void setEnd(LocalDateTime end) {
  74. this.end = end;
  75. }
  76. @Override
  77. public String toString() {
  78. return "Exposer{" +
  79. "秒杀状态=" + exposed +
  80. ", md5加密值='" + md5 + '\'' +
  81. ", 秒杀ID=" + seckillId +
  82. ", 当前时间=" + now +
  83. ", 开始时间=" + start +
  84. ", 结束=" + end +
  85. '}';
  86. }
  87. }

然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.suny.dto包下再建立SeckillExecution用来封装给页面的结果:

  1. public class SeckillExecution {
  2. private long seckillId;
  3. /* 执行秒杀结果的状态 */
  4. private int state;
  5. /* 状态的明文标示 */
  6. private String stateInfo;
  7. /* 当秒杀成功时,需要传递秒杀结果的对象回去 */
  8. private SuccessKilled successKilled;
  9. /* 秒杀成功返回的实体 */
  10. public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {
  11. this.seckillId = seckillId;
  12. this.state = state;
  13. this.stateInfo = stateInfo;
  14. this.successKilled = successKilled;
  15. }
  16. /* 秒杀失败返回的实体 */
  17. public SeckillExecution(long seckillId, int state, String stateInfo) {
  18. this.seckillId = seckillId;
  19. this.state = state;
  20. this.stateInfo = stateInfo;
  21. }
  22. public long getSeckillId() {
  23. return seckillId;
  24. }
  25. public void setSeckillId(long seckillId) {
  26. this.seckillId = seckillId;
  27. }
  28. public int getState() {
  29. return state;
  30. }
  31. public void setState(int state) {
  32. this.state = state;
  33. }
  34. public String getStateInfo() {
  35. return stateInfo;
  36. }
  37. public void setStateInfo(String stateInfo) {
  38. this.stateInfo = stateInfo;
  39. }
  40. public SuccessKilled getSuccessKilled() {
  41. return successKilled;
  42. }
  43. public void setSuccessKilled(SuccessKilled successKilled) {
  44. this.successKilled = successKilled;
  45. }
  46. @Override
  47. public String toString() {
  48. return "SeckillExecution{" +
  49. "秒杀的商品ID=" + seckillId +
  50. ", 秒杀状态=" + state +
  51. ", 秒杀状态信息='" + stateInfo + '\'' +
  52. ", 秒杀的商品=" + successKilled +
  53. '}';
  54. }
  55. }
定义秒杀中可能会出现的异常
  • 定义一个基础的异常,所有的子异常继承这个异常SeckillException

    1. /**
    2. * 秒杀基础异常
    3. * Created by 孙建荣 on 17-5-23.下午8:24
    4. */
    5. public class SeckillException extends RuntimeException {
    6. public SeckillException(String message) {
    7. super(message);
    8. }
    9. public SeckillException(String message, Throwable cause) {
    10. super(message, cause);
    11. }
    12. }
    • 首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常SeckillCloseException,需要继承我们一开始写的基础异常
      ```java
      /**
    • 秒杀已经关闭异常,当秒杀结束就会出现这个异常
    • Created by 孙建荣 on 17-5-23.下午8:27
      */
      public class SeckillCloseException extends SeckillException{
      public SeckillCloseException(String message) {
      super(message);
      }

    public SeckillCloseException(String message, Throwable cause) {

    1. super(message, cause);

    }
    }
    ```

    • 然后还有可能发生重复秒杀异常RepeatKillException
      ```java

/**

  • 重复秒杀异常,不需要我们手动去try catch
  • Created by 孙建荣 on 17-5-23.下午8:26
    */
    public class RepeatKillException extends SeckillException{
    public RepeatKillException(String message) {

    1. super(message);

    }

    public RepeatKillException(String message, Throwable cause) {

    1. super(message, cause);

    }
    }
    ```

    实现Service接口

    ```java
    /**

  • Created by 孙建荣 on 17-5-23.下午9:30
    /
    @Service
    public class SeckillServiceImpl implements SeckillService {
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    /
    加入一个盐值,用于混淆*/
    private final String salt = “thisIsASaltValue”;

    @Autowired
    private SeckillMapper seckillMapper;
    @Autowired
    private SuccessKilledMapper successKilledMapper;

  1. /**
  2. * 查询全部的秒杀记录.
  3. *
  4. * @return 数据库中所有的秒杀记录
  5. */
  6. @Override
  7. public List<Seckill> getSeckillList() {
  8. return seckillMapper.queryAll(0, 4);
  9. }
  10. /**
  11. * 查询单个秒杀记录
  12. *
  13. * @param seckillId 秒杀记录的ID
  14. * @return 根据ID查询出来的记录信息
  15. */
  16. @Override
  17. public Seckill getById(long seckillId) {
  18. return seckillMapper.queryById(seckillId);
  19. }
  20. /**
  21. * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址
  22. *
  23. * @param seckillId 秒杀商品Id
  24. * @return 根据对应的状态返回对应的状态实体
  25. */
  26. @Override
  27. public Exposer exportSeckillUrl(long seckillId) {
  28. // 根据秒杀的ID去查询是否存在这个商品
  29. /* Seckill seckill = seckillMapper.queryById(seckillId);
  30. if (seckill == null) {
  31. logger.warn("查询不到这个秒杀产品的记录");
  32. return new Exposer(false, seckillId);
  33. }*/
  34. Seckill seckill = redisDao.getSeckill(seckillId);
  35. if (seckill == null) {
  36. // 访问数据库读取数据
  37. seckill = seckillMapper.queryById(seckillId);
  38. if (seckill == null) {
  39. return new Exposer(false, seckillId);
  40. } else {
  41. // 放入redis
  42. redisDao.putSeckill(seckill);
  43. }
  44. }
  45. // 判断是否还没到秒杀时间或者是过了秒杀时间
  46. LocalDateTime startTime = seckill.getStartTime();
  47. LocalDateTime endTime = seckill.getEndTime();
  48. LocalDateTime nowTime = LocalDateTime.now();
  49. // 开始时间大于现在的时候说明没有开始秒杀活动 秒杀活动结束时间小于现在的时间说明秒杀已经结束了
  50. /* if (!nowTime.isAfter(startTime)) {
  51. logger.info("现在的时间不在开始时间后面,未开启秒杀");
  52. return new Exposer(false, seckillId, nowTime, startTime, endTime);
  53. }
  54. if (!nowTime.isBefore(endTime)) {
  55. logger.info("现在的时间不在结束的时间之前,可以进行秒杀");
  56. return new Exposer(false, seckillId, nowTime, startTime, endTime);
  57. }*/
  58. if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
  59. //秒杀开启,返回秒杀商品的id,用给接口加密的md5
  60. String md5 = getMd5(seckillId);
  61. return new Exposer(true, md5, seckillId);
  62. }
  63. return new Exposer(false, seckillId, nowTime, startTime, endTime);
  64. }
  65. private String getMd5(long seckillId) {
  66. String base = seckillId + "/" + salt;
  67. return DigestUtils.md5DigestAsHex(base.getBytes());
  68. }
  69. /**
  70. * 执行秒杀操作,失败的,失败我们就抛出异常
  71. *
  72. * @param seckillId 秒杀的商品ID
  73. * @param userPhone 手机号码
  74. * @param md5 md5加密值
  75. * @return 根据不同的结果返回不同的实体信息
  76. */
  77. @Override
  78. public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
  79. if (md5 == null || !md5.equals(getMd5(seckillId))) {
  80. logger.error("秒杀数据被篡改");
  81. throw new SeckillException("seckill data rewrite");
  82. }
  83. // 执行秒杀业务逻辑
  84. LocalDateTime nowTIme = LocalDateTime.now();
  85. try {
  86. //执行减库存操作
  87. int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
  88. if (reduceNumber <= 0) {
  89. logger.warn("没有更新数据库记录,说明秒杀结束");
  90. throw new SeckillCloseException("seckill is closed");
  91. } else {
  92. // 这里至少减少的数量不为0了,秒杀成功了就增加一个秒杀成功详细
  93. int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
  94. // 查看是否被重复插入,即用户是否重复秒杀
  95. if (insertCount <= 0) {
  96. throw new RepeatKillException("seckill repeated");
  97. } else {
  98. // 秒杀成功了,返回那条插入成功秒杀的信息
  99. SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);

// return new SeckillExecution(seckillId,1,”秒杀成功”);
return new SeckillExecution(seckillId,1,”秒杀成功”,successKilled);
}
}
} catch (SeckillCloseException | RepeatKillException e1) {
throw e1;
} catch (Exception e) {
logger.error(e.getMessage(), e);
// 把编译期异常转换为运行时异常
throw new SeckillException(“seckill inner error : “ + e.getMessage());
}
}

  1. 在这里我们捕获了运行时异常,这样做的原因就是`Spring`的事物默认就是发生了`RuntimeException`才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
  2. 然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如`秒杀成功`,`秒杀失败`等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在`con.suny`包下建立`enum`包,专门放置枚举类,然后再建立`SeckillStatEnum`枚举类:
  3. ```java
  4. /**
  5. * 常量枚举类
  6. * Created by 孙建荣 on 17-5-23.下午10:15
  7. */
  8. public enum SeckillStatEnum {
  9. SUCCESS(1, "秒杀成功"),
  10. END(0, "秒杀结束"),
  11. REPEAT_KILL(-1, "重复秒杀"),
  12. INNER_ERROR(-2, "系统异常"),
  13. DATE_REWRITE(-3, "数据篡改");
  14. private int state;
  15. private String info;
  16. SeckillStatEnum() {
  17. }
  18. SeckillStatEnum(int state, String info) {
  19. this.state = state;
  20. this.info = info;
  21. }
  22. public int getState() {
  23. return state;
  24. }
  25. public String getInfo() {
  26. return info;
  27. }
  28. public static SeckillStatEnum stateOf(int index) {
  29. for (SeckillStatEnum statEnum : values()) {
  30. if (statEnum.getState() == index) {
  31. return statEnum;
  32. }
  33. }
  34. return null;
  35. }
  36. }

既然把这些改成了枚举,那么在SeckillServiceImpl类中的executeSeckill方法中成功秒杀的返回值就应该修改为

  1. return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);

改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution类的构造函数,把statestateInfo的值设置从构造函数里面的SeckillStatEnum中取出值来设置:

  1. /* 秒杀成功返回的实体 */
  2. public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
  3. this.seckillId = seckillId;
  4. this.state = statEnum.getState();
  5. this.stateInfo = statEnum.getInfo();
  6. this.successKilled = successKilled;
  7. }
  8. /* 秒杀失败返回的实体 */
  9. public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
  10. this.seckillId = seckillId;
  11. this.state = statEnum.getState();
  12. this.stateInfo = statEnum.getInfo();
  13. }

下一步肯定要注入Service了

首先在resources/spring下建立applicationContext-service.xml文件,用来配置Service层的相关代码:

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <beans xmlns="http://www.springframework.org/schema/beans"
  3. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  4. xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
  5. xsi:schemaLocation="http://www.springframework.org/schema/beans
  6. http://www.springframework.org/schema/beans/spring-beans.xsd
  7. http://www.springframework.org/schema/context
  8. http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
  9. <!--配置自动扫描service包下的注解,在这里配置了自动扫描后,com.suny.service包下所有带有@Service注解的类都会被加入Spring容器中-->
  10. <context:component-scan base-package="com.suny.service"></context:component-scan>
  11. <!--配置事物,这里时使用基于注解的事物-->
  12. <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  13. <!--注入数据库连接池-->
  14. <property name="dataSource" ref="dataSource"></property>
  15. </bean>
  16. <!--开启基于注解的申明式事物-->
  17. <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
  18. </beans>

在这里开启了基于注解的事物,常见的事物操作有以下几种方法

  • 在Spring早期版本中是使用ProxyFactoryBean+XMl方式来配置事物.
  • 在Spring配置文件使用tx:advice+aop命名空间,好处就是一次配置永久生效,你无须去关心中间出的问题,不过出错了你很难找出来在哪里出了问题
  • 注解@Transactional的方式,注解可以在方法定义,接口定义,类定义,public方法上,但是不能注解在private,final,static等方法上,因为Spring的事物管理默认是使用Cglib动态代理的:
    • private方法因为访问权限限制,无法被子类覆盖
    • final方法无法被子类覆盖
    • static时类级别的方法,无法被子类覆盖
    • protected方法可以被子类覆盖,因此可以被动态字节码增强
      不能被Spring AOP事物增强的方法
      | 序号 | 动态代理策略 |不能被事物增强的方法 |
      |:——-:| :——-: |:——-:|
      | 1 |基于接口的动态代理 |出了public以外的所有方法,并且 public static 的方法也不能被增强 |
      | 2 |基于Cglib的动态代理 | private,static,final的方法 |

然后你要在Service类上添加注解@Service,不用在接口上添加注解:

  1. @Service
  2. public class SeckillServiceImpl implements SeckillService

既然已经开启了基于注解的事物,那我们就去需要被事物的方法上加个注解@Transactional吧:

  1. @Transactional
  2. @Override
  3. public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException

Service层的测试

写测试类,我这里的测试类名为SeckillServiceImplTest:

  1. /**
  2. * Created by 孙建荣 on 17-5-23.下午10:30
  3. */
  4. @RunWith(SpringJUnit4ClassRunner.class)
  5. @ContextConfiguration({"classpath:spring/applicationContext-dao.xml", "classpath:spring/applicationContext-service.xml"})
  6. public class SeckillServiceImplTest {
  7. private Logger logger = LoggerFactory.getLogger(this.getClass());
  8. @Autowired
  9. private SeckillService seckillService;
  10. @Test
  11. public void getSeckillList() throws Exception {
  12. List<Seckill> seckillList = seckillService.getSeckillList();
  13. logger.info(seckillList.toString());
  14. System.out.println(seckillList.toString());
  15. }
  16. @Test
  17. public void getById() throws Exception {
  18. long seckillId = 1000;
  19. Seckill byId = seckillService.getById(seckillId);
  20. System.out.println(byId.toString());
  21. }
  22. @Test
  23. public void exportSeckillUrl() throws Exception {
  24. long seckillId = 1000;
  25. Exposer exposer = seckillService.exportSeckillUrl(seckillId);
  26. System.out.println(exposer.toString());
  27. }
  28. @Test
  29. public void executeSeckill() throws Exception {
  30. long seckillId = 1000;
  31. Exposer exposer = seckillService.exportSeckillUrl(seckillId);
  32. if (exposer.isExposed()) {
  33. long userPhone = 12222222222L;
  34. String md5 = "bf204e2683e7452aa7db1a50b5713bae";
  35. try {
  36. SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
  37. System.out.println(seckillExecution.toString());
  38. } catch (SeckillCloseException | RepeatKillException e) {
  39. e.printStackTrace();
  40. }
  41. } else {
  42. System.out.println("秒杀未开启");
  43. }
  44. }
  45. @Test
  46. public void executeSeckillProcedureTest() {
  47. long seckillId = 1001;
  48. long phone = 1368011101;
  49. Exposer exposer = seckillService.exportSeckillUrl(seckillId);
  50. if (exposer.isExposed()) {
  51. String md5 = exposer.getMd5();
  52. SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
  53. System.out.println(execution.getStateInfo());
  54. }
  55. }
  56. }

测试的话如果每个方法测试都通过就说明通过,如果报错了话就仔细看下哪一步错了检查下


(三)Java高并发秒杀系统API之Web层开发

既然是Web层的会肯定要先引入SpringMvc了

  • 修改web.xml,引入SpringMvcDispatcherServlet

    1. <web-app xmlns="http://java.sun.com/xml/ns/javaee"
    2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    4. http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    5. version="3.0"
    6. metadata-complete="true">
    7. <!--用maven创建的web-app需要修改servlet的版本为3.0-->
    8. <servlet>
    9. <servlet-name>seckill-dispatchServlet</servlet-name>
    10. <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    11. <!--配置springmvc的配置文件-->
    12. <init-param>
    13. <param-name>contextConfigLocation</param-name>
    14. <param-value>classpath:spring/applicationContext-*.xml</param-value>
    15. </init-param>
    16. <load-on-startup>
    17. 1
    18. </load-on-startup>
    19. </servlet>
    20. <servlet-mapping>
    21. <servlet-name>seckill-dispatchServlet</servlet-name>
    22. <!--直接拦截所有请求,不再采用spring2.0的/*或者*.do方式-->
    23. <url-pattern>/</url-pattern>
    24. </servlet-mapping>
    25. </web-app>

    在这里的话如果你不配置这一段代码的:

    1. <!--配置springmvc的配置文件-->
    2. <init-param>
    3. <param-name>contextConfigLocation</param-name>
    4. <param-value>classpath:spring/applicationContext-*.xml</param-value>
    5. </init-param>

    SpringMvc默认就会默认去WEB-INF下查找默认规范的配置文件,像我这里配置的servlet-nameseckill-dispatchServlet的话,则默认会寻找WEB-INF一个名为seckill-dispatchServlet-Servlet.xml的配置文件

接下来编写Controller SeckillController

首先在com.suny下建立包为Controller的包,然后在里面新建一个类SeckillController

  1. package com.suny.controller;
  2. /**
  3. * Created by 孙建荣 on 17-5-24.下午10:11
  4. */
  5. @Controller
  6. @RequestMapping("/seckill")
  7. public class SeckillController {
  8. private final SeckillService seckillService;
  9. @Autowired
  10. public SeckillController(SeckillService seckillService) {
  11. this.seckillService = seckillService;
  12. }
  13. /**
  14. * 进入秒杀列表.
  15. *
  16. * @param model 模型数据,里面放置有秒杀商品的信息
  17. * @return 秒杀列表详情页面
  18. */
  19. @RequestMapping(value = {"/list","","index"}, method = RequestMethod.GET)
  20. public String list(Model model) {
  21. List<Seckill> seckillList = seckillService.getSeckillList();
  22. model.addAttribute("list", seckillList);
  23. return "list";
  24. }
  25. @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)
  26. public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
  27. if (seckillId == null) {
  28. return "redirect:/seckill/list";
  29. }
  30. Seckill seckill = seckillService.getById(seckillId);
  31. if (seckill == null) {
  32. return "forward:/seckill/list";
  33. }
  34. model.addAttribute("seckill", seckill);
  35. return "detail";
  36. }
  37. /**
  38. * 暴露秒杀接口的方法.
  39. *
  40. * @param seckillId 秒杀商品的id
  41. * @return 根据用户秒杀的商品id进行业务逻辑判断,返回不同的json实体结果
  42. */
  43. @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.GET)
  44. @ResponseBody
  45. public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId) {
  46. // 查询秒杀商品的结果
  47. SeckillResult<Exposer> result;
  48. try {
  49. Exposer exposer = seckillService.exportSeckillUrl(seckillId);
  50. result = new SeckillResult<>(true, exposer);
  51. } catch (Exception e) {
  52. e.printStackTrace();
  53. result = new SeckillResult<>(false, e.getMessage());
  54. }
  55. return result;
  56. }
  57. /**
  58. * 用户执行秒杀,在页面点击相应的秒杀连接,进入后获取对应的参数进行判断,返回相对应的json实体结果,前端再进行处理.
  59. *
  60. * @param seckillId 秒杀的商品,对应的时秒杀的id
  61. * @param md5 一个被混淆的md5加密值
  62. * @param userPhone 参与秒杀用户的额手机号码,当做账号密码使用
  63. * @return 参与秒杀的结果,为json数据
  64. */
  65. @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST)
  66. @ResponseBody
  67. public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") long seckillId,
  68. @PathVariable("md5") String md5,
  69. @CookieValue(value = "userPhone", required = false) Long userPhone) {
  70. // 如果用户的手机号码为空的说明没有填写手机号码进行秒杀
  71. if (userPhone == null) {
  72. return new SeckillResult<>(false, "没有注册");
  73. }
  74. // 根据用户的手机号码,秒杀商品的id跟md5进行秒杀商品,没异常就是秒杀成功
  75. try {
  76. // 这里换成储存过程
  77. SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
  78. return new SeckillResult<>(true, execution);
  79. } catch (RepeatKillException e1) {
  80. // 重复秒杀
  81. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
  82. return new SeckillResult<>(false, execution);
  83. } catch (SeckillCloseException e2) {
  84. // 秒杀关闭
  85. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
  86. return new SeckillResult<>(false, execution);
  87. } catch (SeckillException e) {
  88. // 不能判断的异常
  89. SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
  90. return new SeckillResult<>(false, execution);
  91. }
  92. // 如果有异常就是秒杀失败
  93. }
  94. /**
  95. * 获取服务器端时间,防止用户篡改客户端时间提前参与秒杀
  96. *
  97. * @return 时间的json数据
  98. */
  99. @RequestMapping(value = "/time/now", method = RequestMethod.GET)
  100. @ResponseBody
  101. public SeckillResult<LocalDateTime> time() {
  102. LocalDateTime localDateTime = LocalDateTime.now();
  103. return new SeckillResult<>(true, localDateTime);
  104. }
  105. }

建立一个全局ajax请求返回类,返回json类型

SeckillResult:

  1. package com.suny.dto;
  2. /**
  3. * 封装所有的ajax请求返回类型,方便返回json
  4. * Created by 孙建荣 on 17-5-24.下午10:18
  5. */
  6. public class SeckillResult<T> {
  7. private boolean success;
  8. private T data;
  9. private String error;
  10. public SeckillResult() {
  11. }
  12. public SeckillResult(boolean success, T data) {
  13. this.success = success;
  14. this.data = data;
  15. }
  16. public SeckillResult(boolean success, String error) {
  17. this.success = success;
  18. this.error = error;
  19. }
  20. public boolean isSuccess() {
  21. return success;
  22. }
  23. public void setSuccess(boolean success) {
  24. this.success = success;
  25. }
  26. public T getData() {
  27. return data;
  28. }
  29. public void setData(T data) {
  30. this.data = data;
  31. }
  32. public String getError() {
  33. return error;
  34. }
  35. public void setError(String error) {
  36. this.error = error;
  37. }
  38. @Override
  39. public String toString() {
  40. return "SeckillResult{" +
  41. "状态=" + success +
  42. ", 数据=" + data +
  43. ", 错误消息='" + error + '\'' +
  44. '}';
  45. }
  46. }

页面的编写

因为项目的前端页面都是由Bootstrap开发的,所以我们要先去下载Bootstrap或者是使用在线的CDN.
-Bootstrap中文官网
-Bootstrap中文文档
使用在线CDN引入的方法:

  1. <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
  2. <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
  3. <!-- 可选的 Bootstrap 主题文件(一般不用引入) -->
  4. <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
  5. <!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
  6. <script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>

文档里面写的很详细,然后我这里是使用离线版本的,方便我们本地调试,避免出现什么别的因素干扰我们:

  • 首先下载JQuery,因为Bootstrap就是依赖JQuery
  • 然后下载Bootstrap
  • 然后下载一个倒计时插件jquery.countdown.min.js
    -再下载一个操作Cookie插件jquery.cookie.min.js
    如图放置:
    资源图

  • 首先编写一个公共的头部jsp文件,位于WEB-INFcommon中的head.jsp

    1. <meta name="viewport" content="width=device-width, initial-scale=1.0">
    2. <meta charset="utf-8">
    3. <link rel="stylesheet" href="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" type="text/css">
    4. <link rel="stylesheet" href="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/css/bootstrap-theme.min.css" type="text/css">
  • 然后编写一个公共的jstl标签库文件,位于WEB-INFcommon中的tag.jsp
    1. <%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
    2. <%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
    3. <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
  • 编写列表页面,位于WEB-INFcommon中的list.jsp

    1. <%@page contentType="text/html; charset=UTF-8" language="java" %>
    2. <%@include file="common/tag.jsp" %>
    3. <!DOCTYPE html>
    4. <html lang="zh-CN">
    5. <head>
    6. <title>秒杀列表</title>
    7. <%@include file="common/head.jsp" %>
    8. </head>
    9. <body>
    10. <div class="container">
    11. <div class="panel panel-default">
    12. <div class="panel-heading text-center">
    13. <h2>秒杀列表</h2>
    14. </div>
    15. <div class="panel-body">
    16. <table class="table table-hover">
    17. <thead>
    18. <tr>
    19. <td>名称</td>
    20. <td>库存</td>
    21. <td>开始时间</td>
    22. <td>结束时间</td>
    23. <td>创建时间</td>
    24. <td>详情页</td>
    25. </tr>
    26. </thead>
    27. <tbody>
    28. <c:forEach items="${list}" var="sk">
    29. <tr>
    30. <td>${sk.name}</td>
    31. <td>${sk.number}</td>
    32. <td><fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"></fmt:formatDate></td>
    33. <td><fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"></fmt:formatDate></td>
    34. <td><fmt:formatDate value="${sk.createTIme}" pattern="yyyy-MM-dd HH:mm:ss"></fmt:formatDate></td>
    35. <td><a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">详情</a></td>
    36. </tr>
    37. </c:forEach>
    38. </tbody>
    39. </table>
    40. </div>
    41. </div>
    42. </div>
    43. </body>
    44. <script src="${pageContext.request.contextPath}/resources/plugins/jquery.js"></script>
    45. <script src="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
    46. </html>
    47. `
    • 编写列表页面,位于WEB-INFcommon中的detail.jsp,秒杀详情页面
      ```jsp
      <%—
      Created by IntelliJ IDEA.
      User: jianrongsun
      Date: 17-5-25
      Time: 下午5:03
      To change this template use File | Settings | File Templates.
      —%>
      <%@ page contentType=”text/html;charset=UTF-8” language=”java” %>
      <%@include file=”common/tag.jsp” %>


      秒杀商品详情页面
      <%@include file=”common/head.jsp” %>




      ${seckill.name}