本文章为转载文章 原网址:https://blog.csdn.net/lewky_liu/article/details/78159983, 侵删
本SSM实战项目使用了Maven进行依赖管理,如果有不清楚Maven是什么的可以参考这篇文章
MAVEN_HOME
,值为Maven的目录X:\XXX\apache-maven-XXX
%MAVEN_HOME%\bin
添加到Path
变量下mvn -v
后可以看到Maven的版本信息等则表示安装成功第一种创建方式:使用命令行手动创建
mvn archetype:generate -DgroupId=com.lewis.seckill -DartifactId=seckill -Dpackage=com.lewis.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp
在视频中使用的是archetype:create
,该方法已被废弃,请使用archetype:generate
来创建。命令行执行后会创建一个maven-archetype-webapp
骨架的Maven项目,其中groupId
是项目组织唯一的标识符,实际对应JAVA的包的结构;artifactId
是项目的唯一的标识符,实际对应项目的名称;package
一般是groupId
+artifactId
,是自动生成的,可以修改
第二种创建方式:借助IDE工具的Maven插件来创建项目
Eclipse安装Maven插件
File
→New
→Other...
→Maven Project
→Next
,进入如下界面
点击Next
,选择要构建的骨架maven-archetype-webapp
,如下图
点击Next
,填写groupId=com.lewis.seckill
,DartifactId=seckill
,package=com.lewis.seckill
(根据实际情况填写),然后Finish
如果是第一次使用Eclipse的Maven插件来创建Maven项目的可能会遇到一些问题,可以参考该博文
当创建完Maven项目后会在根目录下有一个pom.xml文件,Maven项目通过pom.xml进行项目依赖的管理,如果没有该xml文件,Eclipse不会将该项目当作一个Maven项目
添加项目需要的jar包依赖
"http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
4.0.0
com.lewis
seckill
war
0.0.1-SNAPSHOT
seckill Maven Webapp
http://maven.apache.org
junit
junit
4.11
test
org.slf4j
slf4j-api
1.7.12
ch.qos.logback
logback-core
1.1.1
ch.qos.logback
logback-classic
1.1.1
mysql
mysql-connector-java
5.1.35
runtime
c3p0
c3p0
0.9.1.1
org.mybatis
mybatis
3.3.0
org.mybatis
mybatis-spring
1.2.3
taglibs
standard
1.1.2
jstl
jstl
1.2
com.fasterxml.jackson.core
jackson-databind
2.5.4
javax.servlet
javax.servlet-api
3.1.0
org.springframework
spring-core
4.1.7.RELEASE
org.springframework
spring-beans
4.1.7.RELEASE
org.springframework
spring-context
4.1.7.RELEASE
org.springframework
spring-jdbc
4.1.7.RELEASE
org.springframework
spring-tx
4.1.7.RELEASE
org.springframework
spring-web
4.1.7.RELEASE
org.springframework
spring-webmvc
4.1.7.RELEASE
org.springframework
spring-test
4.1.7.RELEASE
redis.clients
jedis
2.7.3
com.dyuproject.protostuff
protostuff-core
1.0.8
com.dyuproject.protostuff
protostuff-runtime
1.0.8
seckill
src/main/java
**/*.xml
false
src/main/resources
关于maven依赖的简化写法
教学视频中老师写了很多的依赖,但其实这里面有一些是可以省略不写的,因为有些包会自动依赖其它的包(Maven的传递性依赖)。这里面可以省略的依赖有:spring-core;spring-beans(上面这两个spring-context会自动依赖);spring-context,spring-jdbc(mybatis-spring会依赖);spring-web(spring-webmvc会依赖);logback-core(logback-classic会依赖)
有想要了解Maven的依赖范围与传递性依赖的请参考该博文
秒杀业务的核心是对库存的处理,其业务流程如下图
用户针对库存业务分析
当用户执行秒杀成功时,应该发生以下两个操作:
这两个操作属于一个完整事务,通过事务来实现数据落地
为什么需要事务?
在实际中,以上都是很严重的事故,会给商家或买家带来损失,这是不能被允许的。一旦发生这种事故,事故责任很自然的就会去找设计实现业务的程序员
如何实现数据落地?
有MySQL与NoSQL两种数据落地的方案
事务机制依然是目前最可靠的数据落地方案。
数据落地与不数据落地
难点问题:如何高效地处理竞争?
当一个用户在执行秒杀某件商品时,其他也想要秒杀该商品的用户就只能等待,直到上一个用户提交或回滚了事务,他才能够得到该商品的锁执行秒杀操作。这里就涉及到了锁的竞争。
对于MySQL来说,竞争反应到背后的技术是就是事务+行级锁:
start transaction(开启事务)→ update库存数量 → insert购买明细 → commit(提交事务)
在秒杀系统中,在同一时刻会有很多用户在秒杀同一件商品,那么如何高效低处理这些竞争?如何高效地提交事务?这些将在Java高并发秒杀API(四)之高并发优化进行分析总结。
实现哪些秒杀功能?
下面先以天猫的秒杀库存系统为例,如下图
可以看到,天猫的秒杀库存系统是很复杂的,需要很多工程师共同开发。在这里,我们只实现秒杀相关的功能
为什么要进行秒杀接口暴露的操作?
现实中有的用户回通过浏览器插件提前知道秒杀接口,填入参数和地址来实现自动秒杀,这对于其他用户来说是不公平的,我们也不希望看到这种情况
源码里有个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 '库存数量',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
`end_time` TIMESTAMP NOT NULL 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-01-01 00:00:00','2016-01-02 00:00:00'),
('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00');
-- 秒杀成功明细表
-- 用户登录认证相关信息(简化为手机号)
CREATE TABLE success_killed(
`seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
`user_phone` BIGINT NOT NULL COMMENT '用户手机号',
`state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone),/*联合主键*/
KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
秒杀成功明细表为何使用联合主键
之所以使用联合主键,是为了能够过滤重复插入,可以通过insert ignore into
语句来避免用户重复秒杀同一件商品。这样当有重复记录就会忽略,语句执行后返回数字0。
可能存在的问题
安装视频里的建表过程,可能会出现建表失败的情况。原因是当你给一个timestamp设置为on update current_timestamp的时候,其他的timestamp字段需要显式设定default值。
但是如果你有两个timestamp字段,但是只把第一个设定为current_timestamp而第二个没有设定默认值,MySQL也能成功建表,但是反过来就不行。这是mysql5.5版本对timestamp的处理。
为了解决这个问题,将create_time放到start_time和end_time的前面,还有的mysql版本需要将三个时间戳都设置默认值。
在
src/main/java
包下创建com.lewis.entity包,接着建立Seckill
实体类
public class Seckill {
private Long seckillId;
private String name;
private Integer number;
private Date createTime;
private Date startTime;
private Date endTime;
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 == null ? null : name.trim();
}
public Integer getNumber() {
return number;
}
public void setNumber(Integer number) {
this.number = number;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
@Override
public String toString() {
return "Seckill [seckillId=" + seckillId + ", name=" + name + ", number=" + number + ", createTime=" + createTime + ", startTime="
+ startTime + ", endTime=" + endTime + "]";
}
}
在com.lewis.entity包下,接着建立
SuccessKilled
实体类
public class SuccessKilled {
private Byte state;
private Date createTime;
private Long seckillId;
private Long userPhone;
// 多对一,因为一件商品在库存中有很多数量,对应的购买明细也有很多。
private Seckill seckill;
public Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
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 Byte getState() {
return state;
}
public void setState(Byte state) {
this.state = state;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "SuccessKilled [state=" + state + ", createTime=" + createTime + ", seckillId=" + seckillId
+ ", userPhone=" + userPhone + "]";
}
}
在
src/main/java
下建立com.lewis.dao
包,在包下建立SeckillDao
接口
public interface SeckillDao {
/**
* 减库存
*
* @param seckillId
* @param killTime
* @return 更新的记录行数,如果返回值<1则表示更新失败
*/
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
/**
* 根据id查询秒杀商品
*
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);
/**
* 根据偏移量查询秒杀商品列表
*
* @param offset
* @param limit
* @return
*/
List queryAll(@Param("offset") int offset, @Param("limit") int limit);
}
在
com.lewis.dao
包下建立SuccessKilledDao
接口
public interface SuccessKilledDao {
/**
* 插入购买明细,可过滤重复
*
* @param seckillId
* @param userphone
* @return 插入的行数,如果返回值<1则表示插入失败
*/
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
/**
* 根据id查询SuccessKilled并携带秒杀商品对象实体
*
* @param seckillId
* @return
*/
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}
为什么有的方法形参前有@Param,有的却没有?
从上面的代码可以发现,当方法的形参在两个及两个以上时,需要在参数前加上@Param,如果不加上该注解会在之后的测试运行时报错。这是Sun提供的默认编译器(javac)在编译后的Class文件中会丢失参数的实际名称,方法中的形参会变成无意义的arg0、arg1等,在只有一个参数时就无所谓,但当参数在两个和两个以上时,传入方法的参数就会找不到对应的形参。因为Java形参的问题,所以在多个基本类型参数时需要用@Param注解区分开来。
MyBatis怎么用?SQL写在哪里?
Mybatis有两种提供SQL的方式:XML提供SQL、注解提供SQL(注解是java5.0之后提供的一个新特性)。
对于实际的使用中建议使用XML文件的方式提供SQL。如果通过注解的方式提供SQL,由于注解本身还是java源码,这对于修改和调整SQL其实是非常不方便的,一样需要重新编译类,当我们写复杂的SQL尤其拼接逻辑时,注解处理起来就会非常繁琐。而XML提供了很多的SQL拼接和处理逻辑的标签,可以非常方便的帮我们去做封装。
如何去实现DAO接口?
Mapper自动实现DAO(也就是DAO只需要设计接口,不需要去写实现类,MyBatis知道我们的参数、返回类型是什么,同时也有SQL文件,它可以自动帮我们生成接口的实现类来帮我们执行参数的封装,执行SQL,把我们的返回结果集封装成我们想要的类型) 。
第二种是通过API编程方式实现DAO接口(MyBatis通过给我们提供了非常多的API,跟其他的ORM和JDBC很像)
在实际开发中建议使用Mapper自动实现DAO,这样可以直接只关注SQL如何编写,如何去设计DAO接口,帮我们节省了很多的维护程序,所有的实现都是MyBatis自动完成。
创建一个目录存放Mybatis的SQL映射
按照Maven的规范,SQL映射文件应该放在src/main/resources
包下,在该包下建立mapper
目录,用来存放映射DAO接口的XML文件。这样Maven在编译时就会自动将src/main/resources
下的这些配置文件编译进来。
我们也可以按照原本的习惯,在src/main/java
下建立com.lewis.mapper
包,将这些SQL映射存放到这里。由于Maven默认不会编译src/main/java
下除源码以外的文件,所以需要在pom.xml中进行额外的配置。
seckill
src/main/java
**/*.xml
false
src/main/resources
在本项目中,我是采用的第二种方式存放Mybatis的SQL映射。(只是将映射DAO的mapper文件放在java包下,其他的关于Spring、MyBatis等的配置文件还是放在resources包下)
在
src/main/resources
目录下配置mybatis-config.xml(配置MyBatis的全局属性)
打开MyBatis的官方文档(MyBatis的官方文档做的非常友好,提供了非常多版本的国际化支持),选择
,找到MyBatis全局配置,里面有XML的规范(XML的标签约束dtd文件),拷入到项目的MyBatis全局配置文件中,开始配置MyBatis,如下:
入门
"1.0" encoding="UTF-8" ?>
"-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
"useGeneratedKeys" value="true" />
"useColumnLabel" value="true" />
"mapUnderscoreToCamelCase" value="true" />
在
src/main/java
目录下的com.lewis.mapper
包里创建SeckillDao.xml
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
"com.lewis.dao.SeckillDao">
"reduceNumber">
UPDATE seckill
SET number = number-1
WHERE seckill_id=#{seckillId}
AND start_time
#{killTime}
AND end_time >= #{killTime}
AND number > 0;
在
src/main/java
目录下的com.lewis.mapper
包里创建SuccessKilledDao.xml
"-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
"com.lewis.dao.SuccessKilledDao">
"insertSuccessKilled">
INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)
注:上面的s.seckill_id “seckill.seckill_id”表示s.seckill_id这一列的数据是Success_killed实体类里的seckill属性里的seckill_id属性,是一个级联的过程,使用的就是别名只是忽略了as关键字,别名要加上双引号。
为什么要用
把
<=
给包起来
CDATA指的是不应由 XML 解析器进行解析的文本数据,在XML元素中,<
和&
是非法的:
<
会产生错误,因为解析器会把该字符解释为新元素的开始。&
也会产生错误,因为解析器会把该字符解释为字符实体的开始。(字符实体:比如
表示一个空格)所以在这里我们需要使用来告诉XML
<=
不是XML的语言。
在resources
目录下创建一个新的目录spring
(存放所有Spring相关的配置)
在resources包下创建jdbc.properties,用于配置数据库的连接信息
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
jdbc.username=root
password=123
在
resources/spring
目录下创建Spring关于DAO层的配置文件spring-dao.xml
"1.0" encoding="UTF-8"?>
"http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
"classpath:jdbc.properties"/>
"dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
"driverClass" value="${driver}" />
"jdbcUrl" value="${url}" />
"user" value="${jdbc.username}" />
"password" value="${password}" />
"maxPoolSize" value="30"/>
"minPoolSize" value="10"/>
"autoCommitOnClose" value="false"/>
"checkoutTimeout" value="1000"/>
"acquireRetryAttempts" value="2"/>
"sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
"dataSource" ref="dataSource"/>
"configLocation" value="classpath:mybatis-config.xml"/>
"typeAliasesPackage" value="com.lewis.entity"/>
"mapperLocations" value="classpath:com/lewis/mapper/*.xml"/>
"org.mybatis.spring.mapper.MapperScannerConfigurer">
"sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
"basePackage" value="com.lewis.dao"/>
关于数据库连接池的配置可能出现的问题
在jdbc.properties里使用的是jdbc.username
,而不是username
或者name
,这是因为后两个属性名可能会与全局变量冲突,导致连接的数据库用户名变成了电脑的用户名,所以使用了jdbc.username
。
相关链接
关于Spring的XML配置文件的头部文件的说明可以参考这篇文章
有不知道Eclipse如何直接进行生成快速的测试单元的,可以看看这篇文章
使用Eclipse工具直接生成测试单元,这些测试代码按照Maven规范放到src/test/java
包下。在生成的测试代码里测试我们的方法,测试的具体代码如下:
SeckillDaoTest.java
package com.lewis.dao;
import static org.junit.Assert.*;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.lewis.entity.Seckill;
/**
* 配置Spring和Junit整合,junit启动时加载springIOC容器 spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring的配置文件
@ContextConfiguration({ "classpath:spring/spring-dao.xml" })
public class SeckillDaoTest {
// 注入Dao实现类依赖
@Resource
private SeckillDao seckillDao;
@Test
public void testQueryById() {
long seckillId = 1000;
Seckill seckill = seckillDao.queryById(seckillId);
System.out.println(seckill.getName());
System.out.println(seckill);
}
@Test
public void testQueryAll() {
List seckills = seckillDao.queryAll(0, 100);
for (Seckill seckill : seckills) {
System.out.println(seckill);
}
}
@Test
public void testReduceNumber() {
long seckillId = 1000;
Date date = new Date();
int updateCount = seckillDao.reduceNumber(seckillId, date);
System.out.println(updateCount);
}
}
测试说明
先左键单击要测试的那个方法名,再右键点击选择Debug As
可以单独对该方法进行单元测试。三个方法都测试通过,但是对于最后一个方法会发现数据库中该商品数量并没有减少,这是因为我们设置了秒杀时间,当前时间不满足秒杀时间,所以不会秒杀成功减少数量。
如果之前没有在DAO接口的多参数方法里在形参前加上@Param注解,那么在这里进行单元测试时,MyBatis会报绑定参数失败的错误,因为无法找到参数。这是因为Java没有保存行参的记录,Java在运行的时候会把queryAll(int offset,int limit)
中的参数变成这样queryAll(int arg0,int arg1)
,导致MyBatis无法识别这两个参数。
SuccessKilledDaoTest.java
package com.lewis.dao;
import static org.junit.Assert.*;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.lewis.entity.SuccessKilled;
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring的配置文件
@ContextConfiguration({ "classpath:spring/spring-dao.xml" })
public class SuccessKilledDaoTest {
@Resource
private SuccessKilledDao successKilledDao;
@Test
public void testInsertSuccessKilled() {
long seckillId = 1000L;
long userPhone = 13476191877L;
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
System.out.println("insertCount=" + insertCount);
}
@Test
public void testQueryByIdWithSeckill() {
long seckillId = 1000L;
long userPhone = 13476191877L;
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
System.out.println(successKilled);
System.out.println(successKilled.getSeckill());
}
}
测试说明
测试方法同上,测试结果通过,另外由于我们使用了联合主键,在insert时使用了ignore关键字,所以对于testInsertSuccessKilled()
重复插入同一条数据是无效的,会被过滤掉,确保了一个用户不能重复秒杀同一件商品。
本节结语
至此,关于Java高并发秒杀API的DAO层的开发与测试已经完成,接下来进行Service层的开发、测试,详情可以参考Java高并发秒杀API(二)之Service层。
出现问题:maven中jar包版本冲突,springframework版本换成4.3.4,junit版本换成4.12版本以上解决