略
首先
- SSM的整合
- 秒杀类系统需求的理解和实现
- 常用技术解决高并发问题
Spring MVC + Spring + MyBatis
- 框架容易使用,轻量级
- 侵入性比较低
- 成熟的社区和用户群
为什么用秒杀类系统
- 秒杀有典型的事务特性
- 秒杀/红包类需求很常见
- 面试常问的问题(如何设计优化秒杀系统)
内容概括
- 秒杀列表
- 详情页内秒杀按钮或者等待时间或者秒杀结束
- 秒杀后库存会减少
- 秒杀需要输入电话号码存到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 | <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" |
秒杀系统业务流程
- 角色有: 商家, 库存, 用户;
- 库存是核心;
- 用户针对库存的业务:
- 减库存+记录购买明细 = 完整事务
- 完整事务需要准确的事务落地
- 购买行为: 记录用户谁购买成功,成功时间有效期,付款/发货信息
如果…
- 减库存没有记录购买明细;
- 记了明细没有库存减少;
- 出现了超卖/少卖;
数据落地(两种)
MySQL vs NoSQL
MySQL 用事务实现准确且最可靠的落地方案
NoSQL 性能,高可用,分布式
难点分析
竞争问题:大量用户
MySQL: 事务+行级锁Strat Transaction
Update 减库存
Insert 记录购买明细
Commit当一个用户update时,其他用户处于等待状态
难点是如何高效处理竞争???
秒杀相关的功能有:
- 秒杀接口暴露;
- 执行秒杀;
- 相关查询
代码开发阶段
编写数据库
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
<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
<!--映射接口-->
<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
<!--映射接口-->
<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
<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;
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
}
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(long seckillId, long userPhone);
SuccessKilled queryByIdWithSeckill(long seckillId, long userPhone);
}
public interface SeckillDao {
int reduceNumber(long seckillId, Date killTime);
Seckill queryById(long seckillId);
List<Seckill> queryAll(int offset, int limit);
}
测试功能
1 |
|
总结
I am a slow walker,but I never walk backwards.