本文主要对秒杀系统在大并发的场景下性能瓶颈的做一个分析,以及秒杀系统的优化实现。秒杀系统的业务分析和系统实现,可以参考上一篇文章 Java高并发秒杀系统(一)
1.秒杀系优化分析
下图列出秒杀的系统流程,其中红色部分是可能发生高并发的地方,绿色则表示没有影响。
1.1 详情页面
详情页面放在CDN
秒杀还未开始,大量的用户不断的点击刷新页面。我们将详情页静态化放到CDN,这样访问detail页面就不会落到我们的业务系统,可以减轻系统的压力。
CDN是什么?
CDN是Content Delivery Network(内容分发网络)的缩写,是可以加快用户获取数据的系统。一般将一些不经常变化的资源放到CDN,比如:静态资源(图片、HTML、CSS、JavaScript)、视频文件等,用户访问到CDN上的资源后就不用请求应用系统,不但减轻了系统的压力还可以提升用户的访问速度。
1.2系统时间
为什么单独提供获取系统时间接口?
系统部署的时候会将秒杀详情页面放到CDN,用户访问页面的时候不用访问我们的系统,所以也就拿不到系统时间,因此提供一个获取服务器系统时间的接口。
获取系统时间接口不需要优化
获取系统时间接口不需要优化,是因为这个接口操作是通过访问内存实现的,访问一次内存大约花费10ns。内存操作速度很快,接口只有一个 new Date() ,基本上可以不用考虑GC,1s可以执行10亿次操作。
1.3地址暴露接口
能否放到CDN缓存?
秒杀地址接口无法使用CDN服务,因为CDN适用于请求资源不会变化的。而秒杀地址接口返回的数据会发生变化的,因此不适合放在CDN缓存。
秒杀地址接口优化思路
可以考虑将秒杀暴露接口返回的结果放到redis中,设置过期时间保证数据的一致性,或者可以在数据发生变化时通知redis将相应的数据进行更新。
1.4执行秒杀操作
秒杀其他方案分析
1)通过原子计数器记录商品的库存,一般采用redis或者其他NoSQL来保证库存数的原子性。 2)记录成功后,将购买记录的行为消息发送到分布式消息队列中。 3)后端系统从消息队列中消息消息,将相应数据修改落地到MySQL 4)优点:方便扩展、伸缩性好,能够抗住非常高的并发。
大型的互联网公司基本都采取这种方案。
使用这套方案的成本非常高,不管是运维成本还是开发成本,需要技术人员对这些技术有比较深入的了解。
如何判断秒杀操作成功?
秒杀操作瓶颈分析
秒杀对应数据库中就是减库存操作和插入购买记录这两步操作,数据库使用行级锁来保证操作的原子性,这也就导致秒杀变成了串行操作。
由于应用系统和MySQL数据库经常不是部署在同一台机器上,所以数据库操作的都要经过网络传输,这就产生了网络延迟问题,通过还伴随着GC操作。
1)网络延迟分析
下面分别对同城机房和异地机房进行分析:
2)减少锁的持有时间
秒杀优化方案
1)简单的优化:将insert语句和update语句调换位置,先执行insert语句,可以去除一些重复秒杀减掉一半的网络延迟和GC,目的是降低MySQL的行rowLock时间。
2)深度优化:将事务操作放到MySQL端执行(存储过程),减少网络延迟和GC的成本,实现方案有两种
2.秒杀系统优化实现
2.1 秒杀地址接口优化
- service层增加redis缓存
因为秒杀商品对象在秒杀活动期间一般不会发生变化的,所以可以在这里做一层缓存。先从缓存中获取秒杀商品对象,如果没有,则访问数据库拿到商品对象之后再放入redis中。如果有,则直接将秒杀地址返回。
public Exposer exportSeckillUrl(long seckillId) {
//优化点1:缓存优化:超时的基础上维护一致性
//1.访问redis
SecKill secKill = redisDao.getSeckill(seckillId);
if(secKill == null){
//2.访问数据库
secKill = secKillDao.queryById(seckillId);
if(secKill == null){
return new Exposer(false,seckillId);
}else{
//3.放入redis
redisDao.putSeckill(secKill);
}
}
Date startTime = secKill.getStartTime();
Date endTime = secKill.getEndTime();
//系统当前时间
Date noewTime = new Date();
if(noewTime.getTime() < startTime.getTime() || noewTime.getTime() > endTime.getTime()){
return new Exposer(false,seckillId,noewTime.getTime(),startTime.getTime(),endTime.getTime());
}
//转化特定字符串的过程,不可逆
String md5 = getMD5(seckillId);
return new Exposer(true,md5,seckillId);
}
- 将数据库操作放到MySQL端执行(存储过程)
创建存储过程,service层通过调用存储过程获取秒杀结果。存储过程定义如下,通过判断返回结果result值来判断,秒杀结果。
-- 秒杀执行存储过程
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, state, create_time)
VALUES (v_seckill_id,v_phone,0,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,18270919398,now(),@r_result);
-- 获取结果
SELECT @r_result;
-- 存储过程
-- 1.存储过程优化:事务行级锁持有时间
-- 2.不要过度依赖存储过程
-- 3.简单的逻辑可以应用存储过程
-- 4.QPS:一个秒杀单 6000/QPS
-- 查看存储过程定义:show create procedure execute_seckill\G
3.秒杀系统的部署
3.1 系统用到的服务
1. CDN:内容分发网络,加速用户获取数据,降低服务器请求量
2. WebServer:Nginx做http服务器以及Jetty服务器的反向代理
3. redis:缓存热点数据
4. MySQL:通过事务保证秒杀的一致性和完整性
3.2请求处理的流程:
1. 逻辑集群是我们开发的部分
4.秒杀系统优化总结
优化点
1.静态页面使用CDN缓存,实现动静态数据分离。 2.后端不经常变化的数据放入redis中进行缓存 3.将事务操作移到MySQL端:MySQL本地执行主键SQL可以达到4w QPS ,Java执行也很快,瓶颈主要存在于网络延迟以及GC的停顿操作。这里可以用存储过程,解决网络延迟以及GC停顿操作带来的问题。
并发优化
5.资源地址
慕课网视频地址
慕课网-Java高并发秒杀API之高并发优化github
秒杀系统代码