SSM架构之高并发秒杀之Service : http://www.jianshu.com/p/1d91a3c0edc7
一、相关技术
1、前端
- 前端交互设计
- Bootstrap
- Jquery
2、Spring
- Spring IOC整合Service
- 声明式事务运用
3、Spring MVC
- Restful接口设计与使用
- 框架用作流程
- Controller开发技巧
4、MyBatis
- DAO层的设计与开发
- MyBatis合理使用
- MyBatis与Spring的整合
5、MySQL
- 表设计
- SQL技巧
- 事务与行级锁
6、高并发
- 高并发点和高并发分析
- 优化思路并实现
7、环境与工具
- Windows
- Maven
- IntelliJ IDEA / eclipse
二、需求分析
1、基本需求分析
- 秒杀业务的核心:就是对库存的处理
- 用户的购买行为
- 潜在问题分析
2、难点分析
- 竞争
mysql 事务 + 行级锁 ,是竞争在技术上的真正体现。
- 事务:开启事务——update 数据库数量更新——insert 购买明细——commit提交(update 库存更新,是资源占用最多的时候)
- 行级锁
- ** 如何高效的处理竞争 **
3、主要的秒杀功能
秒杀接口暴露
执行秒杀
秒杀相关查询
4、代码开发
- DAO 层设计编码
- service 层设计编码
- web 设计编码
三、项目搭建
多查看官方文档,而不是仅仅通过搜索引擎,因为很多博客等技术类的文档都从在这过时以及不完善性,而官方文档不仅新且全。
相关配置的官方地址:
- logback 配置:https://logback.qos.ch/manual/
- spring 配置:http://docs.spring.io/spring/docs/
- mybatis 配置:http://www.mybatis.org/mybatis-3/index.html
1、pom.xml 文件配置
pom.xml配置文件主要用于项目所依赖jar包的管理
对于该项目,主要依赖的jar包有:
- 单元测试(4.0以上版本主要以注入的方式进行单元测试)
- junit 4.10
- 日志:日志记录的主要有:slf4j、log4j、logback、common-logging,其中 slf4j 是规范接口,而 log4j、logback、common-logging 则为日志的实现
- slf4j-api 1.7.12
- logback-core 1.1.1 实现log日志的核心功能
- logback-classic 1.1.1 实现 slf4j ,并对其进行整合
- 数据库相关依赖
* mysql-connector-java 5.1.35 数据库驱动依赖
* c3p0 0.9.1.2 数据库链接池
- DAO 层Mybatis依赖
* mybatis 3.3.0 MyBatis自身的依赖
* mybatis-spring 1.2.3 MyBatis 与 Spring 整合依赖(由MyBatis提供)
- servlet web 相关依赖
* standard 1.2.1 jsp依赖的相关标签
* jstl 1.2 js默认的标签库
* jackson-databind 2.5.4 Jackson的标签
* javax.servlet-api 3.1.0 servlet依赖的api
- Spring依赖
* 1、Spring核心依赖
* spring-core 4.1.7.RELEASE Spring核心
* spring-beans 4.1.7.RELEASE Spring IOC 依赖
* spring-context 4.1.7.RELEASE Spring 扩展依赖
- Spring DAO 依赖
* spring-jdbc 4.1.7.RELEASE Spring jdbc依赖
* spring-tx 4.1.7.RELEASE Spring 事务依赖
- Spring web 依赖
* spring-web 4.1.7.RELEASE Spring web 项目在启动时依赖
* spring-webmvc 4.1.7.RELEASE Spring-MVC依赖
- Spring test依赖(单元测试时要加载Spring的容器)
* spring-test 4.1.7.RELEASE 方便通过junit进行单元测试
4.0.0
org.seckill
seckill
war
1.0-SNAPSHOT
seckill Maven Webapp
http://maven.apache.org
junit
junit
4.10
test
org.slf4j
slf4j-api
1.7.6
ch.qos.logback
logback-core
1.1.1
ch.qos.logback
logback-classic
1.1.1
mysql
mysql-connector-java
5.1.37
runtime
c3p0
c3p0
0.9.1.2
com.alibaba
dubbo
2.5.3
org.springframework
spring
com.alibaba
druid
0.2.19
org.mybatis
mybatis
3.3.0
org.mybatis
mybatis-spring
1.2.3
taglibs
standard
1.1.2
jstl
jstl
1.2
com.fasterxml.jackson.core
jackson-databind
2.5.4
javax.servlet
javax.servlet-api
3.1.0
org.springframework
spring-core
4.1.7.RELEASE
org.springframework
spring-beans
4.1.7.RELEASE
org.springframework
spring-context
4.1.7.RELEASE
org.springframework
spring-jdbc
4.1.7.RELEASE
org.springframework
spring-tx
4.1.7.RELEASE
org.springframework
spring-web
4.1.7.RELEASE
org.springframework
spring-webmvc
4.1.7.RELEASE
org.springframework
spring-test
4.1.7.RELEASE
seckill
2、web.xml
修改 servlet 版本为 3.0 以上,因为只有3.0 以上才支持el等表达式
四、DAO 层、数据库开发设计
1、schema.sql
进行数据设计
-- 数据库初始化脚本
-- 1、创建数据库
-- 创建秒杀库存表
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 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 ('1000元秒杀iPhone6',100,'2017-02-13 00:00:00','2017-02-14 00:00:00'),
('500元秒杀iPad2',200,'2017-02-13 00:00:00','2017-02-14 00:00:00'),
('300元秒杀小米4',300,'2017-02-13 00:00:00','2017-02-14 00:00:00'),
('100元秒杀小米note',400,'2017-02-13 00:00:00','2017-02-14 00:00:00');
-- 秒杀成功明细表
-- 用户登陆认真相关信息
CREATE TABLE success_killed (
seckill_id BIGINT NOT NULL AUTO_INCREMENT 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 '秒杀成功明细表';
2、DAO 实体与接口设计
1) 实体类创建
- Seckill.java
产品实体类
package org.seckill.bean;
import java.util.Date;
/**
* 产品秒杀实体类
* Created by wangxf on 2017/2/13.
*/
public class Seckill {
private long seckillId; // 产品id
private String name; // 产品名称
private int number; // 产品数量
private Date startTime; // 秒杀开始时间
private Date endTime; // 秒杀介绍时间
private Date createTime; // 创建时间
public Seckill() {
}
public Seckill(long seckillId, String name, int number, Date startTime, Date endTime, Date createTime) {
this.seckillId = seckillId;
this.name = name;
this.number = number;
this.startTime = startTime;
this.endTime = endTime;
this.createTime = createTime;
}
public long getSeckillId() {
return seckillId;
}
public String getName() {
return name;
}
public int getNumber() {
return number;
}
public Date getStartTime() {
return startTime;
}
public Date getEndTime() {
return endTime;
}
public Date getCreateTime() {
return createTime;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public void setName(String name) {
this.name = name;
}
public void setNumber(int number) {
this.number = number;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
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 +
'}';
}
}
- SuccessKilled.java
秒杀明细实体类
package org.seckill.bean;
import java.util.Date;
/**
* 秒杀明细实体类
* Created by wangxf on 2017/2/13.
*/
public class SuccessKilled {
private long seckillId; // 产品id
private long userPhone; // 用户手机号码
private short state; // 状态 -1 无效 0 秒杀成功 1 已付款 2 已收货
private Date createTime; // 创建时间
private Seckill seckill; // 产品实体对象 多对一
public SuccessKilled() {
}
public SuccessKilled(long seckillId, long userPhone, short state, Date createTime) {
this.seckillId = seckillId;
this.userPhone = userPhone;
this.state = state;
this.createTime = createTime;
}
public Seckill getSeckill() {
return seckill;
}
public void setSeckill(Seckill seckill) {
this.seckill = seckill;
}
public long getSeckillId() {
return seckillId;
}
public long getUserPhone() {
return userPhone;
}
public short getState() {
return state;
}
public Date getCreateTime() {
return createTime;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public void setUserPhone(long userPhone) {
this.userPhone = userPhone;
}
public void setState(short state) {
this.state = state;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "SuccessKilled{" +
"seckillId=" + seckillId +
", userPhone=" + userPhone +
", state=" + state +
", createTime=" + createTime +
", seckill=" + seckill +
'}';
}
}
2) Dao层接口
- ISeckillDao.java
库存操作Dao层接口
package org.seckill.dao.interfaces;
import org.apache.ibatis.annotations.Param;
import org.seckill.bean.Seckill;
import java.util.Date;
import java.util.List;
/**
* 库存操作Dao层接口
* Created by wangxf on 2017/2/13.
*/
public interface ISeckillDao {
/**
* 减库存,返回更新的行数 0 表示更新失败
* @param seckillId
* @param killTime
* @return
*/
public int updateProductNumber( @Param("seckillId")long seckillId, @Param("killTime") Date killTime );
/**
* 通过产品id来查询产品信息
* @param seckillId
* @return
*/
public Seckill selectProductById(long seckillId);
/**
* 根据偏移量查询秒杀商品列表
* @param offet 偏移量
* @param limit 要取得行数
* @return
*/
/**
* 会遇到的ERROR: Caused by: org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [0, 1, param1, param2]
* 原因:java中没有保存形参的记录,selectProductAll( long offset, int limit ) ——> selectProductAll(args1,args2),
* 即当有多个参数时,会无法分清哪个参数时那个,但一个参数时没有问题的
* 解决方法:利用 MyBatis提供的@Param() ,selectProductAll(@Param("offset") long offset, @Param("linit")int limit ) 显式的告诉 MyBatis 谁是谁
*/
public List selectProductAll(@Param("offset") long offset, @Param("limit")int limit );
}
- ISuccessKilledDao.java
秒杀明细Dao层操作接口
package org.seckill.dao.interfaces;
import org.apache.ibatis.annotations.Param;
import org.seckill.bean.SuccessKilled;
/**
* 秒杀详细信息记录Dao操作接口
* Created by wangxf on 2017/2/13.
*/
public interface ISuccessKilledDao {
/**
* 插入购买明细
* 可以过滤重复,返回插入的行数
* @param seckillId
* @param userPhone
* @return
*/
public int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone );
/**
* 根据 id 查询 SuccessKilled 并携带秒杀产品对象实体
* @param seckillId
* @return
*/
public SuccessKilled selectByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone );
}
3) 基于 MyBatis 实现DAO
- 通过 xml 配置文件来实现 sql 执行语句 (MyBatis也提供注解的方式,但是不提倡使用)。
- 通过 Mapper 自动实现 DAO 层接口(API 编程的方式实现DAO层接口,但不推荐使用)
- **mybatis-config.xml **
mybatis 相关配置
- SeckillDao.xml
Dao层接口 ISeckillDao.java 接口的实现
UPDATE seckill SET number = number -1
WHERE seckill_id = #{seckillId} AND start_time #{killTime} AND end_time >= #{killTime} AND number > 0
- SuccessKilledDao.xml
Dao层接口 ISuccessKilledDao.java 接口的实现
INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)
4) MyBatis 整合 Spring
- 目标:
- 更少的编码:只写接口,不写实现
- 更少的配置:达到自动实现DAO、自动的注入到Spring中
- 足够的灵活:MyBatis能够自由的定制SQL,自由的传参、结果集自动赋值
提倡的整合方式:xml提供sql,DAO 接口Mapper
- spring-dao.xml
5) Junit 单元测试
- ISeckillDaoTest.java
package org.seckill.dao.interfaces;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.bean.Seckill;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* ISeckillDao 接口的测试类
* Spring 和 junit 整合
* 目的:视为了让 junit 在启动时加载 Spring IOC 容器
* 原因:因为 Dao 接口的实现是由 Spring 完成的
*
* 实现:通过 JUnit 的@RunWith(SpringJUnit4ClassRunner.class) 接口来加载Spring 的SpringJUnit4ClassRunner
* 在加载时,用Spring 的ContextConfiguration来加载验证 MyBatis 与 Spring 的整合文件
* spring-test、junit{}
* Created by wangxf on 2017/2/22.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class ISeckillDaoTest {
// 注入 dao 实现类
@Resource
private ISeckillDao seckillDao;
/**
* 库存信息更细测试方法
* @throws Exception
*/
@Test
public void updateProductNumber() throws Exception {
Date killTime = new Date();
int updateCount = seckillDao.updateProductNumber(1000L, killTime);
System.out.println("-------------------------------");
System.out.println(updateCount);
System.out.println("-------------------------------");
}
/**
* 通过id查询库存产品信息
* @throws Exception
*/
@Test
public void selectProductById() throws Exception {
long id = 1000; // id
Seckill seckill = seckillDao.selectProductById(id); // 获取 seckill对象
System.out.println("-------------------------------");
System.out.println(seckill.getName());
System.out.println(seckill);
System.out.println("-------------------------------");
}
/**
* 查询库中所有的产品信息
* @throws Exception
*/
@Test
public void selectProductAll() throws Exception {
List seckillList = seckillDao.selectProductAll(0, 100);
for (Seckill seckill : seckillList) {
System.out.println("----------------------------");
System.out.println(seckill.getName());
System.out.println(seckill);
System.out.println("----------------------------");
}
}
}
- ISuccessSeckilledDaoTest.java
package org.seckill.dao.interfaces;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.bean.SuccessKilled;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import static org.junit.Assert.*;
/**
* ISuccessKilledDao 接口的测试类
* Created by wangxf on 2017/2/23.
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
public class ISuccessKilledDaoTest {
@Resource
private ISuccessKilledDao successKilledDao;
/**
* 插入购买明细测试方法
* @throws Exception
*/
@Test
public void insertSuccessKilled() throws Exception {
long seckillId = 1000L;
long userPhone = 18779118283L;
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
System.out.println("-------------------------------");
System.out.println(insertCount);
System.out.println("-------------------------------");
}
/**
* 根据 id 查询 SuccessKilled 并携带秒杀产品对象实体 测试类
* @throws Exception
*/
@Test
public void selectByIdWithSeckill() throws Exception {
long seckillId = 1000L;
long userPhone = 18779118283L;
SuccessKilled successKilled = successKilledDao.selectByIdWithSeckill(seckillId,userPhone);
System.out.println("-------------------------------");
System.out.println(successKilled.toString());
System.out.println("-------------------------------");
System.out.println(successKilled.getSeckill());
System.out.println("-------------------------------");
}
}
项目参考:http://www.imooc.com/learn/587