高并发处理,CDN,redis---31
高并发分析
详情页:
CDN的理解: (1)CDN (内容分发网络)加速用户获取数据的 系统 (2)部署在
离用户最近
的
网络节点
上 (3)
命中CDN
不需要访问
后端服务器
(4)互联网公司
自己搭建
或
租用
秒杀地址接口优化:
1.无法使用CDN缓存
2.适合服务端缓存:redis等
3.一致性维护成本低
秒杀操作优化分析:
1.无法使用CDN缓存
2.后端缓存困难:库存问题
3.一行数据竞争:热门商品
Mysql压力测试(4wQPS)
Java控制事务行为分析
瓶颈分析--java客户端执行sql事务,update有行级锁(有网络延迟和GC)影响速度
优化方向--减少行级锁持有时间
延迟分析
优化思路:把客户端逻辑放到MySQL服务器端,避免网络延时和GC影响;
放到MySQL服务端 两种方案:
1.定制SQL方案:update/* +[auto_commit]*/,需要修改MySQL源码
2.使用存储过程 :整个事务在MySQL端完成
优化总结:
redis后端缓存优化:地址暴露接口(超时维护:不变化的数据)
redis网址:www.redis.io
windows版:redis网址 https://github.com/MSOpenTech/redis/releases
用jedis Java客户端 操作java对象缓存的api方法 ,和谷歌的 protostuff序列化开源方案 (比Java原生的 序列化效率要高 )来 序列化和反序列化对象 ,缓存 对象必须转换为byte字节数组 存入 redis缓存 ,不像 memcached可以直接缓存对象 。
实际 操作思路 是初次查询 redis缓存 ,如果没有就从 数据库取出对象 放入redis缓存,下次取就会 直接从redis缓存取数据 了。但缓存有 超时时间限制 。
@pom.xml增加依赖
redis.clients
jedis
2.7.3
com.dyuproject.protostuff
protostuff-core
1.0.8
com.dyuproject.protostuff
protostuff-runtime
1.0.8
@RedisDao.java缓存操作文件
public class RedisDao {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
private final JedisPool jedisPool;
private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class);
public RedisDao (String ip,int port){
jedisPool = new JedisPool(ip, port);
}
public Seckill getSeckill(long seckillId){
//redis操作逻辑
try {
Jedis jedis = jedisPool.getResource();
try {
String key = "seckill:"+seckillId;
//并没有实现内部序列化操作
//get ->byte[] -> 反序列化 ->Object(Seckill)
//采用自定义序列化--开源社区protostiff
//protostiff:pojo.
byte[] bytes=jedis.get(key.getBytes());
//缓存获取到
if(bytes != null){
//空对象--待反序列赋值
Seckill seckill = schema.newMessage();
//反序列操作--将bytes根据schema转化为seckill对象
//比原生快2个数量级,空间是1/10~1/5
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
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;
}
}
@spring-dao.xml 注入编写(构造器注入)
@RedisDaoTest.java编写测试类
@RunWith(SpringJUnit4ClassRunner.class)
//告诉Junit spring配置文件
@ContextConfiguration(locations={"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {
private long seckillId = 1001;
@Autowired
private RedisDao redisDao;
@Autowired
private SeckillDao seckillDao;
@Test//get and put
public void testGetSeckill() {
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null) {
seckill = seckillDao.queryById(seckillId);
if(seckill != null){
String result = redisDao.putSeckill(seckill);
System.out.println("result"+result);
seckill = redisDao.getSeckill(seckillId);
System.out.println("seckill="+seckill);
}
}
}
}
@SeckilServiceImpl.java Service实现层改写(增加Radis缓存--减少访问数据库)
public Exposer exportSeckillUrl(long seckillId) {
//优化点:缓存优化:超时的基础上维护一致性(数据不变的情况,多并发)
//1:访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null) {
//2:访问数据库
seckill = seckillDao.queryById(seckillId);
if(seckill != null){
//3:放入redis
String result = redisDao.putSeckill(seckill);
System.out.println("result"+result);
seckill = redisDao.getSeckill(seckillId);
System.out.println("seckill="+seckill);
}else{
return new Exposer(false, seckillId);
}
}
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 = getMD5(seckillId);
return new Exposer(true, seckillId, md5);
}
Mysql事务优化:
瓶颈主要出现在网络延时和GC操作
简单优化:将update后移,减少rowLock时间。
@SeckillServiceImpl.java 秒杀事务优化
/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格。
*2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
*3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
*/
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {
System.out.println("执行executeSeckill。。。");
if (md5 == null || !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");
}
Date killTime = new Date();
// 执行秒杀逻辑:减库存+记录秒杀行为(一个事务(运行期),出现问题,回滚)
try {
// 记录购买行为
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
// 唯一验证:seckillId,userPhone
// 重复秒杀
if (insertCount <= 0){
throw new RepeatKillException("seckill repeated");
} else {
// 减库存 ,热门商品竞争
System.out.println("减库存。。。");
int updateCount = seckillDao.reduceNumber(seckillId, killTime);
System.out.println("减库存完成");
if (updateCount <= 0) {
//没有更新到记录,秒杀结束,rollback
throw new SeckillCloseException("seckill is closed");
} else {
// 秒杀成功 commit
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId,SeckillStatEnum.SUCCESS, successKilled);
}
}
} catch (SeckillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch (Exception e) {
logger.error(e.getMessage(), e);
// 所有编译期异常 转化为运行期异常
throw new SeckillException("seckill inner error" + e.getMessage());
}
}
}
深度优化:事务SQL在MySQL端执行(存储过程--使insert和update在Mysql端执行)
@seckill.sql存储过程源码
-- 秒杀执行存储过程
DELIMITER $$ -- 表示命令界定符设置为了$$ console ; 转换为 $$
-- 定义存储过程
-- 参数:in 输入参数; out 输出参数
-- row_count():返回上一条修改类型sql(delete,insert,update)的影响行数
-- 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(1001, 13631231234, now(), @r_result);
-- 获取结果
SELECT @r_result;
-- 存储过程
-- 1.存储过程优化:事务行级锁持有的时间
-- 2.不要过度依赖存储过程
-- 3.简单的逻辑可以应用存储过程
-- 4.QPS:一个秒杀单6000/qps
@SeckillService.java接口增加存储过程方法
/**
* 执行秒杀操作by存储过程
* @param seckillId
* @param userPhone
* @param md5
* @return SeckillExecution
*/
SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
@SeckillDao.java接口增加存储过程方法
/**
* 使用存储过程执行秒杀
* @param paramMap
*/
void killByprocedure(Map paramMap);
@SeckillDao.xml实现MyBatis调用存储过程
call execute_seckill(
#{seckillId,jdbcType=BIGINT,mode=IN},
#{phone,jdbcType=BIGINT,mode=IN},
#{killTime,jdbcType=TIMESTAMP,mode=IN},
#{result,jdbcType=INTEGER,mode=OUT}
)
@pom.xml加入common工具
commons-collections
commons-collections
3.2
@SeckillServiceImpl.java使用存储过程实现
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
return new SeckillExecution(seckillId, SeckillStateEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map map = new HashMap();
map.put("seckillId", seckillId);
map.put("phone", userPhone);
map.put("killTime", killTime);
map.put("result", null);
// 执行存储过程,result被赋值
try {
seckillDao.killByProcedure(map);
//commons-collections, 获取result
int result = MapUtils.getInteger(map, "result", -2);
if (result == 1) {
SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, sk);
} else {
return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
}
}
@SeckillController.java修改Controller
//使用存储过程调用
SeckillExecution seckillExecution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);