最简单有效的秒杀限流

情景

整点秒杀、限时特卖等活动会吸引大量用户在同一时间点、时间段访问请求系统,从而产生大的并发量,如果没有合理处理的话会出现商品超卖页面访问速度较慢更严重的可能出现系统宕机其他服务也无法正常使用。

本文主要处理服务端使用redis进行限流

涉及到的技术点主要有:
redis分布式锁 redis常用命令

主要逻辑方案:

  1. 预热库存数据

  2. 限流(允许进入两倍库存的访问量)

  3. 10秒失效一次redis库存数据 重新获取(处理因为进入下单页没有提交订单 而导致的库存剩余)

实现

  1. 主要实现代码 判断是否有库存isEnoughRepository

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    /**
    * @author lemon
    * @date 2018-10-06
    * @desc jedis实现秒杀限流
    */
    @Component
    public class JedisSecKill {

    public static final Log LWT_LOG = LogFactory.getLog(JedisSecKill.class.getName());

    @Resource
    MallProductDao mallProductDao;

    /**
    * 初始化商品的库存
    * 1. 如果已经初始化则不需要初始化 直接返回true
    * 2. 获取jedis分布式锁 获取成功查询库 更新jedis库存(如果库存为0则标记已售完)
    * 3. 库存信息10秒后失效
    * @param productId
    * @return 是否初始化成功(之前初始化过/获取锁成功)
    */
    private boolean initRepository(final int productId) {
    final String secProductKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT, productId + "");
    if (JedisUtil.exists(secProductKey)) {
    return true;
    }

    final String secProductStatusKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_STATUS, productId + "");
    boolean lock = JedisLock.lock(productId + "", new LockFunction() {
    @Override
    public void call() {
    List<MallProductSpec> productSpecs = mallProductDao.listMallProductSpec(productId);
    if (CollectionUtils.isNotEmpty(productSpecs)) {
    boolean isSoldOut = true;
    for (MallProductSpec spec : productSpecs) {
    if (StringUtils.equals(spec.getUseRepository(), "N")) {
    isSoldOut = false;
    break;
    }

    if (StringUtils.equals(spec.getUseRepository(), "Y") && spec.getRepository() > 0) {
    isSoldOut = false;
    break;
    }

    }

    if (isSoldOut) {
    JedisUtil.set(secProductStatusKey, "soldOut");
    JedisUtil.expire(secProductStatusKey, 60);
    return;
    } else {
    JedisUtil.set(secProductStatusKey, "inSale", "NX", "EX", 60);
    }

    initPerRepository(productSpecs);
    }
    }

    private void initPerRepository(List<MallProductSpec> productSpecs) {
    Jedis jedis = null;
    try {
    jedis = JedisUtil.getJedis();
    String setRes = jedis.set(secProductKey, "inSale", "NX", "EX", 10);
    if (StringUtils.equals("OK", setRes)) {
    for (MallProductSpec spec : productSpecs) {
    String specKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_SPEC, spec.getSpecKey());
    if (StringUtils.equals(spec.getUseRepository(), "N")) {
    jedis.set(specKey, 99 + "", "NX", "EX", 10);
    } else {
    jedis.set(specKey, (spec.getRepository() * 2) + "", "NX", "EX", 10);
    }
    }
    }

    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (jedis != null) {
    jedis.close();
    }
    }
    }
    });
    return lock;
    }

    /**
    * 根据商品id和规格 获取jedis是否有足够库存
    * 核心使用 jedis.decr 进行单线程操作库存 处理并发
    * @param productId
    * @param specKey
    * @return
    */
    public boolean isEnoughRepository(int productId, String specKey) {
    boolean initRes = initRepository(productId);
    if (!initRes) {
    return false;
    }

    final String secProductStatusKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_STATUS, productId + "");
    Jedis jedis = null;
    try {
    jedis = JedisUtil.getJedis();
    String secStatus = jedis.get(secProductStatusKey);
    if (StringUtils.equals("soldOut", secStatus)) {
    return false;
    }

    String specParamKey = String.format(JedisKeyConstant.SEC_KILL_PRODUCT_SPEC, specKey);
    Boolean exists = jedis.exists(specParamKey);
    Long decr = jedis.decr(specParamKey);
    LWT_LOG.debug("enoughRepository key:"+ specParamKey+" exists:" + exists + ",value:" + jedis.get(specParamKey) + ",decrValue:" + decr);
    if (exists && decr >= 0) {
    return true;
    }
    } finally {
    if (jedis != null) {
    jedis.close();
    }
    }

    return false;
    }

    }

  2. 其中常量值

    1
    2
    3
    4
    5
    6
    //秒杀使用此key控制库存是否过期
    public final static String SEC_KILL_PRODUCT = "sec_kill_product_%s";
    //使用此key控制秒杀是否售罄
    public final static String SEC_KILL_PRODUCT_STATUS = "sec_kill_product_status_%s";
    //使用此key控制秒杀商品sku
    public final static String SEC_KILL_PRODUCT_SPEC = "sec_kill_product_spec_%s";
  3. redis分布式锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    /**
    * @author lemon(lemon @ laowantong.cc)
    * @date 2018-10-06
    * @desc jedis 分布式锁
    */

    public class JedisLock {

    /**
    * 通过获取key的setnx
    * 如果获取成功 执行获取成功之后的function
    * 执行完成之后删除key释放锁
    * @param key
    * @param function
    * @return
    */
    public static boolean lock(String key, LockFunction function) {
    Jedis jedis = null;
    try {
    jedis = JedisUtil.getJedis();
    if (jedis != null) {
    String setRes = jedis.set(key, "b", "NX", "EX", 5);
    if (StringUtils.equals("OK", setRes)) {
    function.call();
    jedis.del(key);
    return true;
    }
    }
    } catch (Exception e) {
    e.printStackTrace();
    } finally {
    if (jedis != null) {
    jedis.close();
    }
    }

    return false;
    }
    }

此方案的问题

  1. 分布式锁竞争期间 其他争夺资源的并发请求 处理结果为失败
  2. 没有彻底满足先到先得的原则(可使用队列方案实现)

附一秒杀架构图架构优化

欢迎交流 原文地址:不爱更新的博客

未经作者允许 请勿转载,谢谢 :)

lemon wechat
欢迎大家关注我的订阅号 SeeMoonUp
写的不错?鼓励一下?不差钱?