Java高并发之秒杀系统DAO层开发

基于Mybatis的Java高并发之秒杀系统DAO层开发。

一、数据库编码设计

创建秒杀库存表seckill记录秒杀的商品

  • 其中商品id为主键。

  • 分别对秒杀开启时间,秒杀结束时间,创建时间建立索引优化查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
--创建秒杀库存表
CREATE TABLE seckill
(
`seckill_id` bigint not null auto_increment comment '商品库存id',
`name` varchar(120) 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 ='秒杀库存表';

初始化秒杀库存表中的商品

1
2
3
4
-- 初始化数据
insert into seckill(name, number, start_time, end_time)
values ('1000元秒杀IPhone11', 100, '2019-10-1 00:00:00', '2019-10-1 00:00:00'),
('800元秒杀小米8', 200, '2019-10-1 00:00:00', '2019-10-1 00:00:00');

创建秒杀成功明细表success_killed记录秒杀成功订单信息

  • 以秒杀商品id与用户手机号作为联合主键。
1
2
3
4
5
6
7
8
9
10
11
12
13
-- 秒杀成功明细表
-- 用户登录认证相关信息
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 default current_timestamp comment '创建时间',
primary key (seckill_id, user_phone), /*联合主键*/
key idx_create_time (create_time)

) engine = innoDB
default charset = utf8 comment '秒杀成功明细表'

二、DAO实体和编码设计

实体属性映射数据库表的列名。

DAO接口定义方法。

  • 使用lombok插件简化代码
  • 属性名称遵循驼峰命名

Seckill.java

1
2
3
4
5
6
7
8
9
@Data
public class Seckill {
private long seckillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime;
}

SuccessKilled.java

1
2
3
4
5
6
7
8
9
10
@Data
public class SuccessKilled {
private long seckillId;
private long userPhone;
private int state;
private Date createTime;

//多对一,成功的秒杀订单可能包含多个商品
private Seckill seckill;
}

SeckillDao接口

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
public interface SeckillDao {
/**
* 减库存
*
* @param seckillId
* @param killTime
* @return 如果影响行数>=1,表示更新 的记录行数
* java传递的是形参,向Mybatis传入参数需要@Param注解
*/
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);

/**
* 根据id查询秒杀对象
*
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);

/**
* 根据偏移量(offset开始的limit个)查询秒杀商品列表
*
* @param offset
* @param limit
* @return
* java传递的是形参,向Mybatis传入参数需要@Param注解
*/
List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);

}

SuccessKilledDao接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public interface SuccessKilledDao {
/**
* 插入购买明细,可过滤重复
*
* @param seckillId
* @param userPhone
* @return 插入的行数
*/
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);
}

三、基于Mybatis实现DAO编程

Mybatis配置文件(mybatis-config.xml)

在resource文件夹下创建mybatis-config.xml。

  • 引入配置文件的DTD约束。
  • 当设置了useGeneratedKeys=“true”,Mybatis会调用JDBC的getGeneratedKeys方法,并将获取的主键值赋值给keyProperty 指定的属性。
  • 使用别名替换列名。 例如:select name as title from table,默认使用title。
  • 开启驼峰命名转换。
    使用前提:数据库表设计按照规范“字段名中各单词使用下划线”_“划分”;seckill_id–>seckillId。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?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
select name as title from table-->
<setting name="useColumnLabel" value="true"/>

<!--开启驼峰命名转换:Table(create_time)->Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>

SeckillDao映射文件(SeckillDao.xml)

实现SeckillDao接口中定义的方法

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
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="szy.seckill.dao.SeckillDao">
<update id="reduceNumber">
<!--具体的sql语句-->
update
seckill
set
number=number-1
where seckill_id=#{seckillId}
and start_time&lt;=#{killTime}
and end_time&gt;=#{killTime}
and number&gt;0
</update>

<select id="queryById" resultType="Seckill" parameterType="long">
select seckill_id as seckillId, name, number, start_time, end_time, create_time
from seckill
where seckill_id = #{seckillId}

</select>

<select id="queryAll" resultType="Seckill" parameterType="int">
select seckill_id as seckillId, name, number, start_time, end_time, create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
</mapper>

SuccessKilledDao映射文件(SuccessKilledDao.xml)

实现SuccessKilledDao接口中定义的方法

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
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="szy.seckill.dao.SuccessKilledDao">
<!--目的:为DAO接口方法提供sql语句配置-->
<insert id="insertSuccessKilled">
<!--主键冲突,报错-->
insert ignore into success_killed(seckill_id, user_phone,state)
values (#{seckillId},#{userPhone},0)
</insert>

<select id="queryByIdWithSeckill" resultType="SuccessKilled" parameterType="long">
<!--根据id查询SuccessKilled并携带秒杀实体-->
<!--结果映射到SuccessKilled同时映射Seckill属性-->
<!--与其他框架的区别:可以自由控制SQL-->
<!--前面开启的“驼峰命名转换"将seckill_id->seckillId-->
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 success_killed sk inner join seckill s
on sk.seckill_id=s.seckill_id
where sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}
</select>

</mapper>

四、Spring整合Mybatis

Spring4.0.4 官方文档数据源配置属性前要加jdbc

jdbc.properties

配置驱动,连接地址,用户名,密码

1
2
3
4
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/imseckill?serverTimezone=GMT%2B8&useSSL=false
jdbc.username=shenziyao
jdbc.password=960206ash

Spring整合Mybatis文件(spring-dao.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
39
40
41
42
43
44
45
46
47
48
49
<?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">
<!--配置整合mybatis-->
<!--1:配置数据库相关参数properties的属性:${url}-->
<context:property-placeholder location="classpath:jdbc.properties"/>

<!--2:数据库连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--配置连接池属性-->
<property name="driverClass" value="${jdbc.driver}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>

<!--c3p0连接池的私有属性(根据实际经验和业务场景调整)-->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!--连接关闭时,回滚未提交的操作-->
<property name="autoCommitOnClose" value="false"/>
<!--设置连接超时时间为1s-->
<property name="checkoutTimeout" value="1000"/>
<!--连接失败时重试次数-->
<property name="acquireRetryAttempts" value="2"/>
</bean>

<!--3:配置SqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
<!--配置mybatis全局配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描entity包,使用别名-->
<property name="typeAliasesPackage" value="szy.seckill.entity"/>
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>

<!--配置扫描dao接口包,动态实现Dao接口,注入到spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入sqlSessionFactory属性-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--给出扫描Dao接口包-->
<property name="basePackage" value="szy.seckill.dao"/>
</bean>
</beans>

五、DAO层单元测试

在IDEA中选中需要单元测试的类按下Ctrl+Shift+T,创建测试类

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
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring 配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})

public class SeckillDaoTest {
//注入Dao实现类依赖
@Resource
private SeckillDao seckillDao;

@Test
//当前时间处于秒杀时间之间,则减库存,返回减少的行数
public void reduceNumber() {
Date killTime = new Date();
int updateCount = seckillDao.reduceNumber(1000L, killTime);
System.out.println(updateCount);
}

@Test
//根据商品id查询商品
public void queryById() {
long id = 1000;
Seckill seckill = seckillDao.queryById(id);
System.out.println(seckill.getName());
System.out.println(seckill);
}

@Test
//从offset开始查询limit个商品
public void queryAll() {

List<Seckill> listOfSeckill = seckillDao.queryAll(0, 1);
for (Seckill seckillDao : listOfSeckill) {
System.out.println(seckillDao);
}
}
}
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
/**
* 配置spring和junit整合,junit启动时加载springIOC容器
* spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring 配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})

public class SuccessKilledDaoTest {
//注入Dao实现类依赖
@Resource
private SuccessKilledDao successKilledDao;

@Test
public void insertSuccessKilled() {
/*
第一次:insertcount=1
第二次:insertcount=0
说明不允许重复秒杀
*/
long id = 1001L;
long phone = 18305177367L;
int insertcount = successKilledDao.insertSuccessKilled(id, phone);
System.out.println("insertcount=" + insertcount);
}

@Test
public void queryByIdWithSeckill() {
long id = 1001L;
long phone = 18305177367L;
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone);
System.out.println(successKilled);
System.out.println(successKilled.getSeckill());

}
}