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

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

  • 相关阅读:
    leetcode 309. Best Time to Buy and Sell Stock with Cooldown
    leetcode 714. Best Time to Buy and Sell Stock with Transaction Fee
    leetcode 32. Longest Valid Parentheses
    leetcode 224. Basic Calculator
    leetcode 540. Single Element in a Sorted Array
    leetcode 109. Convert Sorted List to Binary Search Tree
    leetcode 3. Longest Substring Without Repeating Characters
    leetcode 84. Largest Rectangle in Histogram
    leetcode 338. Counting Bits
    git教程之回到过去,版本对比
  • 原文地址:https://www.cnblogs.com/nullering/p/9533795.html
Copyright © 2011-2022 走看看