博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
Seckill秒杀系统高并发优化
阅读量:5826 次
发布时间:2019-06-18

本文共 5950 字,大约阅读时间需要 19 分钟。

hot3.png

        绝大多数秒杀系统都需要实现高并发,这样就必须在原来的项目基础上进行优化。简单的优化很有可能就会很大地提高系统的并发性能,但是这些优化往往是系统开发人员很少注意的,或者直接被人们忽略。因此要成为一个出色的开发人员,学会优化技巧与时刻具备系统优化的意识是必须的。

项目源码地址:

本项目秒杀业务核心SQL操作:

        先是UPDATE货存(货存减1),再是INSERT购买明细。中间可能会出现重复秒杀,秒杀结束,系统内部错误等异常,只要出现异常,事务就会回滚。

事务行为分析:

        当一个事务开启的时候拿到了表中某一行的行级锁,另一个事务进来数据库时发现锁住了同一行,若之前的事务不提交或回滚,这个行级锁不会被释放,后面进来的那个事务就要等待行级锁。当第一个事务提交或回滚后,行级锁被释放,第二个事务就能获得这个行级锁进行数据操作,多个事务以此类推,这些过程是一个串行化的操作,也是一个含有大量阻塞的操作。这是数据库或是绝大多数关系型数据库事务实现的方案。

秒杀系统瓶颈分析:

  1. 现在的事务实现方案是通过Spring的事务对秒杀业务核心进行管理。
  2. 系统目前的秒杀逻辑:java客户端发送UPDATE语句至MySQL服务端(虽然有网络延迟,但是各个事务并行),各事务开始竞争行级锁(阻塞开始),UPDATE执行后将UPDATE结果返回至java客户端(存在网络延迟与可能的GC操作),客户端判断如果执行成功,则发送INSERT购买明细的SQL语句至MySQL服务端再执行(存在网络延迟与可能的GC操作),将执行结果返回至java客户端(存在网络延迟与可能的GC操作),客户端再判断是否执行成功,如果成功,就告知MySQL提交事务(存在网络延迟)。
  3. 因此,阻塞的时间即从各事务在MySQL服务端竞争行级锁开始,一直到最后的事务提交,中间有4次的网络延迟以及java客户端的各种逻辑判断。这样事务的执行周期就会比较长。当排队的事务比较多的时候,系统性能就会呈指数级下降。

注:的GC操作:项目中DAO层各数据库操作类通过MyBatis实现的生成相应对象注入容器中,当使用后不再被使用时,就会进行垃圾回收。

项目优化分析:

通过分析事务的行为与秒杀系统瓶颈可以知道,要减少事务等待的时间,削弱阻塞的过程,就要想办法减少行级锁持有的时间。

  1. 优化思路一:持有行级锁是在UPDATE上(INSERT不涉及行级锁),释放锁是在Commit(客户端Spring控制),也就是锁持有时间是UPDATE和Commit之间。这个过程网络请求越少,锁持有时间就越短。
  2. 优化思路二:把客户端逻辑放在MySQL服务端(使用存储过程,整个事务在MySQL端完成),避免网络延迟与GC的影响,也没有java客户端的逻辑判断。

简单的并发优化(优化思路一):

这里写图片描述

分析:

参照优化思路一,持有行级锁在UPDATE上,INSERT不涉及行级锁(没INSERT之前根本不存在相应的行,更不可能会有行级锁)。因此可以先插入购买明细,这个过程虽然存在网络延迟,但是各个事务之间是可以并行的所以不需要等待,这样就可以减少各个事务一部分的等待与阻塞。实现减少MySQL row lock的持有时间。(但还是要把UPDATE库存的结果返回给客户端,客户端再决定是否提交事务,即还有2次网络延迟)

修改秒杀业务核心代码顺序后:

int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone,nowTime);            //唯一:seckillId,userPhone(联合主键)            if(insertCount<=0){                //重复秒杀                throw new RepeatKillException("seckill repeated");            }            else {                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);                if (updateCount <= 0) {                    //并发量太高,有可能在等行级锁的时候库存没有了,并且秒杀时间问题在前面已经验证。                    throw new SeckillCloseException("seckill is closed");                }                else {                    //秒杀成功                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);                    return new SeckillExecution(seckillId, SeckillStateEnums.SUCCESS, successKilled);  //枚举                }            }

深度优化(利用存储过程实现事务SQL在MySQL端执行):

  1. 参照优化思路二,利用存储过程将秒杀业务核心事务SQL放在MySQL端执行,这样就可以避免事务执行过程中的网络延迟与GC影响,事务行级锁持有时间几乎就是数据库数据操作的时间。大大削弱了事务等待的阻塞效应。

秒杀核心SQL事务存储过程:

DELIMITER //CREATE PROCEDURE excuteSeckill(IN fadeSeckillId INT,IN fadeUserPhone VARCHAR (15),IN fadeKillTime TIMESTAMP ,OUT fadeResult INT)  BEGIN    DECLARE insertCount INT DEFAULT 0;    START TRANSACTION ;    INSERT IGNORE success_killed(seckill_id,user_phone,state,create_time) VALUES(fadeSeckillId,fadeUserPhone,0,fadeKillTime);  --先插入购买明细    SELECT ROW_COUNT() INTO insertCount;    IF(insertCount = 0) THEN      ROLLBACK ;      SET fadeResult = -1;   --重复秒杀    ELSEIF(insertCount < 0) THEN      ROLLBACK ;      SET fadeResult = -2;   --内部错误    ELSE   --已经插入购买明细,接下来要减少库存      UPDATE seckill SET number = number -1 WHERE seckill_id = fadeSeckillId AND start_time < fadeKillTime AND end_time > fadeKillTime AND number > 0;      SELECT ROW_COUNT() INTO insertCount;      IF (insertCount = 0)  THEN        ROLLBACK ;        SET fadeResult = 0;   --库存没有了,代表秒杀已经关闭      ELSEIF (insertCount < 0) THEN        ROLLBACK ;        SET fadeResult = -2;   --内部错误      ELSE        COMMIT ;    --秒杀成功,事务提交        SET  fadeResult = 1;   --秒杀成功返回值为1      END IF;    END IF;  END//DELIMITER ;SET @fadeResult = -3;CALL excuteSeckill(8,13813813822,NOW(),@fadeResult);SELECT @fadeResult;

Java客户端(MyBatis)调用数据库存储过程:

首先,在Dao层新建一个接口:void killByProcedure(Map [泛型:String,Object] paramMap); 然后在相应的XML中配置实现(注意:jdbcType没有INT类型的枚举,要使用BIGINT;同样没有VARCHAR的枚举,要使用BIGINT代替。):

然后,Service层重新写入一个方法SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5);(注意:在使用MapUtils时要注入commons-collections 3.2依赖)

public SeckillExecution executeSeckillProcedure(int seckillId, String userPhone, String md5) {        if( md5==null || !md5.equals(getMD5(seckillId)) ){            return new SeckillExecution(seckillId,SeckillStateEnums.DATA_REWRITE);        }        Timestamp nowTime = new Timestamp(System.currentTimeMillis());        Map
map = new HashMap
(); map.put("seckillId",seckillId); map.put("phone",userPhone); map.put("killTime",nowTime); map.put("result", null); try{ seckillDao.killByProcedure(map); int result = MapUtils.getInteger(map,"result",-2); if(result == 1){ SuccessKilled sk = successKilledDao.queryByIdWithSeckill(seckillId,userPhone); return new SeckillExecution(seckillId,SeckillStateEnums.SUCCESS,sk); } else{ return new SeckillExecution(seckillId,SeckillStateEnums.stateOf(result)); } } catch (Exception e){ logger.error(e.getMessage(),e); return new SeckillExecution(seckillId,SeckillStateEnums.INNER_ERROR); } }

再者,在web-control层将调用方法改成executeSeckillProcedure,同时因为executeSeckillProcedure已经将重复秒杀,秒杀结束(无库存)合并到返回的SeckillExecution中,所以不用再捕获这两个异常(原本在service层要抛出这两个异常,是为了告诉Spring声明式事务该程序出错要进行事务回滚)

try{      SeckillExecution seckillExecution = seckillService.executeSeckillProcedure(seckillId,phone,md5);      return new SeckillResult
(true,seckillExecution);}catch (Exception e){ logger.error(e.getMessage(),e); SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStateEnums.INNER_ERROR); return new SeckillResult
(true,seckillExecution);}

最后,集成web层:

这里写图片描述 

这里写图片描述 
这里写图片描述

可见秒杀成功,重复秒杀,秒杀结束都正常进行!

转载于:https://my.oschina.net/u/3339803/blog/883179

你可能感兴趣的文章
OCP读书笔记(24) - 题库(ExamD)
查看>>
.net excel利用NPOI导入oracle
查看>>
$_SERVER['SCRIPT_FLENAME']与__FILE__
查看>>
hive基本操作与应用
查看>>
html5纲要,细谈HTML 5新增的元素
查看>>
Android应用集成支付宝接口的简化
查看>>
[分享]Ubuntu12.04安装基础教程(图文)
查看>>
django 目录结构修改
查看>>
win8 关闭防火墙
查看>>
CSS——(2)与标准流盒模型
查看>>
MYSQL 基本SQL语句
查看>>
C#中的Marshal
查看>>
linux命令:ls
查看>>
Using RequireJS in AngularJS Applications
查看>>
hdu 2444(二分图最大匹配)
查看>>
【SAP HANA】关于SAP HANA中带层次结构的计算视图Cacultation View创建、激活状况下在系统中生成对象的研究...
查看>>
DevOps 前世今生 | mPaaS 线上直播 CodeHub #1 回顾
查看>>
iOS 解决UITabelView刷新闪动
查看>>
CentOS 7 装vim遇到的问题和解决方法
查看>>
JavaScript基础教程1-20160612
查看>>