Java高并发秒杀系统(二)

来源:互联网 发布:hadoop书籍推荐知乎 编辑:程序博客网 时间:2024/06/05 19:41

  • 秒杀系优化分析
    • 1 详情页面
    • 2系统时间
    • 3地址暴露接口
    • 4执行秒杀操作
  • 秒杀系统优化实现
    • 1 秒杀地址接口优化
  • 秒杀系统的部署
    • 1 系统用到的服务
    • 2请求处理的流程
  • 秒杀系统优化总结
  • 资源地址

本文主要对秒杀系统在大并发的场景下性能瓶颈的做一个分析,以及秒杀系统的优化实现。秒杀系统的业务分析和系统实现,可以参考上一篇文章 Java高并发秒杀系统(一)

1.秒杀系优化分析

下图列出秒杀的系统流程,其中红色部分是可能发生高并发的地方,绿色则表示没有影响。
这里写图片描述

1.1 详情页面

  1. 详情页面放在CDN
    秒杀还未开始,大量的用户不断的点击刷新页面。我们将详情页静态化放到CDN,这样访问detail页面就不会落到我们的业务系统,可以减轻系统的压力。
    这里写图片描述

  2. CDN是什么?
    CDN是Content Delivery Network(内容分发网络)的缩写,是可以加快用户获取数据的系统。一般将一些不经常变化的资源放到CDN,比如:静态资源(图片、HTML、CSS、JavaScript)、视频文件等,用户访问到CDN上的资源后就不用请求应用系统,不但减轻了系统的压力还可以提升用户的访问速度。
    这里写图片描述

1.2系统时间

  1. 为什么单独提供获取系统时间接口?

    系统部署的时候会将秒杀详情页面放到CDN,用户访问页面的时候不用访问我们的系统,所以也就拿不到系统时间,因此提供一个获取服务器系统时间的接口。
  2. 获取系统时间接口不需要优化

    获取系统时间接口不需要优化,是因为这个接口操作是通过访问内存实现的,访问一次内存大约花费10ns。内存操作速度很快,接口只有一个 new Date() ,基本上可以不用考虑GC,1s可以执行10亿次操作。

1.3地址暴露接口

  1. 能否放到CDN缓存?

    秒杀地址接口无法使用CDN服务,因为CDN适用于请求资源不会变化的。而秒杀地址接口返回的数据会发生变化的,因此不适合放在CDN缓存。
  2. 秒杀地址接口优化思路

    可以考虑将秒杀暴露接口返回的结果放到redis中,设置过期时间保证数据的一致性,或者可以在数据发生变化时通知redis将相应的数据进行更新。

    这里写图片描述

1.4执行秒杀操作

  1. 秒杀其他方案分析

    1)通过原子计数器记录商品的库存,一般采用redis或者其他NoSQL来保证库存数的原子性。2)记录成功后,将购买记录的行为消息发送到分布式消息队列中。3)后端系统从消息队列中消息消息,将相应数据修改落地到MySQL4)优点:方便扩展、伸缩性好,能够抗住非常高的并发。

    大型的互联网公司基本都采取这种方案。
    这里写图片描述

    使用这套方案的成本非常高,不管是运维成本还是开发成本,需要技术人员对这些技术有比较深入的了解。 

    这里写图片描述

  2. 如何判断秒杀操作成功?
    这里写图片描述

  3. 秒杀操作瓶颈分析

    秒杀对应数据库中就是减库存操作和插入购买记录这两步操作,数据库使用行级锁来保证操作的原子性,这也就导致秒杀变成了串行操作。

    这里写图片描述

    由于应用系统和MySQL数据库经常不是部署在同一台机器上,所以数据库操作的都要经过网络传输,这就产生了网络延迟问题,通过还伴随着GC操作。

    这里写图片描述

    1)网络延迟分析
    下面分别对同城机房和异地机房进行分析:
    这里写图片描述

    这里写图片描述

    2)减少锁的持有时间
    这里写图片描述

  4. 秒杀优化方案
    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错误/未执行修改sqlCREATE 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
    秒杀系统代码

原创粉丝点击