Java高并发秒杀API之业务分析与DAO层
Posted 德峰
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java高并发秒杀API之业务分析与DAO层相关的知识,希望对你有一定的参考价值。
课程介绍
高并发和秒杀都是当今的热门词汇,如何使用Java框架实现高并发秒杀API是该系列课程要研究的内容。秒杀系列课程分为四门,本门课程是第一门,主要对秒杀业务进行分析设计,以及DAO层的实现。课程中使用了流行的框架组合SpringMVC+Spring+MyBatis,还等什么,赶快来加入吧!
第1章 课程介绍
本章介绍秒杀系统的技术内容,以及系统演示。并介绍不同程度的学员可以学到什么内容。
第2章 梳理所有技术和搭建工程
本章首先介绍秒杀系统所用框架和技术点,然后介绍如何基于maven搭建项目,最后对工程目录包进行了划分。
第3章 秒杀业务分析
本章讲解常见秒杀业务以及如何用最常用的技术实现。分析了秒杀业务的难点,以及本课程要实现哪些秒杀API。
第4章 DAO层设计与开发
本章介绍秒杀系统数据库设计与实现,分析DAO数据持久化层所需接口,并编码实现。以及MyBatis如何与spring进行整合,最后介绍如何测试整合框架,完成DAO层的单元测试。
第1章 课程介绍
1-1 课程介绍
SpringMVC+Spring+MyBatis使用与整合
秒杀类系统需求理解和实现
常用技术解决高并发问题(java web、前端、mysql)
为什么使用这三个框架?
互联网公司常用框架(阿里、京东、搜狐、美团…)
框架易于使用和轻量级
底代码侵入性
成熟的社区和用户群
为什么用秒杀系统来讲本课程?
秒杀业务场景具有典型“事务”特性
秒杀/红包类需求越来越常见
面试常问问题(如何设计一个秒杀系统和优化一个秒杀系统)
从本课程学到什么?
初学者:框架的使用与整合 技巧
有经验者:秒杀分析过程和优化思路
秒杀系列将分为四门课程进行,分别是:
Java高并发秒杀API之业务分析与DAO层
Java高并发秒杀API之Service
Java高并发秒杀API之web
Java高并发秒杀API之高并发优化
1-2 项目效果演示
第2章 梳理所有技术和搭建工程
2-1 相关技术介绍
用到那些技术?
MySQL(表设计 、SQL技巧、事务和行级锁)
MyBatis(DAO层设计与开发、MyBatis合理使用、MyBatis与Spring整合)
Spring(Spring IOC整合Service、声明式事务运用)
SpringMVC(Restful接口设计和使用、框架运作流程、controller开发技巧)
前端(交互设计、Bootstrap、jQuery)
高并发(高并发点和高并发分析、优化思路并实现)
基于maven创建项目
2-2 创建项目和依赖
开始创建项目之前的说明
从零开始创建
从官网获取相关配置
使用Maven创建项目
为什么从官网获取资源
文档更全面权威
避免过时货错误
官网地址:
logback配置:http://logback.qos.ch/manual/configuration.html
spring配置:http://docs.spring.io/spring/docs/
mybatis配置:http://mybatis.github.io/mybatis-3/zh/index.html
开始创建项目
maven命令创建web骨架项目
mvn archetype:generate -DarchetypeCatalog=internal -DgroupId=com.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp
注:-DgroupId和-DartifactId标注项目的坐标,项目叫com.seckill,-DarchetypeArtifactId=maven-archetype-webapp表示使用maven的原型webapp的原型去创建项目
下面就通过IntelliJ IDEA或Eclipse开发工具Import项目
存在的问题:
1.用maven创建的webapp要修改servlet的版本:
maven 创建项目的web.xml 版本比较低(Servlet2.3,jsp默认的el表达式是不工作的),所以需要把他切换到更高的Servlet版本,可以去tomcat webapps包下面去找到示例项目的web.xml的头copy过来。如果使用的IntelliJ IDEA导入的项目需要把一些没有的目录补全,如下示例(仅供参考):
打开maven的pom.xml文件把默认的junit版本改为4.11是应为3.0的junit默认使用编程的方式,4.0是使用注解的方式来运行junit。再接下来就是补全项目的依赖
第3章 秒杀业务分析
3-1 秒杀业务分析
秒杀业务的核心–> 库存的处理
为什么需要事物?
关于数据落地:MySQL VS NoSQL(事务依然是目前最可靠的落地方案,NoSQL对事物的支持做的不是很好。)
3-2 MySQL实现秒杀难点分析
对于MySQL来说竞争反应到背后的技术是怎样的呢?(事物+行级锁)
事物:
Start Transaction
Update库存数量
Insert购买明细
Commit
秒杀的难点是如何高效的处理竞争?
知道问题后如何解决?
3-3 实现哪些秒杀功能
我们只实现秒杀相关的功能
第4章 DAO层设计与开发
4-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 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='描述库存表';
--创建索引,用于加速查询
--key idx_start_time(start_time),
--key idx_end_time(end_time),
--key idx_create_time(create_time)
--默认mysql有多种存储引擎,但是可以支持事物的只有InnoDB,所以在这里通过显示的语法去告诉mysql这个表的ENGINE引擎是InnoDB
--由于数据库采用的是自增作为主键,给出初始的自增id:AUTO_INCREMENT=1000
--默认编码 DEFAULT CHARSET=utf-8
--表的注释 COMMENT='秒杀库存表'
--初始化数据
insert into
seckill(name,number,start_time,end_time)
value
('1000元秒杀iphone6',100,'2012-12-12 00:00:00','2012-12-13 00:00:00'),
('500元秒杀ipad2',200,'2012-12-12 00:00:00','2012-12-13 00:00:00'),
('300元秒杀mi4',300,'2012-12-12 00:00:00','2012-12-13 00:00:00'),
('200元秒杀红米note',400,'2012-12-12 00:00:00','2012-12-13 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 -p123456
--查看创建表的结构
show create table seckill\\G
--为什么手写DDL
--记录每次上线的DDL修改
--上线V1.1
ALTER TABLE seckill
DROP INDEX idx_create_time,
ADD INDEX idx_c_s(start_time,create_time);
--上线V1.2
--DDL
4-2 DAO实体和接口编码
Table —-> Entity
package org.seckill.entity;
import java.util.Date;
/**
* 秒杀库存实体
* @author Administrator
*
*/
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 {
/**
* 一个秒杀seckill对应多个成功记录
*/
private Seckill seckill;
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
public long getSeckillId() {
return seckillId;
}
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 Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
this.seckill = seckill;
}
@Override
public String toString() {
return "SuccessKilled{" +
"seckill=" + seckill +
", seckillId=" + seckillId +
", userPhone=" + userPhone +
", state=" + state +
", createTime=" + createTime +
'}';
}
}
DAO相关接口编码(DAO针对的是具体实体来操作的“实体的增删改查”)
package org.seckill.dao;
import java.util.Date;
import java.util.List;
import org.seckill.entity.Seckill;
public interface SeckillDao {
/**
* 减库存
* @param seckillId
* @param killTime
* @return 如果更新行数大于1,表示更新的行数
*/
int reduceNumber(long seckillId,Date killTime);
/**
* 根据id查询秒杀对象
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);
/**
* 根据偏移量查询描述列表
* @param offet
* @param limit
* @return
*/
List<Seckill> queyAll(int offet,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并携带秒杀产品对象
* @param seckillId
* @return
*/
SuccessKilled queryByIdWithSeckill(long seckillId);
}
4-3 基于myBatis实现DAO理论
MyBatis用来做什么?
MyBatis怎么用
SQL写在哪?(XML提供SQL、注解提供SQL,注:注解是java5.0之后提供的一个新特性)
对于实际的使用中建议使用XML文件的方式提供SQL,通过注解的方式呢,注解本身还是java源码,修改和调整SQL其实是非常不方便的,一样需要重新编译类,当我们写复杂的SQL尤其拼接逻辑时,注解处理起来就会非常繁琐,那么XML提供了很多的SQL拼接和处理逻辑的标签可以非常方便的帮我们去做封装。
如何DAO接口接口
Mapper自动实现DAO(也就是DAO只需要设计接口,不需要去写实现类,MyBatis知道我们的参数、返回类型是什么同时也有SQL文件,他可以制动帮我们生成接口的实现类来帮我们执行参数的封装,执行SQL,把我们的返回结果集封装成我们想要的类型)
第二种是通过API编程方式实现DAO接口(MyBatis通过给我们提供了非常多的API,跟其他的ORM和JDBC很像)
在实际开发中建议使用Mapper自动实现DAO,这样可以直接只关注SQL如何编写,如何去设计DAO接口,帮我们节省了很多的维护程序,所有的实现都是MyBatis自动完成。
4-4 基于myBatis实现DAO编程(上)
打开MyBatis的官方文档
http://www.mybatis.org/mybatis-3/zh/index.html
入门——>找到MyBatis全局配置,里面有XML的规范(XML的标签约束dtd文件)拷入到项目的MyBatis全局配置文件中,开始配置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获取自增主键值,当inert一条记录时我们是不插入id的,id是通过自增去赋值的,当插入完后想得到该插入记录的id时可以调用jdbc的getGeneratekeys -->
<setting name="useGeneratedKeys" value="true" />
<!--使用列别名替换别名 默认true select name as title form table; -->
<setting name="useColumnLabel" value="true" />
<!--开启驼峰命名转换Table:create_time到 Entity(createTime) -->
<setting name="mapUnderscoreToCamelCase" value="true" />
</settings>
</configuration>
使用Mapper自动实现DAO
在mapper目录中创建SeckillDao.xml和SuccessKilledDao.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- namespace:指定为那个接口提供配置 -->
<mapper namespace="org.seckill.dao.SeckillDao">
<!-- 目的:为DAO接口方法提供sql语句配置 -->
<!-- int reduceNumber(long seckillId, Date killTime);-->
<update id="reduceNumber">
<!-- 具体sql -->
UPDATE seckill set number = number -1
where seckill_id = #{seckillId}
AND start_time <![CDATA[ <= ]]> #{killTime}
AND end_time >= #{killTime}
AND number >0;
</update>
<!-- parameterType:参数类型,正常情况java表示一个类型的包名+类名,这直接写类名,因为后面有一个配置可以简化写包名的过程 -->
<!-- Seckill queryById(long seckillId);-->
<select id="queryById" resultType="Seckill" parameterType="long">
<!-- 可以通过别名的方式列明到java名的转换,如果开启了驼峰命名法就可以不用这么写了
select seckill_id as seckillId
-->
SELECT seckill_id,name,number,start_time,end_time,create_time
FROM seckill
WHERE seckill_id = #{seckillId}
</select>
<!-- List<Seckill> queryAll(int offset,int limit);-->
<select id="queryAll" resultType="Seckill">
SELECT seckill_id,name,number,start_time,end_time,create_time
FROM seckill
ORDER BY create_time DESC
limit #{offset},#{limit}
</select>
</mapper>
4-5 基于myBatis实现DAO编程(下)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SuccessKilledDao">
<!--通过ignore关键字将主键冲突时的报错改为返回0-->
<!--int insertSuccessKilled(long seckilledId,long userPhone);-->
<update id="insertSuccessKilled">
INSERT ignore INTO success_killed(seckill_id,user_phone,state)VALUES (#{seckilledId},#{userPhone},1)
</update>
<!-- SuccessKilled queryByIdWithSeckill(@Param("seckilledId") long seckilledId, @Param("userPhone") long userPhone);-->
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
SELECT
sk.seckill_id,sk.user_phone,sk.create_time,sk.state,
s.seckill_id "seckill.seckill_id",s.name "seckill.name", 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=#{seckilledId} and sk.user_phone=#{userPhone};
</select>
</mapper>
注:上面的s.seckill_id “seckill.seckill_id”表示将s.seckill_id这一列的数据是seckill属性里的seckill_id属性,是一个级联的过程,使用的就是别名只是忽略了as关键字,别名要加上双引号。
4-6 myBatis整合Spring理论
整合目标
更少的编码(只写接口,不写实现,MyBatis实现DAO)
更少的配置
足够的灵活性
XML提供SQL,DAO接口Mapper来作为接口实现类时完全可以依赖于Spring去帮我们做好这些事情,当用Spring整合MyBatis时建议使用这两种方式ML提供SQL,DAO接口提供Mapper。
4-7 mybatis整合Spring编码
在resource目录下创建一个新的目录Spring(存放所有spring相关的配置)
<?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.数据库连接池-->
<!--todo java.sql.SQLException: Access denied for user ''@'localhost' (using password: NO)-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!-- 配置连接池属性 -->
<property name="driverClass" value="${driverClassName}"></property>
<property name="jdbcUrl" value="${jdbc.url}"></property>
<property name="user" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
<!--
<property name="driverClass" value="com.mysql.jdbc.Driver"></property>
<property name="jdbcUrl" value="jdbc:mysql://localhost:3306/test_mysql"></property>
<property name="jdbcUrl" value="jdbc:mysql://127.0.0.1:3306/test_mysql"></property>
<property name="user" value="root"></property>
<property name="password" value="111111"></property>
-->
<!-- c3p0连接池的私有属性 -->
<property name="maxPoolSize" value="30"></property>
<property name="minPoolSize" value="10"></property>
<!-- 关闭连接后不自动commit -->
<property name="autoCommitOnClose" value="false"></property>
<!-- 获取连接超时时间 -->
<property name="checkoutTimeout" value="10000"></property>
<!-- 获取连接重试次数 -->
<property name="acquireRetryAttempts" value="3"></property>
</bean>
<!--
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/test_mysql"/>
<property name="username" value="root"/>
<property name="password" value="111111"/>
todo
<property name="driverClassName" value="${driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
-->
<!--3.配置SqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开-->
<property name="typeAliasesPackage" value="org.seckill.entity"/>
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--4:配置扫描Dao接口包,动态实现DAO接口,注入到spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口-->
<property name="basePackage" value="org.seckill.dao"/>
</bean>
<!--RedisDao-->
<!-- <bean id="redisDao" class="org.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="localhost"/>
<constructor-arg index="1" value="6379"/>
</bean> -->
</beans>
4-8 DAO层单元测试编码和问题排查(上)
package org.seckill.dao;
import java.util.Date;
import java.util.List;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.entity.Seckill;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Created by wchb7 on 16-5-8.
*/
/**
* 配置Spring和Junit整合,junit启动时加载springIOC容器
* spring-test,junit
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
public class SeckillDaoTest {
//注入Dao实现类依赖
@Resource
private SeckillDao seckillDao;
@Test
public void testQueryById() throws Exception {
long id = 1000;
Seckill seckill = seckillDao.queryById(id);
System.out.println(seckill);
}
@Test
public void testQueryAll() throws Exception {
// Java没有保存形参的记录:QueryAll(int offset,int limit)->QueryAll(arg0,arg1);
// 因为java形参的问题,多个基本类型参数的时候需要用@Param("seckillId")注解区分开来
List<Seckill> seckills = seckillDao.queryAll(0, 4);
for (Seckill seckill : seckills) {
System.out.println(seckill);
}
}
@Test
public void testReduceNumber() throws Exception {
Date killTime = new Date();
int updateCount = seckillDao.reduceNumber(1000L, killTime);
System.out.println("updateCount: " + updateCount);
}
}
4-9 DAO层单元测试编码和问题排查(下)
package org.seckill.dao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.entity.SuccessKilled;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import static org.junit.Assert.*;
/**
* Created by wchb7 on 16-5-9.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
public class SuccessKilledDaoTest {
@Resource
private SuccessKilledDao successKilledDao;
@Test
public void testInsertSuccessKilled() throws Exception {
/*
第一次:iinsertCount=1
第一次:iinsertCount=0
*/
long id = 1000L;
long phone = 15811112222L;
int insertCount = successKilledDao.insertSuccessKilled(id, phone);
System.out.println("insertCount: " + insertCount);
}
@Test
public void testQueryByIdWithSeckill() throws Exception {
long id = 1000L;
long phone = 15811112222L;
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(id, phone);
System.out.println(successKilled);
if (successKilled != null) {
System.out.println(successKilled.getSeckill());
}
}
}
以上是关于Java高并发秒杀API之业务分析与DAO层的主要内容,如果未能解决你的问题,请参考以下文章
01 整合IDEA+Maven+SSM框架的高并发的商品秒杀项目之业务分析与DAO层