基于SSM实现高并发秒杀API(DAO层)

首先

  1. SSM的整合
  2. 秒杀类系统需求的理解和实现
  3. 常用技术解决高并发问题

Spring MVC + Spring + MyBatis

  • 框架容易使用,轻量级
  • 侵入性比较低
  • 成熟的社区和用户群

为什么用秒杀类系统

  • 秒杀有典型的事务特性
  • 秒杀/红包类需求很常见
  • 面试常问的问题(如何设计优化秒杀系统)

内容概括

  1. 秒杀列表
  2. 详情页内秒杀按钮或者等待时间或者秒杀结束
  3. 秒杀后库存会减少
  4. 秒杀需要输入电话号码存到cookie中.

开始

MySQL

  • 表设计
  • SQL技巧
  • 事务和行级锁

MyBatis合理使用

  • DAO层设计与开发
  • MyBatis合理使用
  • MyBatis与Spring整合

Spring

  • Spring IOC整合Service
  • 声明式事务的运用

Spring MVC

  • Restful接口的设计和使用
  • 框架运作流程
  • Controller开发技巧

前端

  • 交互设计
  • BootStrap
  • JQuery

高并发

  • 高并发点和高并发分析
  • 优化思路并实现

基于Maven创建项目

建议从官网中获取文档

1
mvn archetype:generate -DgroupId=com.yuda.test -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
<project xmlns="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">
<modelVersion>4.0.0</modelVersion>
<groupId>com.yuda.test</groupId>
<artifactId>seckill</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>seckill Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--
日志相关
slf4j: 是规范/接口
日志实现: log4j,logback,common-logging
这里使用: slf4j + logback
-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</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相关 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>
<!-- Servlet相关依赖 -->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>javax.servlet</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依赖 -->
<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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<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>
<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>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>seckill</finalName>
</build>
</project>

秒杀系统业务流程

  1. 角色有: 商家, 库存, 用户;
  2. 库存是核心;
  3. 用户针对库存的业务:
    1. 减库存+记录购买明细 = 完整事务
    2. 完整事务需要准确的事务落地
  4. 购买行为: 记录用户谁购买成功,成功时间有效期,付款/发货信息

如果…

  1. 减库存没有记录购买明细;
  2. 记了明细没有库存减少;
  3. 出现了超卖/少卖;

数据落地(两种)

MySQL vs NoSQL

MySQL 用事务实现准确且最可靠的落地方案
NoSQL 性能,高可用,分布式

难点分析

  1. 竞争问题:大量用户
    MySQL: 事务+行级锁

    Strat Transaction
    Update 减库存
    Insert 记录购买明细
    Commit

    当一个用户update时,其他用户处于等待状态

    难点是如何高效处理竞争???

  2. 秒杀相关的功能有:

    • 秒杀接口暴露;
    • 执行秒杀;
    • 相关查询

代码开发阶段

编写数据库
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
-- 创建数据库
CREATE DATABASE seckill;

USE seckill;

-- 库存表

CREATE TABLE t_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` 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 ='秒杀成功明细表';

-- 修改
ALTER TABLE seckill.t_seckill
DROP INDEX idx_create_time,
ADD INDEX idx_c_s(start_time, create_time)
> **注意**:购买记录表中的主键使用了联合主键(商品和用户唯一表示),防止了用户购买多个同种商品,
mybatis-config.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?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="useGeneratedKeys" value="true"/>
<!--使用列别名替换列名 默认:true-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
<!--下面的配置交给Spring(为Spring打Call)-->
</configuration>
实体类mapper.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--映射接口-->
<mapper namespace="com.yuda.dao.SeckillDao">
<!--具体的sql语句-->
<update id="reduceNumber">
UPDATE seckill.t_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" parameterType="long" resultType="com.yuda.entity.Seckill">
SELECT
seckill_id,
name,
number,
start_time,
end_time,
create_time
FROM seckill.t_seckill
WHERE seckill_id = #{seckillId};
</select>
<select id="queryAll" resultType="com.yuda.entity.Seckill">
SELECT
seckill_id,
name,
number,
start_time,
end_time,
create_time
FROM seckill.t_seckill
ORDER BY create_time DESC
LIMIT #{offset}, #{limit};
</select>
</mapper>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--映射接口-->
<mapper namespace="com.yuda.dao.SuccessKilledDao">
<!-- 如果出现重复插入问题(主键冲突),不报错,返回0 -->
<insert id="insertSucceccKilled">
INSERT IGNORE INTO seckill.success_killed (seckill_id, user_phone, state)
VALUES (#{seckillId}, #{userPhone}, 0);
</insert>
<!--设置别名用于级联操作-->
<select id="queryByIdWithSeckill" resultType="com.yuda.entity.SuccessKilled">
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.create_time"
FROM seckill.success_killed sk
INNER JOIN seckill.t_seckill s
ON sk.seckill_id = s.seckill_id
WHERE sk.seckill_id = #{seckillId} AND sk.user_phone = #{userPhone};
</select>
</mapper>
spring-dao.xml
> 整合Spring后:
> 1. 更少的编码 (只写接口)
> 2. 更少的配置 (包扫描)
> 3. 足够的灵活性 (SQL自己定义,还可以动态拼接)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
<?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.xsd">
<context:component-scan base-package="com.yuda"/>
<context:property-placeholder location="classpath*:jdbc.properties"/>
<!--DataSource-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${mysql.driver}"/>
<property name="jdbcUrl" value="${mysql.url}"/>
<property name="user" value="${mysql.username}"/>
<property name="password" value="${mysql.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>
<!-- 用spring来创建sqlsessionfactory-->
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<!--管理MyBatis-->
<!--主配文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!-- 扫描entity,生成别名 -->
<property name="typeAliasesPackage" value="com.yuda.entity"/>
<!--扫描实体类配置文件-->
<property name="mapperLocations" value="classpath:com/yuda/mapper/*.xml"/>
</bean>
<!--扫描,配置DAO接口包,注入到容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!-- 扫描Mapper -->
<property name="basePackage" value="com.yuda.dao"/>
<!--注入sessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sessionFactory"/>
</bean>
</beans>
Entity
Table -> 对应 -> Entity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import lombok.Data;
@Data
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
}
@Data
public class SuccessKilled {
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
//多对一
private Seckill seckill;
}
Mapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public interface SuccessKilledDao {

int insertSucceccKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);

SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}
public interface SeckillDao {

int reduceNumber(@Param("seckillId") long seckillId,@Param("killTime") Date killTime);

Seckill queryById(long seckillId);

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

测试功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RunWith(value = SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath*:spring-dao.xml")
public class MainTest {

//Autowired和Resource作用一样不过A是根据类型注入,R是根据名字注入
@Autowired
private SeckillDao seckillDao;
@Resource
private SuccessKilledDao successKilledDao;

@Test
public void demo1() {
Seckill seckill = seckillDao.queryById(1000);
System.out.println("灿宇达" + seckill);
}

@Test
public void demo2() throws Exception {
List<Seckill> seckills = seckillDao.queryAll(0, 5);
for (Seckill seckill : seckills) {
System.out.println(seckill);
}
}

@Test
public void demo3() throws Exception {
Date killTime = new Date();
int i = seckillDao.reduceNumber(1000, killTime);
System.out.println("状态码::" + i);
}

@Test
public void insertSucceccKilled() throws Exception {
int i = successKilledDao.insertSucceccKilled(1001L, 13972218121L);
System.out.println("状态码::" + i);
}

@Test
public void queryByIdWithSeckill() throws Exception {
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(1001L, 13972218121L);
System.out.println(successKilled);
System.out.println(successKilled.getSeckill());
}
}

总结

I am a slow walker,but I never walk backwards.