zoukankan      html  css  js  c++  java
  • 秒杀系统实现高并发的优化

    菜鸟拙见,望请纠正

    一:前言  

    先上代码看着代码学习效率更好:https://github.com/3218870799/Seckill

    高并发问题

      就是指在同一个时间点,有大量用户同时访问URL地址,比如淘宝双11都会产生高并发。

    高并发带来的后果

      • 服务端
          导致站点服务器、DB服务器资源被占满崩溃。
          数据的存储和更新结果和理想的设计不一致。
      • 用户角度
          尼玛,网站这么卡,刷新了还这样,垃圾网站,不玩了

    二:分析阻碍服务速度的原因

    1:事物行级锁的等待

    java的事务管理机制会限制在一次commit之前,下一个用户线程是无法获得锁的,只能等待

    2:网络延迟

    3:JAVA的自动回收机制(GC)

    三:处理高并发的常见方法

    1:首先可以将静态资源放入CDN中,减少后端服务器的访问

    2:访问数据使用Redis进行缓存

    3:使用Negix实现负载均衡

    4:数据库集群与库表散列

     四:实战优化秒杀系统

    1:分析原因

    当用户在想秒杀时,秒杀时间未到,用户可能会一直刷新页面,获取系统时间和资源(A:此时会一直访问服务器),当时间到了,大量用户同时获取秒杀接口API(B),获取API之后执行秒杀(C),指令传输到各地服务器,服务器执行再将传递到中央数据库执行(D),服务器启用事务执行减库存操作,在服务器端JAVA执行过程中,可能因为JAVA的自动回收机制,还需要一部分时间回收内存(E)。

    2:优化思路:

    面对上面分析可能会影响的过程,我们可以进行如下优化

    A:我们可以将一些静态的资源放到CDN上,这样可以减少对系统服务器的请求

    B:对于暴露秒杀接口,这种动态的无法放到CDN上,我们可以采用Redis进行缓存

    request——>Redis——>MySQL

    C:数据库操作,对于MYSQL的执行速度大约可以达到1秒钟40000次,影响速度的还是因为行级锁,我们应尽可能减少行级锁持有时间。

    DE:对于数据库来说操作可以说是相当快了,我们可以将指令放到MYSQL数据库上去执行,减少网络延迟以及服务器GC的时间。

    3:具体实现

    3.1:使用Redis进行缓存(Redis的操作可以参考我以前的博客https://www.cnblogs.com/nullering/p/9332589.html

    引入redis访问客户端Jedis

    1 <!-- redis客户端:Jedis -->
    2    <dependency>
    3         <groupId>redis.clients</groupId>
    4         <artifactId>jedis</artifactId>
    5         <version>2.7.3</version>
    6      </dependency>
    pom.xml

    优化暴露秒杀接口:对于SecviceImpl 中 exportSeckillUrl 方法的优化,伪代码如下

    get from cache      //首先我们要从Redis中获取需要暴露的URL

    if null    //如果从Redis中获取的为空

    get db    //那么我们就访问MYSQL数据库进行获取

    put cache   //获取到后放入Redis中

    else locgoin  //否则,则直接执行

    我们一般不能直接访问Redis数据库,首先先建立数据访问层RedisDao,RedisDao中需要提供两个方法,一个是  getSeckill  和  putSeckill

    在编写这两个方法时还需要注意一个问题,那就是序列化的问题,Redis并没有提供序列化和反序列化,我们需要自定义序列化,我们使用  protostuff  进行序列化与反序列化操作

    引入 protostuff 依赖包

     1       <!-- protostuff序列化依赖 -->
     2       <dependency>
     3           <groupId>com.dyuproject.protostuff</groupId>
     4           <artifactId>protostuff-core</artifactId>
     5           <version>1.0.8</version>
     6       </dependency>
     7       <dependency>
     8           <groupId>com.dyuproject.protostuff</groupId>
     9           <artifactId>protostuff-runtime</artifactId>
    10           <version>1.0.8</version>
    11       </dependency>
    pom.xml

    编写数据访问层RedisDao

     1 package com.xqc.seckill.dao.cache;
     2 
     3 import org.slf4j.Logger;
     4 import org.slf4j.LoggerFactory;
     5 
     6 import com.dyuproject.protostuff.LinkedBuffer;
     7 import com.dyuproject.protostuff.ProtostuffIOUtil;
     8 import com.dyuproject.protostuff.runtime.RuntimeSchema;
     9 import com.xqc.seckill.entity.Seckill;
    10 
    11 import redis.clients.jedis.Jedis;
    12 import redis.clients.jedis.JedisPool;
    13 
    14 /**
    15  * Redis缓存优化
    16  * 
    17  * @author A Cang(xqc)
    18  *
    19  */
    20 public class RedisDao {
    21     private final Logger logger = LoggerFactory.getLogger(this.getClass());
    22 
    23     private final JedisPool jedisPool;
    24 
    25     public RedisDao(String ip, int port) {
    26         jedisPool = new JedisPool(ip, port);
    27     }
    28 
    29     private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
    30 
    31     public Seckill getSeckill(long seckillId) {
    32         //redis操作逻辑
    33         try {
    34             Jedis jedis = jedisPool.getResource();
    35             try {
    36                 String key = "seckill:" + seckillId;
    37                 //并没有实现内部序列化操作
    38                 // get-> byte[] -> 反序列化 ->Object(Seckill)
    39                 // 采用自定义序列化
    40                 //protostuff : pojo.
    41                 byte[] bytes = jedis.get(key.getBytes());
    42                 //缓存中获取到bytes
    43                 if (bytes != null) {
    44                     //空对象
    45                     Seckill seckill = schema.newMessage();
    46                     ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
    47                     //seckill 被反序列化
    48                     return seckill;
    49                 }
    50             } finally {
    51                 jedis.close();
    52             }
    53         } catch (Exception e) {
    54             logger.error(e.getMessage(), e);
    55         }
    56         return null;
    57     }
    58 
    59     public String putSeckill(Seckill seckill) {
    60         // set Object(Seckill) -> 序列化 -> byte[]
    61         try {
    62             Jedis jedis = jedisPool.getResource();
    63             try {
    64                 String key = "seckill:" + seckill.getSeckillId();
    65                 byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
    66                         LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
    67                 //超时缓存
    68                 int timeout = 60 * 60;//1小时
    69                 String result = jedis.setex(key.getBytes(), timeout, bytes);
    70                 return result;
    71             } finally {
    72                 jedis.close();
    73             }
    74         } catch (Exception e) {
    75             logger.error(e.getMessage(), e);
    76         }
    77 
    78         return null;
    79     }
    80 
    81 
    82 }
    RedisDao.java

    优化ServiceImpl的 exportSeckillUrl 的方法

     1     public Exposer exportSeckillUrl(long seckillId) {
     2         // 优化点:缓存优化:超时的基础上维护一致性
     3         //1:访问redis
     4         Seckill seckill = redisDao.getSeckill(seckillId);
     5         if (seckill == null) {
     6             //2:访问数据库
     7             seckill = seckillDao.queryById(seckillId);
     8             if (seckill == null) {
     9                 return new Exposer(false, seckillId);
    10             } else {
    11                 //3:放入redis
    12                 redisDao.putSeckill(seckill);
    13             }
    14         }
    15 
    16         Date startTime = seckill.getStartTime();
    17         Date endTime = seckill.getEndTime();
    18         //系统当前时间
    19         Date nowTime = new Date();
    20         if (nowTime.getTime() < startTime.getTime()
    21                 || nowTime.getTime() > endTime.getTime()) {
    22             return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(),
    23                     endTime.getTime());
    24         }
    25         //转化特定字符串的过程,不可逆
    26         String md5 = getMD5(seckillId);
    27         return new Exposer(true, md5, seckillId);
    28     }
    29 
    30     private String getMD5(long seckillId) {
    31         String base = seckillId + "/" + salt;
    32         String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
    33         return md5;
    34     }
    ServiceImpl的exportSeckillUrl方法

     3.2 并发优化:

      在执行秒杀操作死,正常的执行应该如下:先减库存,并且得到行级锁,再执行插入购买明细,然后再提交释放行级锁,这个时候行级锁锁住了其他一些操作,我们可以进行如下优化,这时只需要延迟一倍。

     修改executeSeckill方法如下:

     1     @Transactional
     2     /**
     3      * 使用注解控制事务方法的优点:
     4      * 1:开发团队达成一致约定,明确标注事务方法的编程风格。
     5      * 2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部.
     6      * 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制.
     7      */
     8     public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
     9             throws SeckillException, RepeatKillException, SeckillCloseException {
    10         if (md5 == null || !md5.equals(getMD5(seckillId))) {
    11             throw new SeckillException("seckill data rewrite");
    12         }
    13         //执行秒杀逻辑:减库存 + 记录购买行为
    14         Date nowTime = new Date();
    15 
    16         try {
    17             //记录购买行为
    18             int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
    19             //唯一:seckillId,userPhone
    20             if (insertCount <= 0) {
    21                 //重复秒杀
    22                 throw new RepeatKillException("seckill repeated");
    23             } else {
    24                 //减库存,热点商品竞争
    25                 int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
    26                 if (updateCount <= 0) {
    27                     //没有更新到记录,秒杀结束,rollback
    28                     throw new SeckillCloseException("seckill is closed");
    29                 } else {
    30                     //秒杀成功 commit
    31                     SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
    32                     return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
    33                 }
    34             }
    35         } catch (SeckillCloseException e1) {
    36             throw e1;
    37         } catch (RepeatKillException e2) {
    38             throw e2;
    39         } catch (Exception e) {
    40             logger.error(e.getMessage(), e);
    41             //所有编译期异常 转化为运行期异常
    42             throw new SeckillException("seckill inner error:" + e.getMessage());
    43         }
    44     }
    ServiceImpl的executeSeckill方法

    3.3深度优化:(存储过程)

    定义一个新的接口,使用存储过程执行秒杀操作

    1     /**
    2      * 执行秒杀操作by 存储过程
    3      * @param seckillId
    4      * @param userPhone
    5      * @param md5
    6      */
    7     SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5);
    executeSeckillProcedure接口

    实现executeSeckillProcedure方法

     1     public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
     2         if (md5 == null || !md5.equals(getMD5(seckillId))) {
     3             return new SeckillExecution(seckillId, SeckillStatEnum.DATA_REWRITE);
     4         }
     5         Date killTime = new Date();
     6         Map<String, Object> map = new HashMap<String, Object>();
     7         map.put("seckillId", seckillId);
     8         map.put("phone", userPhone);
     9         map.put("killTime", killTime);
    10         map.put("result", null);
    11         //执行存储过程,result被复制
    12         try {
    13             seckillDao.killByProcedure(map);
    14             //获取result
    15             int result = MapUtils.getInteger(map, "result", -2);
    16             if (result == 1) {
    17                 SuccessKilled sk = successKilledDao.
    18                         queryByIdWithSeckill(seckillId, userPhone);
    19                 return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sk);
    20             } else {
    21                 return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
    22             }
    23         } catch (Exception e) {
    24             logger.error(e.getMessage(), e);
    25             return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
    26 
    27         }
    28 
    29     }
    executeSeckillProcedure实现

    编写SeckillDao实现有存储过程执行秒杀的逻辑

    1     /**
    2      * 使用存储过程执行秒杀
    3      * @param paramMap
    4      */
    5     void killByProcedure(Map<String,Object> paramMap);
    SeckillDao.java

    在Mybatis中使用

    1     <!-- mybatis调用存储过程 -->
    2     <select id="killByProcedure" statementType="CALLABLE">
    3         call execute_seckill(
    4             #{seckillId,jdbcType=BIGINT,mode=IN},
    5             #{phone,jdbcType=BIGINT,mode=IN},
    6             #{killTime,jdbcType=TIMESTAMP,mode=IN},
    7             #{result,jdbcType=INTEGER,mode=OUT}
    8         )
    9     </select>
    seclillDao.xml

    在Controller层使用

     1     @ResponseBody
     2     public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
     3                                                    @PathVariable("md5") String md5,
     4                                                    @CookieValue(value = "killPhone", required = false) Long phone) {
     5         //springmvc valid
     6         if (phone == null) {
     7             return new SeckillResult<SeckillExecution>(false, "未注册");
     8         }
     9         SeckillResult<SeckillExecution> result;
    10         try {
    11             //存储过程调用.
    12             SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
    13             return new SeckillResult<SeckillExecution>(true,execution);
    14         } catch (RepeatKillException e) {
    15             SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
    16             return new SeckillResult<SeckillExecution>(true,execution);
    17         } catch (SeckillCloseException e) {
    18             SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
    19             return new SeckillResult<SeckillExecution>(true,execution);
    20         } catch (Exception e) {
    21             logger.error(e.getMessage(), e);
    22             SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
    23             return new SeckillResult<SeckillExecution>(true,execution);
    24         }
    25     }
    SeckillResult

     至此,此系统的代码优化工作基本完成。但是在部署时可以将其更加优化,我们一般会使用如下架构

  • 相关阅读:
    Java中new关键字和newInstance方法的区别
    一道关于简单界面设计的练习题
    一道关于接口的练习题
    SPSS与聚类分析
    Nunit中文文档
    对比MS Test与NUnit Test框架
    Unit Test单元测试时如何模拟HttpContext
    如何vs升级后10和12都能同时兼容
    LINQ 从 CSV 文件生成 XML
    使用FileSystemWatcher监视文件变化
  • 原文地址:https://www.cnblogs.com/nullering/p/9533795.html
Copyright © 2011-2022 走看看