整合运用SSM框架,做一个基于Spring+SpringMVC+Mybatis实现的高并发秒杀项目,后面采用redis缓存来优化高并发。学习秒杀类系统需求理解和实现以及解决高并发问题的常用技术。
第一部分写一下业务分析已经DAO层的设计与实现。
源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利
一、相关技术及开发环境
①Mysql:
进行表设计
常用SQL技巧
事务和行级锁
②MyBatis:
DAO层设计与开发
Mybatis的合理使用
Mybatis与Spring整合
③Spring:
SpringIOC整合Service
声明式事务的运用
④SpringMVC:
Restful接口的设计与使用
框架的运作流程
Controller开发技巧
⑤前端:
交互设计
Bootstrap
jQuery
Ajax
⑥高并发
高并发点和高并发分析
优化思路并实现
开发环境:Ubuntu16.04+IntelliJIDEA+jdk1.8+Mysql
二、秒杀系统业务分析及难点分析
①秒杀系统的业务流程如下:
可以从图中看到,秒杀系统的业务核心就是对库存的处理。
②用户针对库存的业务分析如下:
因此要实现对库存的处理,关键就要记录秒杀成功的信息,即用户的购买行为。购买行为就是指 谁购买成功了;成功的时间/有效期;付款/发货信息。
③MySQL实现秒杀难点分析
秒杀系统要解决的难点问题其实就是“竞争”的问题,即多个用户同时争夺物品。
这个“竞争”反映到MySQL中就是事务加行级锁,在后面的实现中会逐一介绍。
三、DAO层设计与开发
首先使用IDEA构建一个Maven项目,引入相关的依赖,具体的依赖可以在pom.xml文件中查看。
1)数据库设计
①创建秒杀库存表
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 DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀开始时间',
`end_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '秒杀结束时间',
`create_time` TIMESTAMP 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
('6000元秒杀iphone8',100,'2018-05-20 00:00:00','2018-05-21 00:00:00'),
('3800元秒杀ipad',200,'2018-05-20 00:00:00','2018-05-21 00:00:00'),
('9600元秒杀mac book pro',300,'2018-05-20 00:00:00','2018-05-20 00:00:00'),
('9999元秒杀锤子TNT',400,'2018-05-20 00:00:00','2016-05-21 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='秒杀成功明细表';
2)DAO实体及接口定义
①根据所创建的数据库表,定义出相应的实体类与数据查询接口。
新建entity包,里面新建Seckill类和SuccessKilled类,属性与对应的数据表一致,getter()、setter()方法等。
②新建dao包,里面新建SeckillDao接口,定义秒杀商品所需要的方法
public interface SeckillDao {
//减库存
//java没有保存形参的记录:有多个参数的时候会报错
// 加上param注解 ,识别形参名称。
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
//根据id查询秒杀的商品信息
Seckill queryById(long seckillId);
//根据偏移量查询秒杀商品列表
List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
}
③dao包下新建SuccessKillDao接口,定义秒杀成功所需要的方法
public interface SuccessKillDao {
//新增购买明细记录,由于表中设计了联合主键,可过滤重复
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
//根据秒杀商品id查询明细SuccessKilled对象,该对象中携带了Seckill对象
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}
3)基于Mybatis实现DAO接口
Mybatis实现接口有两种方式:一是通过Mapper会自动实现DAO接口,二是可以通过API编程方式来实现DAO接口。
我们这里采用第一种方式,减少代码量。
①resources目录下新建mybatis-config.xml文件,用于配置mybatis的相关属性。
<?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的getGeneratekeys获取自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!--使用列别名替换列名 默认值为true
select name as title(实体中的属性名是title) form table;
开启后mybatis会自动帮我们把表中name的值赋到对应实体的title属性中
-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
②resources目录下新建mapper目录,用于配置相关的映射文件。
新建SeckillDao.xml如下:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--目的:为dao接口方法提供sql语句配置
即针对dao接口中的方法编写对应的sql语句-->
<mapper namespace="dao.SeckillDao">
<update id="reduceNumber">
UPDATE 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" resultType="Seckill" parameterType="long">
SELECT * FROM seckill WHERE seckill_id = #{seckillId}
</select>
<select id="queryAll" resultType="Seckill">
SELECT * FROM seckill
ORDER BY create_time DESC
limit #{offset},#{limit}
</select>
</mapper>
新建SuccessKillDao.xml如下:
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="dao.SuccessKillDao">
<!-- insert ignore表示,如果中已经存在相同的记录,则忽略当前新数据;-->
<!--当出现主键冲突时(即重复秒杀时),会报错;这里不想让程序报错,加入ignore,这样能通过返回的更新行数0 , 1 来确定是否更新成功-->
<insert id="insertSuccessKilled">
INSERT ignore INTO success_killed(seckill_id, user_phone, state)
VALUES (#{seckillId},#{userPhone,0})
</insert>
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!--根据seckillId查询SuccessKilled对象,并携带Seckill对象-->
<!--告诉mybatis把结果映射到SuccessKilled属性同时映射到Seckill属性-->
SELECT
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id as "seckill.seckill_id",
s.name as "seckill.name",
s.number as "seckill.number",
s.start_time as "seckill.start_time",
s.end_time as "seckill.end_time",
s.create_time as "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>
4)Mybatis整合Spring
①resources目录下新建jdbc.properties文件,用于配置数据库信息
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/seckill
jdbc.username=root
jdbc.password=
②resources目录下新建spring 目录,里面新建spring-dao.xml文件,用于向Spring IOC中装配dao层所需要的bean,包括数据库连接池、Mybatis的SqlsessionFactory对象,以及扫描DAO接口实现类并注入进IOC
<?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.配置数据库相关参数-->
<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"/>
<!--关闭连接后不自动commit-->
<property name="autoCommitOnClose" value="false"/>
<!--获取连接超时时间-->
<property name="checkoutTimeout" value="1000"/>
<!--当获取连接失败重试次数-->
<property name="acquireRetryAttempts" value="2"/>
</bean>
<!--3.配置SqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--往下才是mybatis和spring真正整合的配置-->
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开-->
<property name="typeAliasesPackage" value="entity"/>
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--4:配置扫描Dao接口包,动态实现DAO接口,注入到spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory、在数据库连接池加载后才注入SQlsessionfactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口-->
<property name="basePackage" value="dao"/>
</bean>
</beans>
5)DAO层单元测试
接下来就对SecKillDao和SuccessKillDao接口层进行测试。
①首先在SecKillDao类中使用idea的快捷键“shift+ctrl+t”快速生成测试类,注意选用jUnit4。
编写测试代码如下:
/**
* 配置spring和junit整合,这样junit在启动时就会加载spring容器
*/
@RunWith(SpringJUnit4ClassRunner.class)//Spring启动时加载spring容器
@ContextConfiguration({"classpath:spring/spring-dao.xml"})//告诉junit spring的配置文件,完成bean的注入
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" + updateCount);
}
@Test
public void queryById() {
long id = 1000;//数据库表中设置的从1000开始自增
Seckill seckill = seckillDao.queryById(id);
System.out.println(seckill);
}
@Test
public void queryAll() {
List<Seckill> seckills = seckillDao.queryAll(0,100);
for (Seckill seckill : seckills){
System.out.println(seckill);
}
}
}
逐个测试每个方法,均验证通过。
②同理验证SuccessKillDao接口,测试类如下:
/**
* 配置spring和junit整合,这样junit在启动时就会加载spring容器
*/
@RunWith(SpringJUnit4ClassRunner.class)//Spring启动时加载spring容器
@ContextConfiguration({"classpath:spring/spring-dao.xml"})//告诉junit spring的配置文件,完成bean的注入
public class SuccessKillDaoTest {
@Resource
private SuccessKillDao successKillDao;
@Test
public void insertSuccessKilled() {
Long secKillId = 1000L;
long phone = 13066632317L;
int insertCount = successKillDao.insertSuccessKilled(secKillId, phone);
System.out.println(insertCount);
//第一次的执行结果为1,秒杀成功;第二次执行结果为0,秒杀失败,因为表中设置了联合主键,不允许重复秒杀
}
@Test
public void queryByIdWithSeckill() {
Long secKillId = 1000L;
long phone = 13066632317L;
SuccessKilled successKilled = successKillDao.queryByIdWithSeckill(secKillId, phone);
System.out.println(successKilled);
System.out.println(successKilled.getSeckill());
}
}
逐个测试每个方法,均验证通过。
6)DAO层小结
到此,秒杀项目的DAO层已经实现完成,由于采用了MybatisORM框架,DAO层工作演变为:接口设计+SQL编写。代码和SQL分离,方便后期review。
并且DAO层只实现相关数据库的增删改查基本功能,具体的DAO拼接等复杂逻辑会在下一部分的Service层完成。
网友评论