Java高并发秒杀API之业务分析与DAO层,Service层,Web层,高并发优化 过程记录
本项目是Java EE完成(jee只处理了项目创建过程,后期开发的主要过程都是在IntelliJ IDEA 15.0.2中完成)
第一期:Java高并发秒杀API之业务分析与DAO层 20170425左右开始
第二期:Java高并发秒杀API之Service层 2017-05-20开始
第三期:Java高并发秒杀API之Web层 2017-05-20开始
第四期:Java高并发秒杀API之高并发优化 2017-06-01号开始
注意:自动导包输入是大小写敏感的.
1、秒杀具备典型的"事务"特性、秒杀/红包类的需求越来越多、面试常见问题
2、学习框架的使用和整合技巧、秒杀的分析过程以及优化
3、技术介绍
MySQL: 表设计 SQL技巧 事务和行级锁
MyBatis DAO层设计与开发 MyBatis合理利用 MyBatis与Spring整合
Spring:Spring IOC整合Service 声明式事务运用
SpringMVC Restful接口设计和使用 框架运行流程 Controller开发技巧
前端:交互设计 Bootstrap jQuery
高并发:高并发点和高并发分析 优化思路并实现
4、基于Maven创建项目
从0开始创建项目 从官网获取相关配置 使用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
5、先安装Maven http://jingyan.baidu.com/article/1709ad808ad49f4634c4f00d.html
下载 ->
安装 -> 直接放在C盘
添加环境变量->
生成本地仓库-> mvn help:system 命令然后回车 会在C:\Users\Administrator\.m2\repository生成文件(Administrator路径可能不能系统也会不同)
成功打印详细信息,最后一条显示MAVEN_PROJECTBASEDIR=C:\Users\Administrator
自定义用户配置 -> C:\Users\Administrator\.m2 \settings.xml (来源C盘解压目录)
6、开始创建项目
maven命令创建web骨架项目
mvn archetype:create -DgroupId=org.seckill -DartifactId=seckill -
DarchetypeArtifactId=maven-archetype-webapp
使用这句创建
mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=internal
(高版本中使用 archetype:generate而不是 archetype:create ,参数 -DarchetypeCatalog=internal 不让执行过程卡主等待很久)
注意:
注意先不使用IDE工具创建项目,使用mvn创建最基本的项目结构后,
使用eclipse import -> Maven -> Existing Maven Projects->选择创建项目根文件夹->自动勾选pom.xml
使用IntelliJ IDEA,File —> Open 复制 目录的路径后直接打开 D:\workspace\idea\seckill
(使用eclipse可能导致dependency不能提示自动补全)
依赖敲完后clean一下项目开始自动更新或者下载jar
7、处理默认创建的项目问题
切换servlet更高的版本:直接在别的项目中复制WEB-INF/web.xml
8、main文件夹下新建Java,放置Java源文件,在src目录下新建一个test文件夹并新建java、resources配置
9、查看pom.xml
把默认3.8.1的junit改成4.11版本(通过注释的方式运行junit)
继续补全依赖
日志 Java日志 slf4j,log4j,logback,common-logging
slf4j: 是规范/接口
日志实现:log4j,logback,commont-logging
使用:slf4j + logback
注意:从视频中看只要添加artifactId,其他两个值可以自动提示
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<!--2、 数据库相关依赖 -->
<scope>runtime</scope>
<!-- 数据库的连接池 (优化数据库操作)-->
<!-- DAO框架:MYbatis依赖 (数据库持久化) -->
<!-- mybatis自身实现的整合依赖 -->
<!--3、 Servlet web相关依赖 -->
<!-- 4、Spring依赖 -->
<!-- 1)spring核心依赖 -->
<!-- 2)spring dao层依赖 -->
<!-- 3)spring web依赖 -->
<!-- 4)spring test依赖 -->
添加完依赖后自行Ctrl B,Build ALL
10、秒杀业务分析
针对库存做处理
针对用户:减库存、记录购买明细=>完整事务=>数据落地
关于数据落地:MySQL VS NoSQL(可以理解为所有非关系型数据库 追求性能、高可用、分布式,对事务支持不是很好)
事务机制依然是目前最可靠的落地方案
11、MySQL实现秒杀的难点分析
"竞争":事务+行级锁(等待:如果高效的处理竞争)
12、秒杀功能
秒杀接口暴露(防止提前暴露秒杀地址)
执行秒杀
相关查询
13、代码开发阶段
DAO设计编码
Service设计编码
Web设计编码
14、数据库编码
--MySQL数据库使用5.7版本--
使用之前启动服务
net start MYSQL57 (MYSQL57 是安装MySQL是设置的)
新建sql包并新建schema.sql
手写代码
注意对表属性设置的方法
ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT="秒杀库存表";
创建索引
PRIMARY KEY (seckill_id),
key idx_start_time(start_time),
key idx_create_time(create_time),
初始化数据(values可以连续写)
insert into seckill(name,number,start_time,end_time)
values
('1000元秒杀iPhone6',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'),
('500元秒杀iPad2',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'),
('300元秒杀小米4',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'),
('200元秒杀红米note',100,'2015-11-01 00:00:00','2015-11-02 00:00:00'),
/*联合主键*/
PRIMARY KEY(seckill_id,user_phone),
查看创建表的语句包括语句
show create table seckill\G
15、开始DAO层实体和接口编码
Table => Entity
在java包下新建org.seckill.entity org.seckill.dao
继续创建entity下的实体Seckill SuccessKilled
继续新建dao包下的接口 SeckillDao SuccessKilledDao
16、基于MyBatis实现DAO
数据库 <=> 映射 <=> 对象
MyBatis HIBERNATE 都是工作在映射这一层 ORM(Object Relational Mapping 对象关系映射)
特点:参数 + SQL = Entity/List
SQL写在哪? XML提供SQL(推荐) 注解提供SQL
如何DAO接口? Mapper自动实现DAO接口(推荐) API编程方式实现DAO接口
17、使用MyBatis编码
首先在resource文件夹下新建mybatis全局配置文件 mybatis-config.xml
再创建一个目录 mapper,放mybatis映射的文件
http://www.mybatis.org/mybatis-3/zh/getting-started.html(中文)
http://www.mybatis.org/mybatis-3/getting-started.html
<?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>
<setting name="" value="" />
</settings>
</configuration>
18、在mapper在新建SeckillDao.xml 注意命名规范
添加
<mapper namespace="org.seckill.dao.SeckillDao"> ---指定命名空间,就是指定具体位置
<!-- 目的:为dao接口方法提供sql语句配置 -->
<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>
</mapper>
这里的参数都是直接来源于org.seckill.dao.SeckillDao中的接口方法
resultType="Seckill" parameterType="long" 多个参数的话就不要指定?
一般情况下,指定类型需要指定包名+类名,这里直接使用类名,还需要配置一下
19、
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!-- 根据id查询SuccessKilled并携带秒杀产品对象 -->
<!-- 如果告诉MyBatis把结果映射到SuccessKilled同时映射seckill属性 -->
<!-- 可以自由控制SQL -->
select
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",---Mybatis可以省略as,加""为了防止执行混淆
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};
</select>
20、MyBatis整合Spring
整合目标
--更少的编码:
只写接口,不写实现.
接口本身能说明很多事(参数 方法名(行为) 返回类型) => SQL
--更少的配置
别名:org.seckill.entity.Seckill => Seckill
(package scan => Seckill,Killed....)
配置扫描:
<mapper resource="mapper/SeckillDao.xml"/> => 自动扫描配置文件
dao实现:
<bean id="ClubDao" class="...ClubDao"/> => 自动实现DAO接口/自动注入Spring容器
--足够的灵活性
自己定制SQL
只有传参
结果集自动赋值
XML提供SQL DAO接口Mapper
21、开始配置整合mybatis过程
在resource下新建spring,并生成spring-dao.xml
http://docs.spring.io/spring/docs/current/spring-framework-reference/
下载pdf后搜 7.2 Container overview
这个头部信息不够完整,实测可运行的如下
<?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">
</beans>
22、开始单元测试
(先确认数据库服务已经启动)
新建test.java.org.seckill.dao 并新建SeckillDaoTest.java
23、注意
// org.apache.ibatis.binding.BindingException: Parameter 'offset' not found. Available parameters are [0, 1, param1, param2]
// List<Seckill> queryAll(int offset,int limit);
// java 没有保存新参的记录queryAll(int offset,int limit)->queryAll(arg0,arg1)
//改成 List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
// (多个参数时需要配置)
第一期结束
24、Java高并发秒杀API之Service层开始 -2017-05-20
DAO编码之后的思考:DAO层工作演变为:接口设计+SQL编写
DAO(Data Access Object) 数据访问对象是一个面向对象的数据库接口
代码和SQL的分离,方便Review
DAO拼接等逻辑在Service层完成
25、在org.seckill中新建service和exception包
再创建dto包:数据传输层 (web和service之间的数据传递,entity是业务相关)
26、在service下新建SeckillService的接口
业务接口:站在"使用者"的角度设计接口
1、方法定义粒度 2、参数 3、返回类型(return 类型/异常)
在dto中新建Exposer类 -> 暴露秒杀地址DTO
在dto中新建SeckillExecution类 -> 封装秒杀执行后结果
在exception中新建RepeatKillException类 extends RuntimeException -> 重复秒杀异常
在exception中新建SeckillCloseException类 extends RuntimeException -> 秒杀关闭异常
在exception中新建SeckillException类 extends RuntimeException -> 秒杀相关业务的异常
将前两个异常继承改成SeckillException
27、写接口的实现
在service下新建impl,并新建SeckillServiceImpl 实现SeckillService
并具体实现对应的方法
生成md5 private String getMD5(long seckillId) {
String base = seckillId + "/" + salt;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());//DigestUtils有Spring提供
return md5;
}
//所有编译期异常转换成运行期异常? //运行期异常会rollback
} catch (SeckillCloseException e1) {//先catch一些异常
throw e1;
} catch (RepeatKillException e2) {
throw e2;
} catch (Exception e) {
logger.error(e.getMessage(), e);
//所有编译期异常转换成运行期异常 运行期异常会rollback
throw new SeckillException("seckill inner error" + e.getMessage());
}
28、新建enums包存放常量
SeckillStateEnum
注意枚举中可是使用 values()得到所有的枚举常量
29、基于Spring管理Service依赖
Spring IOC功能理解:依赖注入 控制反转(Inversion of Control,英文缩写为IoC)
对象工厂 + 依赖管理 -> 一致的访问接口
业务对象依赖图
↗ SeckillDao ↘ //体现在SeckillServiceImpl.java
SeckillService < > SqlSessionFactory
↘ SuccessKilledDao ↗ ↓
↓
DataSource...
为什么使用IOC
对象创建统一托管
规范的生命周期管理
灵活的依赖注入
一致的获取对象(默认都是单例singleton)
Spring-IOC注入方式和场景
XML:1、Bean实现来自第三方类库,如:DataSource等;2,需要命名空间配置,如:context,aop,mvc等
注解:项目中自身开发使用的类,可直接在代码中使用注解如:@Service,@Controller等
Java配置类:需要通过代码控制对象创建逻辑的场景 如:自定义修改依赖类库
本项目IOC使用
XML配置 -> package-scan -> Annotation注解
30、在spring包下新建spring-service.xml
new -> XML Configuration File -> Spring Config -> 创建并自动生成头部信息
但是头部信息好像不全,所以最好还是复制。
<context:component-scan base-package="org.seckill.service"/>
<!--扫描service包下所有使用注解的类型-->
再到service实现类中添加注解
SeckillServiceImpl.java
//@Component(统称) @Service @Dao @Controller
@Autowired // @Resource @Inject
31、Spring声明式事务
什么是生命式事务(解脱事务代码,交给第三方框架)
开始事务 -> 修改SQL1 -> 修改SQL2 -> 修改SQL3 -> 修改SQLn -> 提交/回滚事务
声明式事务使用方式
ProxyFactoryBean + XML --> 早期使用方式(2.0)
tx:advice+aop命名空间 --> 一次配置永久生效
注解 @Transactional --> 注解控制(推荐)
事务方法嵌套
声明式事务独有的概念
传播行为 -> propagation_required(新事务加入到原有事务中,没有就创建新事务)
...
什么时候回滚事务
抛出运行期异常(RuntimeException)
小心不当的try-catch(Spring没有感觉到异常)
32、配置Spring声明式事务
在spring-service.xml中配置
然后再SeckillServiceImpl.java中的executeSeckill()方法添加@Transactional
/**
*使用注解控制事务方法的优点
*1:开发团队一致约定,明确标注事务方法的编码风格
*2:保证事务方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求或者剥离到事务方法外部
*3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制(注:这个说法跟其他的视频介绍的优点不同)
*/
33、单元测试
配置Ctrl + Shift + 创建单元测试类
http://blog.csdn.net/jiaotuwoaini/article/details/52767007
在SeckillService.java的类名后Ctrl+Shift+T,创建单元测试类(未生效)
手动创建 org.seckill.service包和SeckillServiceTest类
给Test类添加注解
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({
"classpath:/spring/spring-dao.xml",
"classpath:/spring/spring-service.xml"})
注意多个xml实验数组(Java的数据初始化是{})
slf4j的日志工具
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
private Logger logger = LoggerFactory.getLogger(this.getClass());
相关配置 https://logback.qos.ch/manual/configuration.html
在resources下新建logback.xml
复制(并加入XML头)
<configuration debug="true">
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are by default assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
34、执行测试
testExportSeckillUrl()注意seckill表确定数据中数据(开始时间、结束时间)
logger.info("seckillExecution----------------{}", seckillExecution);
//返回的一直是null 和 {},具体问题原因未知,toSting()都写了
新增一个单元测试方法合并
testExportSeckillUrl()
testExecuteSeckill()
为testSeckillLogic()
Java高并发秒杀API之Service层 2017-05-20结束
35、Java高并发秒杀API之Web层 2017-05-20开始
前端交互设计
Restful
SpringMVC
Bootstrap+jQuery
前端页面流程:
列表页 → 详情页
↓
登录操作 ← login
↓ ↓
写入Cookie → 展示逻辑
详情页流程逻辑:
1:获取标准系统时间
↓
4: 3: ← 倒计时 ← 2:时间判断
执行 ← 秒杀 开始时间 秒杀
秒杀 地址 ← 结束时间 → 结束
↓
结束
36、什么是Restful
兴起于Rails 一种优雅的URI表述方式 资源的状态和状态转移
GET /seckill/list
POST /seckill/execute/{seckillId} ╳
/seckill/{seckillId}/execution √(资源的状态)
DELETE /seckill/{id}/delete
GET -> 查询操作
POST -> 添加/修改操作
PUT -> 修改操作
DELETE -> 删除操作
URL设计
/模块/资源/{标示}/集合1/...
/user/{uid}/friends -> 好友列表
/user/{uid}/followers -> 关注着列表
秒杀API的URL设计
GET /seckill/list 秒杀列表
GET /seckill/{id}/detail 详情页
GET /seckill/time/now 系统时间
POST /seckill/{id}/exposer 暴露秒杀
POST /seckill/{id}/{md5}/execution 执行秒杀
37、使用SpringMVC框架理论
围绕Handlder开发
数据Model
Handler →
页面View
38、HTTP请求地址映射原理
Servlet容器(Tomcat,Jetty等)
Http请求 → SpringMVC Handler Mapping注解,XML编程等 → Handler处理方法
注解映射技巧
@RequestMapping注解:
(1)支持标准的URL
(2)Ant风格URL (?和*和**等字符)
(3)带(xxx)占位符的URL.
例如
/user/*/creation
匹配/user/aaa/creation、/user/bbb/creation等URL
/user/**/creation
匹配/user/creation、/user/aaa/bbb/creation等URL
/user/{userId}
匹配/user/123、/user/abc等URL
/company/{companyId}/user/{userId}/detail
匹配/company/123/user/456/detail等URL
请求方法细节处理
1、请求参数绑定
2、请求方式限制
3、请求转发和重定向
4、数据模型赋值
5、返回json数据
6、cookie访问
例 子
@RequestMapping(value = "/{seckill}/detail",method=RequestMethod.GET)
public String detail(@PathVariable("seckill") Long seckillId,Model model){
if(seckillId==null){
return "redirect:/seckill/list";
}
Seckill seckill = seckillService.getById(seckill);
if(seckill==null){
return "forward:/seckill/list";
}
model.addAttribute("seckill",seckill);//model
return "detail";//view
}
返回json数据
@RequestMapping(value = "/{seckill}/detail",method=RequestMethod.POST,produces={
"application/json;charset=UTF-8"
})
@ResponseBody
public SeckillResult<Exposer> exportSeckillURL(@PathVariable("id") long id){
SeckillResult<Exposer> result;
try{
Exposer exposer=...
}catch(Exception e){
logger.error(e.getMessage(),e);
result = new SeckillResult<Exposer>(false,e.getMessage());
}
return result;
}
Cookie访问
@RequestMapping(value="/{seckillId}/{md5}/execution",method=RequestMethod.POST)
public SeckillResult<SeckillExecution> execute(
@PathVariable("seckillId") long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value="killPhone",required=false) Long phone){
if(phone == null){
return new SeckillResult<SeckillExecution>(false,"未注册电话");
}
SeckillExecution execution = seckillService.executeSeckillKillByProcedure(seckillId,phone,md5);
SeckillResult<SeckillExecution> result = new SeckillResult<SeckillExecution>(true,execution);
return result;
}
39、整合配置SpringMVC框架
首先打开 webapp/WEB_INF/web.xml
整合顺序 MyBatis -> spring -> springMVC
新建spring-web.xml
注意这条命名空间不要选错
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
40、实现Restful的API
新建org.seckill.web包并新建SeckillController包
@Controller // @Service @Component
@RequestMapping("/seckill")// URL:/模块/资源/{id}/细分 /seckill/list
@RequestMapping(value = "/list", method = RequestMethod.GET)
public String list(Model model) {
//list.jsp+model=ModelAndView
return null;
}
并写好详情页的接口
然后写返回秒杀地址的ajax接口,返回json
在dto中新建SeckillResult<T> 泛型 用于封装json结果
//重点理解如何转换成json的
---暴露秒杀地址---
//ajax 返回json
@RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(Long seckillId) {
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<Exposer>(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult<Exposer>(false, e.getMessage());
}
return result;
}
---执行秒杀接口---
@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST,
produces = {"application/json;charset:UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(
@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "killPhone", required = false) Long phone) {
//这里设置 required = false防止报错,验证放在程序中
//也可使用springMVC valid来验证 适合很多参数
if (phone == null) {
return new SeckillResult<SeckillExecution>(false, "未注册");
}
SeckillResult<SeckillExecution> result;
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult<SeckillExecution>(true, seckillExecution);
} catch (RepeatKillException e) {//注意处理异常的优先级
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(false, execution);
} catch (SeckillCloseException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
return new SeckillResult<SeckillExecution>(false, execution);
} catch (Exception e) {
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(false, execution);
}
}
---获取系统时间---
完成public SeckillResult<Long> time() now.getTime());//注意这里需要getTime()
41、
使用bootstrap开发前端页面
环境安装http://www.runoob.com/bootstrap/bootstrap-environment-setup.html
拷贝HTML模板,新建jsp/list.jsp jsp/detail.jsp
将要CDN改成推荐版本(链接) 去掉css和主题,并将引用改到</body>后面
新建common文件夹存放通用的jsp, /common/head.jsp ---静态包含---(动态包含会有多个servlet)
新建common文件夹存放通用的jsp, /common/tag.jsp
完成list.jsp
42、
运行测试
不能连接数据库要注意执行 net start mysql启动数据库服务
http://localhost:8080/seckill/list
修改网页代码后也需要重新编译
Build->Build Artifacts->选择一个war, 不重启服务器
43、
在detail.jsp中实现cookie登录交互,首先完成相应的布局
从http://www.bootcdn.cn/获取jQuery cookie操作插件和countDown倒计时插件
开始编写交互逻辑
在webapp下新建resources/script/seckill.js
$.post(killUrl, {}, function (result) {
}
);//post请求,但是把参数都放在url上面了.
针对重复秒杀的情况 出现系统异常的恢复 json不显示
44、
第三期:Java高并发秒杀API之Web层 2017-05-31号完成
第四期:Java高并发秒杀API之高并发优化 2017-06-01号开始
可能出现高并发的点
详情页
系统时间
地址暴露接口
执行秒杀操作
为什么要单独获取系统时间
用户大量刷新->CDN(detail页静态化 静态资源css,js等)->秒杀系统
获取系统时间不用优化
访问一次内存(Cacheline)大约10ns(1秒钟可执行1亿次)
秒杀地址接口分析
无法使用CDN,适合服务端缓存:redis等,一致性维护成本低
秒杀地址接口优化
请求地址->redis->Mysql(两者一致性维护:超时穿透/主动更新)
秒杀操作优化分析
无法使用cdn,后端缓存困难:库存问题,一行数据竞争:热点商品
其他方案分析
执行秒杀
原子计数器 -> 技术 -> redis/NoSql
|
记录行为消息 -> 技术 -> 分布式MQ
|
消费消息并落地 -> 技术 -> MySql
成本分析:运维成本和稳定性:NoSQL,MQ等 开发成本:数据一致性,回滚方案等,幂等性难保证:重复秒杀问题,不适合新手
为什么不用MySQL解决?认为MySql低效。MySql真的低效?
测试:一条update压力测试(约4wQPS)
Java控制事务行为分析
sql->insert购买明细->等待行锁......
瓶颈分析:
update减库存-> 网络延迟、GC -> insert购买明细 -> 网络延迟、GC -> commit/rollback
优化分析:
行级锁是在Commit之后释放 -> 优化方向减小行级锁持有时间 ->
延迟分析
同城机房:
异地机房:北京到上海总距离1300公里,真空中30w公里/s,光在玻璃的传播速度是真空中的2/3,
往返距离1300*2=2600公里,1300* 2/(300000 * 2 / 3)=13毫秒
实际在20毫秒左右,QPS最好只有50(只考虑一次网络连接时间)
如何判断Update更新库存成功
两个条件:1、Update自身没有错 2、客户端确认Update影响记录数
优化思路:把客户端逻辑放到MySql服务器,避免网络延迟和GC影响
如何放在Mysql服务器?
两种解决方案:
-定制SQL方案:update/*+[auto_commit]*/ 需要修改MySql源码
-使用存储过程:整个事务在MySql端完成
优化总结
前端控制:暴露接口、按钮防重复
动静态数据分离:CDN缓存(好像有API推送),后端缓存(redis、memcache)
事务竞争优化:减少事务锁时间(好像是把几条语句放在一起执行,相当于一条语句执行,(一秒钟可以执行几万次)
这样同时减少了网络连接过程中的延迟时间)
45、开始秒杀优化
redis后端缓存优化
使用redis优化"地址暴露接口"
redis官方不支持windows,但是window开放技术团队开发支持了redis的应用
本次测试在linux下配置redis服务
下载->解压->进入->make->make install->redis-server->redis-cli -p 6379(默认端口可省略)->info 查看运行情况
打开pom.xml,引入Java访问redis客户端的jar包https://redis.io/clients
注意redis-server 和 redis-cli 没有空格
打开SeckillServiceImpl.java
开始优化exportSeckillUrl()
在dao中新建cache包和RedisDao类
配置protostuff序列化依赖(空间、速度、cpu)
Java序列化性能对比https://github.com/eishay/jvm-serializers/wiki
protostuff的简单实用http://blog.csdn.net/canot/article/details/53750443
RedisDaoTest的单元测试
spring-dao.xml中配置
<!--RedisDao-->
<bean id="redisDao" class="org.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="192.168.0.107"/><!--ifconfig localhost 192.168.0.107-->
<constructor-arg index="1" value="6379"/>
<!--JedisDataException: DENIED Redis is running in protected mode because protected mode is....
http://blog.csdn.net/u010003835/article/details/52853574
先redis-cli ,然后执行CONFIG SET protected-mode no-->
</bean>
执行dbsize查看已经写入的缓存条数
执行keys * 查看所有的key
执行get xxxkey 查看被序列化的值
单元测试通过:
开始优化exportSeckillUrl()方法
46、
简单优化
调整减库存和插入购买明细操作的顺序
insert购买明细 -> 网络延迟/GC -> update减库存rowLock -> 网络延迟/GC -> commit/rollback freeLock
进入SeckillServiceImpl.java 中的executeSeckill()
//先屏蔽一部分重复秒杀的请求
深度优化
事务SQL在MySQL端执行(存储过程) 减少网络延迟的影响,提高TPS
在sql包中创建sckill.sql
连接数据库mysql -uadmin -padmin -Dseckill
分别执行
DELIMITER $$ 和 定义存储过程 部分
再执行show create procedure execute_seckill\G
执行DELIMITER ; --还原
执行set @r_result=-3; 并查看一下数据 select @r_result;
执行存储过程前查看数据 select * from seckill where seckill_id=1001\G
mysql> select * from seckill where seckill_id=1001\G
*************************** 1. row ***************************
seckill_id: 1001
name: 500元秒杀iPad2
number: 198
start_time: 2017-06-01 23:33:35
end_time: 2017-07-19 00:00:00
create_time: 2017-05-02 22:27:23
1 row in set (0.00 sec)
查看秒杀成功表中的1003的数据或者直接删除所有1003的数据
select * from success_killed;
delete from success_killed where seckill_id = 1001;
执行存储过程 call execute_seckill(1001,13502172008,now(),@r_result);
查看输出 select @r_result; 返回 1
查看成功记录表 select * from success_killed;
继续执行存储过程 并查看返回值,返回 -1,并确定成功记录表和商品数量表中数据没有变化
总结
-- 存储过程
-- 1:存储构成优化:事务行级锁私持有时间
-- 2:不要过度依赖存储过程
-- 3: 简单的逻辑可以应用存储过程(银行业使用,一般的互联网应用不用)
-- 4:QPS:一个秒杀单6000/qps
47、seckill项目调用存储过程代码实现
SeckillService.java中定义一个新接口executeSeckillProcedure(),不需要抛出异常
并在SeckillServiceImpl.java中实现这个接口
会在SeckillDao.java中新建void killByProcedure(Map<String,Object> paramMap);
并在SeckillDao.xml中写sql
继续实现接口(其中会添加依赖 commons-collections MapUtils获得工具)
写单元测试testExecuteSeckillProcedure()
注意SeckillDao.xml中的call execute_seckill,
这个"存储过程"就是seckill.sql中创建并在命令行中执行的,而不是后期在代码中实现!!!
通过Navicat for MySQL工具连接数据库后,数据库中并没有execute_seckill表,而是有一个execute_seckill函数
单元测试通过后修改SeckillController中的execute()方式
调用执行秒杀调用executeSeckillProcedure()
重新编译后启动web测试效果
48、大型系统部署架构
系统会用到哪些服务?
CDN
WebServer:Nginx+Jetty
Redis
MySQL
系统部署的架构应该是什么样的?
智能DNS解析 ← 流量
Nginx Nginx ↓
↓ CDN缓存
逻辑集群
Jetty Jetty Jetty → 缓存集群
↓ Redis Redis
↓mod(seckillId)
分库分表 → 统计分析
DB-1 DB-2 DB-3
分库分表框架:阿里巴巴阿里巴巴中间件TDDL
可能参与的角色?
开发:前端+后端
测试
DBA
运维:机器、监控、初始化脚本、Nginx之间配置
49、课程总结
---数据库设计和实现
---MyBatis理解和使用技巧
---MyBatis整合Spring技巧(自动包扫描、sql文件扫描、别名识别,做到一次配置之后不需要修改)
业务层技术回顾
---业务接口设计和封装(站在使用者的角度去设计)
---SpringIOC配置技巧:声明式事务:xml配置,自己开发的dao、service、controller:使用注解
---Spring声明式事务使用和理解:什么情况下回滚,什么情况下提交
Web技术回顾
---Restful接口运用(规范和理解:描述一个资源,通过不同的提交方式来达到描述行为的目的,写通过POST,读通过GET)
---SpringMVC使用技巧 (如何配置 参数如何映射 如何打包成json返回给浏览器
(返回json是通过Spring框架(包package org.springframework.web.bind.annotation;),而不是Restful,Resultful只是一种设计规范))
---前端交互分析过程
---Bootstrap和JS使用
---系统瓶颈点分析
---事务,锁,网络延迟理解(还有GC),
---前端,CDN,缓存等理解使用
---集群化部署
2017-06-06 21:11 学习结束