具体可以参考github
mvn archetype:generate -DgroupId=cn.codingxiaxw.seckill -DartifactId=seckill -Dpackage=cn.codingxiaxw.seckill -Dversion=1.0-SNAPSHOT -DarchetypeArtifactId=maven-archetype-webapp
打开WEB-INF下的web.xml,它默认为我们创建servlet版本为2.3,需要修改它的根标签为:
项目架构如下:
pom.xml如下:
4.0.0
cn.czy.seckill
seckill
war
1.0-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
seckill
这样项目也就基本构建好了。
为什么我们的系统需要事务?看如下这些故障:1.若是用户成功秒杀商品我们记录了其购买明细却没有减库存。导致商品的超卖。2.减了库存却没有记录用户的购买明细。导致商品的少卖。对于上述两个故障,若是没有事务的支持,损失最大的无疑是我们的用户和商家。在MySQL中,它内置的事务机制,可以准确的帮我们完成减库存和记录用户购买明细的过程。
MySQL实现秒杀的难点分析:当用户A秒杀id为10的商品时,此时MySQL需要进行的操作是:1.开启事务。2.更新商品的库存信息。3.添加用户的购买明细,包括用户秒杀的商品id以及唯一标识用户身份的信息如电话号码等。4.提交事务。若此时有另一个用户B也在秒杀这件id为10的商品,他就需要等待,等待到用户A成功秒杀到这件商品然后MySQL成功的提交了事务他才能拿到这个id为10的商品的锁从而进行秒杀,而同一时间是不可能只有用户B在等待,肯定是有很多很多的用户都在等待拿到这个行级锁。秒杀的难点就在这里,如何高效的处理这些竞争?如何高效的完成事务?在后面第4个模块如何进行高并发的优化为大家讲解。
实现功能:
1.秒杀接口的暴露。
2.执行秒杀的操作。
3.相关查询,比如说列表查询,详情页查询。
CREATE TABLE seckill (
`seckill_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
`name` VARCHAR(120) NOT NULL COMMENT '商品名称',
`number` INT NOT NULL COMMENT '库存数量',
`start_time` TIMESTAMP NOT NULL COMMENT '开始时间',
`end_time` TIMESTAMP NOT NULL COMMENT '结束时间',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (seckill_id),
KEY idx_start_time(start_time),
KEY idx_end_time(end_time),
KEY idx_create_time(create_time)
) ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
When I try to create the table with the previous query, this is the error I get:
ERROR 1067 (42000): Invalid default value for ‘end_time’
答案:
In MySQL, the TIMESTAMP data type differs in nonstandard ways from other data types:
Those nonstandard behaviors remain the default for TIMESTAMP but as of MySQL 5.6.6 are deprecated and this warning appears at startup:
[Warning] TIMESTAMP with implicit DEFAULT value is deprecated.
Please use --explicit_defaults_for_timestamp server option
(see documentation for more details).
在mysql高版本会出现这样的问题,在命令行加上这句话就可以了
set @@session.explicit_defaults_for_timestamp=on;
建完该表后,就set @@session.explicit_defaults_for_timestamp=off;再建下一个表就好了。
在MySQL 5.7版本之前,且在MySQL 5.6.6版本之后(explicit_defaults_for_timestamp参数在MySQL 5.6.6开始加入)的版本中,如果没有设置explicit_defaults_for_timestamp=1的情况下:
1)在默认情况下,如果TIMESTAMP列没有显示的指明null属性,那么该列会被自动加上not null属性(而其他类型的列如果没有被显示的指定not null,那么是允许null值的),如果往这个列中插入null值,会自动的设置该列的值为current timestamp值。
2)表中的第一个TIMESTAMP列,如果没有指定null属性或者没有指定默认值,也没有指定ON UPDATE语句。那么该列会自动被加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP属性。
3)第一个TIMESTAMP列之后的其他的TIMESTAMP类型的列,如果没有指定null属性,也没有指定默认值,那么该列会被自动加上DEFAULT ‘0000-00-00 00:00:00’属性。如果insert语句中没有为该列指定值,那么该列中插入’0000-00-00 00:00:00’,并且没有warning。
如果我们在启动的时候在配置文件中指定了explicit_defaults_for_timestamp=1,MySQL会按照如下的方式处理TIMESTAMP列:
1)此时如果TIMESTAMP列没有显示的指定not null属性,那么默认的该列可以为null,此时向该列中插入null值时,会直接记录null,而不是current timestamp。
2)不会自动的为表中的第一个TIMESTAMP列加上DEFAULT CURRENT_TIMESTAMP和ON UPDATE CURRENT_TIMESTAMP属性,除非你在建表的时候显示的指明。
3)如果TIMESTAMP列被加上了not null属性,并且没有指定默认值。这时如果向表中插入记录,但是没有给该TIMESTAMP列指定值的时候,如果strict sql_mode被指定了,那么会直接报错。如果strict sql_mode没有被指定,那么会向该列中插入’0000-00-00 00:00:00’并且产生一个warning。
-- 数据库初始化脚本
-- 创建数据库
CREATE DATABASE seckill;
-- 使用数据库
use seckill;
--创建秒杀库存表
CREATE TABLE seckill(
`seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID',
`name` VARCHAR(120) NOT NULL COMMENT '商品名称',
`number` int NOT NULL COMMENT '库存数量',
`start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
`end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (seckill_id),
key idx_start_time(start_time),
key idx_end_time(end_time),
key idx_create_time(create_time)
)ENGINE=INNODB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
--初始化数据
INSERT INTO seckill(name,number,start_time,end_time)
VALUES
('1000元秒杀iphone6',100,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
('800元秒杀ipad',200,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
('6600元秒杀mac book pro',300,'2018-05-01 00:00:00','2018-05-02 00:00:00'),
('7000元秒杀iMac',400,'2018-05-01 00:00:00','2018-05-02 00:00:00');
-- 秒杀成功明细表
-- 用户登录认证相关信息(简化为手机号)
CREATE TABLE success_killed(
`seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
`user_phone` BIGINT NOT NULL COMMENT '用户手机号',
`state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone),/*联合主键*/
KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
--连接数据库控制台
mysql -uroot -p
-- SHOW CREATE TABLE seckill;#显示表的创建信息
package org.seckill.entity;
import java.util.Date;
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Seckill{" +
"seckillId=" + seckillId +
", name='" + name + '\'' +
", number=" + number +
", startTime=" + startTime +
", endTime=" + endTime +
", createTime=" + createTime +
'}';
}
}
package org.seckill.entity;
import java.util.Date;
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
private Seckill seckill;
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public long getUserPhone() {
return userPhone;
}
public void setUserPhone(long userPhone) {
this.userPhone = userPhone;
}
public short getState() {
return state;
}
public void setState(short state) {
this.state = state;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckill(Seckill seckill) {
this.seckill = seckill;
}
@Override
public String toString() {
return "SuccessKilled{" +
"seckillId=" + seckillId +
", userPhone=" + userPhone +
", state=" + state +
", createTime=" + createTime +
'}';
}
}
package org.seckill.dao;
import org.seckill.entity.Seckill;
import java.util.Date;
import java.util.List;
public interface SeckillDao {
/**
* 减库存
* @param seckillId
* @param killTime
* @return 如果影响行数>1,表示更新库存的记录行数
*/
int reduceNumber(long seckillId, Date killTime);
/**
* 根据id查询秒杀的商品信息
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);
/**
* 根据偏移量查询秒杀商品列表
* @param off
* @param limit
* @return
*/
List queryAll(int off, int limit);
}
package org.seckill.dao;
import org.seckill.entity.SuccessKilled;
public interface SuccessKilledDao {
/**
* 插入购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return插入的行数
*/
int insertSuccessKilled(long seckillId,long userPhone);
/**
* 根据秒杀商品的id查询明细SuccessKilled对象(该对象携带了Seckill秒杀产品对象)
* @param seckillId
* @return
*/
SuccessKilled queryByIdWithSeckill(long seckillId, long userPhone);
}
在resources包下创建MyBatis全局配置文件mybatis-config.xml文件
在resources下创建mapper包,在包下创建对应Dao接口的xml映射文件。SeckillDao.xml和SuccessKilledDao.xml
UPDATE seckill
SET number = number-1
WHERE seckill_id=#{seckillId}
AND start_time #{killTime}
AND end_time >= #{killTime}
AND number > 0;
需要我们在resources包下创建jdbc.properties用于配置数据库的连接信息,内容如下:
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8&useSSL=false
jdbc.username=root
jdbc.password=newpass
首先测试SeckillDao.java,利用IDEA快捷键shift+command+T
对SeckillDao.java进行测试,然后IDEA会自动在test包的java包下为我们生成对SeckillDao.java中所有方法的测试类SeckillDaoTest.java,内容如下:
public class SeckillDaoTest {
@Test
public void reduceNumber() throws Exception {
}
@Test
public void queryById() throws Exception {
}
@Test
public void queryAll() throws Exception {
}
}