一个整合SSM框架的高并发和商品秒杀项目,学习目前较流行的Java框架组合实现高并发秒杀API
项目开发流程
本项目很适合学习一些技术的基础,这个项目的开发分为几个流程,很基础地教你接触到一个相对有技术含量的项目
1.Java高并发秒杀API之业务分析与DAO层
2.Java高并发秒杀API之web层
3.Java高并发秒杀API之Service层
4.Java高并发秒杀API之高并发优化
按照上面几个流程走下去,你要有基本的Maven认识以及Java语法的一些概念,要不然可能不太理解
其实这几个流程也就是开发的流程,首先从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
git clone https://github.com/Sunybyjava/seckill.git
导入到IDE
这里因为是使用IDEA创建的项目,所以使用IDEA直接打开是很方便的,提前是你要配置好maven的相关配置,以及项目JDK版本,JDK版本必须在1.8以上,因为在项目中使用了Java8的LocalDateTime以及LocalDate,所以低于这个版本编译会失败的
(一)Java高并发秒杀APi之业务分析与DAO层代码编写
构建项目的基本骨架
首先我们要搭建出一个符合Maven约定的目录来,这里大致有两种方式,第一种:
第一种使用命令行手动构建一个maven结构的目录,当然我基本不会这样构建
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项目,然后groupId为com.suny.seckill ,artifactId为seckill,这里是Maven相关知识,可以按照自己的情况进行修改
2.第二种直接在IDE中进行创建,这里以IDEA为例
+然后就填写你的Maven的那几个重要的坐标了,自己看着填吧
+再就配置你的Maven的相关信息,默认应该是配置好的
+之后就是点Finsh,到此不出意外的话就应该创建成功了
构建pom文件
项目基本的骨架我们就创建出来了,接下来我们要添加一些基本的JAR包的依赖,也就是在pom.xml中添加各种开源组件的三坐标了
建立数据库
在根目录下有一个sql文件夹里面有一个sql数据库脚本,如果你不想自己手写的话就直接导入到你的数据库里面去吧,不过还是建议自己手写一遍加深印象
-- 整个项目的数据库脚本
-- 开始创建一个数据库
CREATE DATABASE seckill;
-- 使用数据库
USE seckill;
-- 创建秒杀库存表
CREATE TABLE seckill(
`seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存ID',
`name` VARCHAR(120) NOT NULL COMMENT '商品名称',
`number` INT NOT NULL COMMENT '库存数量',
`start_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '秒杀开启的时间',
`end_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '秒杀结束的时间',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP() COMMENT '创建的时间',
PRIMARY KEY (seckill_id),
KEY idx_start_time(start_time),
KEY idx_end_time(end_time),
KEY idx_create_time(create_time)
)ENGINE =InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
-- 插入初始化数据
insert into
seckill(name,number,start_time,end_time)
values
('1000元秒杀iphone6',100,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
('500元秒杀iPad2',200,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
('300元秒杀小米4',300,'2016-5-22 00:00:00','2016-5-23 00:00:00'),
('200元秒杀红米note',400,'2016-5-22 00:00:00','2016-5-23 00:00:00');
-- 秒杀成功明细表
-- 用户登录相关信息
create table success_killed(
`seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
`user_phone` BIGINT NOT NULL COMMENT '用户手机号',
`state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标示:-1无效 0成功 1已付款',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY (seckill_id,user_phone), /*联合主键*/
KEY idx_create_time(create_time)
)ENGINE =InnDB DEFAULT CHARSET =utf8 COMMENT ='秒杀成功明细表'
在建立数据库的,如果按照我这里的数据库脚本建立的话应该是没问题的,但是我按照视频里面的数据库脚本建表的话发生了一个错误
这个报错看起来比较的诡异,我仔细检查sql也没有错误,它总提示我end_time要有一个默认的值,可我记得我以前就不会这样,然后视频里面也没有执行错误,然后我感觉可能时MySQL版本的差异,我查看了下我数据库版本,在登录Mysql控制台后输入指令,在控制台的我暂时知道的有两种方式:
select version();
select @@version;
我的输出结果如下:
进控制台就已经可以看到版本了,我的Mysql是5.7的,以前我用的时5.6的,然后去Google上搜索了下,找到了几个答案,参考链接:
mysql 5.7中,默认使用的是严格模式,这里的日期必须要有时间,所以一定要给出默认值,要么就修改数据库设置
然后网友评论里总结出来的几种解决办法,未经测试!:
start_time
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀开始时间',end_time
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀结束时间',create_time
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
对比下我修改过后的跟视频里面的sql片段:
我们可以看到在这三个字段有一个小差别,那就是给start_time,end_time,create_time三个字段都添加一个默认值,然后执行数据库语句就没问题了
这里我们需要修改下web.xml中的servlet版本为3.0
打开WEB-INF下的web.xml,修改为以下代码:
修改的原因有以下几点:
context
org.springframework.context.ContextLoaderServlet
1
org.springframework.context.ContextLoaderListener
两种方法的效果都是一样的,主要不要同时使用,否则会报错的
建立实体类
package com.suny.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
public class SuccessKilled implements Serializable {
private static final long serialVersionUID = 1834437127882846202L;
private long seckillId;
/* 用户的手机号码*/
private long userPhone;
/* 秒杀的状态*/
private short state;
/* 创建时间*/
private LocalDateTime createTime;
/* 多对一,因为一件商品在库存中肯定有许多,对应的购买信息也有很多*/
private Seckill seckill;
public SuccessKilled() {
}
public SuccessKilled(long seckillId, long userPhone, short state, LocalDateTime createTime, Seckill seckill) {
this.seckillId = seckillId;
this.userPhone = userPhone;
this.state = state;
this.createTime = createTime;
this.seckill = seckill;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public long getUserPhone() {
return userPhone;
}
public void setUserPhone(long userPhone) {
this.userPhone = userPhone;
}
public short getState() {
return state;
}
public void setState(short state) {
this.state = state;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
this.seckill = seckill;
}
@Override
public String toString() {
return "SuccessKilled{" +
"主键ID=" + seckillId +
", 手机号码=" + userPhone +
", 秒杀状态=" + state +
", 创建时间=" + createTime +
", 秒杀的商品=" + seckill +
'}';
}
}
package com.suny.entity;
import java.io.Serializable;
import java.time.LocalDateTime;
public class Seckill implements Serializable {
private static final long serialVersionUID = 2912164127598660137L;
/* 主键ID*/
private long seckillId;
/* 秒杀商品名字 */
private String name;
/* 秒杀的商品编号 */
private int number;
/* 开始秒杀的时间 */
private LocalDateTime startTime;
/* 结束秒杀的时间 */
private LocalDateTime endTime;
/* 创建的时间 */
private LocalDateTime createTIme;
public Seckill() {
}
public Seckill(long seckillId, String name, int number, LocalDateTime startTime, LocalDateTime endTime, LocalDateTime createTIme) {
this.seckillId = seckillId;
this.name = name;
this.number = number;
this.startTime = startTime;
this.endTime = endTime;
this.createTIme = createTIme;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public LocalDateTime getCreateTIme() {
return createTIme;
}
public void setCreateTIme(LocalDateTime createTIme) {
this.createTIme = createTIme;
}
@Override
public String toString() {
return "com.suny.entity.Seckill{" +
"主键ID=" + seckillId +
", 秒杀商品='" + name + ''' +
", 编号=" + number +
", 开始秒杀时间=" + startTime +
", 结束秒杀时间=" + endTime +
", 创建时间=" + createTIme +
'}';
}
}
对实体类创建对应的mapper接口,也就是dao接口类
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 {
/**
* 根据传过来的seckillId
去减少商品的库存.
*
* @param seckillId 秒杀商品ID
* @param killTime 秒杀的精确时间
* @return 如果秒杀成功就返回1,否则就返回0
*/
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") LocalDateTime killTime);
/**
* 根据传过来的seckillId
去查询秒杀商品的详情.
*
* @param seckillId 秒杀商品ID
* @return 对应商品ID的的数据
*/
Seckill queryById(@Param("seckillId") long seckillId);
/**
* 根据一个偏移量去查询秒杀的商品列表.
*
* @param offset 偏移量
* @param limit 限制查询的数据个数
* @return 符合偏移量查出来的数据个数
*/
List
}
package com.suny.dao;
import com.suny.entity.SuccessKilled;
import org.apache.ibatis.annotations.Param;
public interface SuccessKilledMapper {
/**
* 插入一条详细的购买信息.
*
* @param seckillId 秒杀商品的ID
* @param userPhone 购买用户的手机号码
* @return 成功插入就返回1, 否则就返回0
*/
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
/**
* 根据秒杀商品的ID查询SuccessKilled
的明细信息.
*
* @param seckillId 秒杀商品的ID
* @param userPhone 购买用户的手机号码
* @return 秒杀商品的明细信息
*/
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}
接下来书写xml配置文件
建立对应的mapper.xml
首先在src/main/resources建立com.suny.dao这个包,也就是对应mapper接口文件包一样的包名,这样符合Maven的约定,就是资源放置在Resource包下,Java包下则是放置java类文件,编译后最后还是会在同一个目录下
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
UPDATE seckill
SET number = number - 1
WHERE seckill_id = #{seckillId}
AND start_time
<=
]]>
#{killTime}
AND end_time >= #{killTime}
AND number > 0
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
INSERT IGNORE INTO success_killed (seckill_id, user_phone, state)
VALUES (#{seckillId}, #{userPhone}, 0)
"-//mybatis.org//DTD MyBatis Generator Configuration 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd" >
然后建立连接数据库的配置文件jdbc.properties,这里的属性要根据自己的需要去进行修改,切勿直接复制使用
jdbc.driver=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=root
jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
建立Spring的dao的配置文件,在resources包下创建applicationContext-dao.xml
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">
建立SeckillMapperTest文件,代码如下
package com.suny.dao;
import com.suny.entity.Seckill;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.List;
import static org.junit.Assert.*;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/applicationContext-dao.xml"})
public class SeckillMapperTest {
@Resource
private SeckillMapper seckillMapper;
@Test
public void reduceNumber() throws Exception {
long seckillId=1000;
LocalDateTime localDateTime=LocalDateTime.now();
int i = seckillMapper.reduceNumber(seckillId, localDateTime);
System.out.println(i);
}
@Test
public void queryById() throws Exception {
long seckillId = 1000;
Seckill seckill = seckillMapper.queryById(seckillId);
System.out.println(seckill.toString());
}
@Test
public void queryAll() throws Exception {
List
for (Seckill seckill : seckills) {
System.out.println(seckill.toString());
}
}
}
测试中可能会出现Mybatis参数绑定失败的错误,在mapper接口中的方法里面添加@Param的注解,显示的告诉mybatis参数的名称是什么,例如
ListqueryAll(@Param("offset") int offset, @Param("limit") int limit);