慕课网《Java高并发秒杀API之高并发优化》学习总结
时间:2017年08月25日星期五
说明:本文部分内容均来自慕课网。@慕课网:http://www.imooc.com
教学源码:无
学习源码:https://github.com/zccodere/study-imooc
第一章:优化分析
1-1 优化分析(上)
学习本门课程前请先学习前置课程《Java高并发秒杀API之业务分析与DAO层》和《Java高并发秒杀API之Service层》和《Java高并发秒杀API之web层》
首先搞清楚高并发发生在哪?
详情页
CDN的理解
CDN(内容分发网络)加速用户获取数据的系统
部署在离用户最近的网络节点上
命中CDN不需要访问后端服务器
互联网公司自己搭建或租用
秒杀地址接口分析
无法使用CDN缓存
适合服务端缓存redis等
一致性维护成本低
秒杀地址接口优化
秒杀操作优化分析
无法使用CDN缓存
后端缓存困难:库存问题
一行数据竞争:热点商品
1-2 优化分析(下)
秒杀其他方案分析
成本分析
运维成本和稳定性:NoSQL、MQ等
开发成本:数据一致性、回滚方案等
幂等性难保证:重复秒杀问题
不适合新手的架构
Java控制事务行为分析
瓶颈分析
优化分析
行级锁在Commit之后释放
优化方向减少行级锁持有时间
如何判断Update更新库存成功
两个条件
Update自身没报错
客户端确认Update影响记录数
优化思路
把客户端逻辑放到MySQL服务端,避免网络延迟和GC影响
如何放到MySQL服务端:两种解决方案
定制SQL方案:update /*+[auto_commit]*/,需要修改MySQL源码
使用存储过程:整个事务在MySQL端完成
优化总结
前端控制:暴露接口,按钮防重复
动静态数据分离:CDN缓存,后端缓存
事务竞争优化:减少事务锁时间
第二章:缓存优化
2-1 后端缓存
代码编写
1.编写RedisDao类
package com.myimooc.seckill.dao.cache;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import com.myimooc.seckill.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
/**
* @describe Redis数据访问对象
* @author zc
* @version 1.0 2017-08-26
*/
public class RedisDao {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final JedisPool jedisPool;
public RedisDao(String ip,int port){
jedisPool = new JedisPool(ip,port);
}
private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
public Seckill getSeckill(long seckillId){
// redis操作逻辑
try {
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:"+seckillId;
// 并没有实现内部序列化操作
// get->byte[] ->反序列化 ->Object(Seckill)
// 采用自定义序列化
// protostuff : pojo
byte[] bytes = jedis.get(key.getBytes());
// 缓存中获取到
if(bytes != null){
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
// seckill 被反序列
return seckill;
}
}finally {
jedis.close();
}
} catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
public String putSeckill(Seckill seckill){
// set Object(Seckill) -> 序列化 ->byte[]
try {
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:"+seckill.getSeckillId();
byte[] bytes = ProtostuffIOUtil.toByteArray(seckill,schema,
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
// 超时缓存
int timeout = 60 * 60;// 1小时
String result = jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally {
jedis.close();
}
} catch (Exception e){
logger.error(e.getMessage(),e);
}
return null;
}
}
2.修改SeckillServiceImpl类 输出秒杀接口地址方法
/**
* 秒杀开启时输出秒杀接口地址,否则输出系统时间和秒杀时间
*
* @param seckillId
*/
public Exposer exportSeckillUrl(long seckillId) {
// 优化点:缓存优化:超时的基础上维护一致性
//1:访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
if(null == seckill){
//2:访问数据库
seckill = seckillDao.queryById(seckillId);
if(null == seckill){
return new Exposer(false,seckillId);
}else {
//3:放入redis
redisDao.putSeckill(seckill);
}
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
// 系统时间
Date nowTime = new Date();
if(nowTime.getTime() < startTime.getTime()
|| nowTime.getTime() > endTime.getTime()){
return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
}
// 转换特定字符串的过程,不可逆
String md5 = this.getMD5(seckillId);
return new Exposer(true,md5,seckillId);
}
第三章:并发优化
3-1 简单优化
回顾事务执行
简单优化
事务SQL在MySQL端执行(存储过程)
1.编写存储过程
-- 秒杀执行存储过程
DELIMITER $$ -- console ; 转换为 $$
-- 定义存储过程
-- 参数:in 输入参数;out 输出参数
-- row_count():返回上一条修改类型sql的影响行数
-- row_count():0 未修改数据;>0 修改的行数;<0 sql错误或未执行修改sql
CREATE PROCEDURE 'seckill'.'execute_seckill'
(in v_seckill_id bigint,in v_phone bigint,
in v_kill_time timestamp,out r_result int)
BEGIN
DECLARE insert_count int DEFAULT 0;
START TRANSACTION;
insert ignore into success_killed
(seckill_id,user_phone,create_time)
values(v_seckill_id,v_phone,v_kill_time);
select row_count() into insert_count;
IF(insert_count = 0) THEN
ROLLBACK;
set r_result = -1;
ELSEIF(insert_count < 0) THEN
ROLLBACK;
set r_result = -2;
ELSE
update seckill
set number = number-1
where seckill_id = v_seckill_id
and end_time > v_kill_time
and start_time < v_kill_time
and number > 0;
select row_count() into insert_count;
IF(insert_count = 0) THEN
ROLLBACK;
set r_result = 0;
ELSEIF(insert_count < 0) THEN
ROLLBACK;
set r_result = -2;
ELSE
COMMIT;
set r_result = 1;
END IF;
END IF;
END;
$$
-- 存储过程定义结束
DELIMITER ;
set @r_result=-3;
-- 执行存储过程
call execute_seckill(1003,13521542111,new(),@r_result);
select @r_result
-- 存储过程
-- 1:存储过程优化:事务行级锁持有的时间
-- 2:不要过度依赖存储过程
-- 3:简单的逻辑,可以应用存储过程
-- 4:QPS:一个秒杀单6000/qps
2.修改SeckillDao.xml
<!-- mybatis调用存存储过程 -->
<select id="killByProcedure" statementType="CALLABLE">
call execute_seckill(
#{seckillId,jdbcType=BIGINT,mode=IN},
#{phone,jdbcType=BIGINT,mode=IN},
#{killTime,jdbcType=TIMESTAMP,mode=IN},
#{result,jdbcType=INTEGER,mode=OUT}
)
</select>
3.修改SeckillServiceImpl类
/**
* 执行秒杀操作,存储过程
*
* @param seckillId
* @param userPhone
* @param md5
*/
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5){
if(md5 == null || !md5.equals(getMD5(seckillId))){
return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map<String,Object> map = new HashMap<String, Object>();
map.put("seckillId",seckillId);
map.put("phone",userPhone);
map.put("killTime",killTime);
map.put("result",null);
// 执行存储过程,result被赋值
try {
seckillDao.killByProcedure(map);
// 获取result
int result = MapUtils.getInteger(map,"result",-2);
if(result == 1){
SuccessSeckilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS,sk);
}else{
return new SeckillExecution(seckillId,SeckillStatEnum.stateOf(result));
}
}catch (Exception e){
logger.error(e.getMessage(),e);
return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROR);
}
}
第四章:系统部署
4-1 部署架构
系统可能用到哪些服务
CDN(内容分发网络)
WebServer:Nginx+Jetty
Redis:服务器端缓存
MySQL:数据存储服务
大型系统部署架构
可能参与的角色
开发:前端+后端
测试
DBA
运维
第五章:课程总结
5-1 课程总结
数据层技术回顾
数据库设计和实现
MyBatis理解和使用技巧
MyBatis整合Spring技巧
业务层技术回顾
业务接口设计和封装
SpringIOC配置技巧
Spring声明式事务使用和理解
WEB层技术回顾
Restful接口运用
SpringMVC使用技巧
前端交互分析过程
Bootstrap和js使用
并发优化
系统瓶颈点分析
事务、锁、网络延迟理解
前端CDN、缓存等理解使用
集群化部署