zoukankan      html  css  js  c++  java
  • spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

    一,秒杀需要具备的功能:

            秒杀通常是电商中用到的吸引流量的促销活动方式

            搭建秒杀系统,需要具备以下几点:

            1,限制每个用户购买的商品数量,(秒杀价格为吸引流量一般会订的很低,不能让一个用户全部抢购到手)

            2,处理速度要快,避免在高并发的情况下发生堵塞

            3,高并发情况下,不能出现库存超卖的情况

            因为redis中对lua脚本执行的原子性,不会出现因高并发而导致数据查询的延迟

            所以我们选择使用redis+lua来实现秒杀的功能

           例子:如果同一个秒杀活动中有多件商品,而有人用软件刷接口的方式来下单,

                    这时就需要有针对当前活动的购买数量限制

    说明:刘宏缔的架构森林是一个专注架构的博客,地址:https://www.cnblogs.com/architectforest

             对应的源码可以访问这里获取: https://github.com/liuhongdi/

    说明:作者:刘宏缔 邮箱: 371125307@qq.com

    二,本演示项目的相关信息

    1,项目地址:

    https://github.com/liuhongdi/seconddemo

    2,项目原理:

    在秒杀项目开始前,要把sku及其库存数同步到redis中,

    有秒杀请求时,判断商品库存数,

    判断用户已购买的同一sku数量,

    判断用户已购买的同一秒杀活动中的商品数量,

    如果以上两个数量大于0时,需要进行限制

    如有问题时,返回秒杀失败

    都没有问题时,减库存,返回秒杀成功

    要注意的地方:

    秒杀前要获取此活动中的对购买活动/sku的数量限制

    秒杀成功后,如果用户未支付导致订单过期恢复库存时,redis中的库存数也要同步

    3,项目结构:

      

    三,lua代码说明

    1,second.lua

    local userId = KEYS[1]
    local buyNum = tonumber(KEYS[2])
    
    local skuId = KEYS[3]
    local perSkuLim = tonumber(KEYS[4])
    
    local actId = KEYS[5]
    local perActLim = tonumber(KEYS[6])
    
    local orderTime = KEYS[7]
    
    --用到的各个hash
    local user_sku_hash = 'sec_'..actId..'_u_sku_hash'
    local user_act_hash = 'sec_'..actId..'_u_act_hash'
    local sku_amount_hash = 'sec_'..actId..'_sku_amount_hash'
    local second_log_hash = 'sec_'..actId..'_log_hash'
    
    --当前sku是否还有库存
    local skuAmountStr = redis.call('hget',sku_amount_hash,skuId)
    if skuAmountStr == false then
            --redis.log(redis.LOG_NOTICE,'skuAmountStr is nil ')
            return '-3'
    end;
    local skuAmount = tonumber(skuAmountStr)
     --redis.log(redis.LOG_NOTICE,'sku:'..skuId..';skuAmount:'..skuAmount)
     if skuAmount <= 0 then
       return '0'
    end
    
    redis.log(redis.LOG_NOTICE,'perActLim:'..perActLim)
    local userActKey = userId..'_'..actId
    --当前用户已购买此活动多少件
     if perActLim > 0 then
       local userActNumInt = 0
       local userActNum = redis.call('hget',user_act_hash,userActKey)
       if userActNum == false then
          --redis.log(redis.LOG_NOTICE,'userActKey:'..userActKey..' is nil')
          userActNumInt = buyNum
       else
          --redis.log(redis.LOG_NOTICE,userActKey..':userActNum:'..userActNum..';perActLim:'..perActLim)
          local curUserActNumInt = tonumber(userActNum)
          userActNumInt =  curUserActNumInt+buyNum
       end
       if userActNumInt > perActLim then
           return '-2'
       end
     end
    
    local goodsUserKey = userId..'_'..skuId
    --redis.log(redis.LOG_NOTICE,'perSkuLim:'..perSkuLim)
    --当前用户已购买此sku多少件
    if perSkuLim > 0 then
       local goodsUserNum = redis.call('hget',user_sku_hash,goodsUserKey)
       local goodsUserNumint = 0
       if goodsUserNum == false then
          --redis.log(redis.LOG_NOTICE,'goodsUserNum is nil')
          goodsUserNumint = buyNum
       else
          --redis.log(redis.LOG_NOTICE,'goodsUserNum:'..goodsUserNum..';perSkuLim:'..perSkuLim)
          local curSkuUserNumint = tonumber(goodsUserNum)
          goodsUserNumint =  curSkuUserNumint+buyNum
       end
    
       --redis.log(redis.LOG_NOTICE,'------goodsUserNumint:'..goodsUserNumint..';perSkuLim:'..perSkuLim)
       if goodsUserNumint > perSkuLim then
           return '-1'
       end
    end
    
    --判断是否还有库存满足当前秒杀数量
    if skuAmount >= buyNum then
         local decrNum = 0-buyNum
         redis.call('hincrby',sku_amount_hash,skuId,decrNum)
         --redis.log(redis.LOG_NOTICE,'second success:'..skuId..'-'..buyNum)
    
         if perSkuLim > 0 then
             redis.call('hincrby',user_sku_hash,goodsUserKey,buyNum)
         end
    
         if perActLim > 0 then
             redis.call('hincrby',user_act_hash,userActKey,buyNum)
         end
    
         local orderKey = userId..'_'..skuId..'_'..buyNum..'_'..orderTime
         local orderStr = '1'
         redis.call('hset',second_log_hash,orderKey,orderStr)
    
       return orderKey
    else
       return '0'
    end

    2,功能说明:

    --用到的各个参数

    local userId  用户id

    local buyNum 用户购买的数量

    local skuId 用户购买的sku

    local perSkuLim 每人购买此sku的数量限制

    local actId 活动id

    local perActLim 此活动中商品每人购买数量的限制

    local orderTime 下订单的时间

    --用到的各个hash
    local user_sku_hash 每个用户购买的某一sku的数量
    local user_act_hash 每个用户购买的某一活动中商品的数量
    local sku_amount_hash sku的库存数
    local second_log_hash 秒杀成功的记录

    判断的流程:

    判断商品库存数,

    判断用户已购买的同一sku数量,

    判断用户已购买的同一秒杀活动中的商品数量

    四,java代码说明:

    1,SecondServiceImpl.java

    功能:传递参数,执行秒杀功能

     /*
        * 秒杀功能,
        * 调用second.lua脚本
        * actId:活动id
        * userId:用户id
        * buyNum:购买数量
        * skuId:sku的id
        * perSkuLim:每个用户购买当前sku的个数限制
        * perActLim:每个用户购买当前活动内所有sku的总数量限制
        * 返回:
        * 秒杀的结果
        *  * */
        @Override
        public String skuSecond(String actId,String userId,int buyNum,String skuId,int perSkuLim,int perActLim) {
    
            //时间字串,用来区分秒杀成功的订单
            int START = 100000;
            int END = 900000;
            int rand_num = ThreadLocalRandom.current().nextInt(END - START + 1) + START;
            String order_time = TimeUtil.getTimeNowStr()+"-"+rand_num;
    
            List<String> keyList = new ArrayList();
            keyList.add(userId);
            keyList.add(String.valueOf(buyNum));
            keyList.add(skuId);
            keyList.add(String.valueOf(perSkuLim));
            keyList.add(actId);
            keyList.add(String.valueOf(perActLim));
            keyList.add(order_time);
    
            String result = redisLuaUtil.runLuaScript("second.lua",keyList);
            System.out.println("------------------lua result:"+result);
            return result;
        }

    2,RedisLuaUtil.java

    功能:负责调用lua脚本的类

    @Service
    public class RedisLuaUtil {
        @Resource
        private StringRedisTemplate stringRedisTemplate;
    
        private static final Logger logger = LogManager.getLogger("bussniesslog");
        /*
        run a lua script
        luaFileName: lua file name,no path
        keyList: list for redis key
        return other: fail
               1: success
        */
        public String runLuaScript(String luaFileName,List<String> keyList) {
            DefaultRedisScript<String> redisScript = new DefaultRedisScript<>();
            redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("lua/"+luaFileName)));
            redisScript.setResultType(String.class);
            String result = "";
            String argsone = "none";
            //logger.error("开始执行lua");
            try {
                result = stringRedisTemplate.execute(redisScript, keyList,argsone);
            } catch (Exception e) {
                logger.error("发生异常",e);
            }
    
            return result;
        }
    }

    五,测试秒杀的效果

    1,访问:http://127.0.0.1:8080/second/index

      添加库存

      如图:

      

    2,配置jmeter开始测试:

      参见这一篇:   

    https://www.cnblogs.com/architectforest/p/13087798.html

     定义测试用到的变量:

    定义线程组数量为100

    定义http请求:

    在查看结果树中查看结果:

    3,查看代码中的输出:

    ------------------lua result:u3_cpugreen_1_20200611162435-487367
    ------------------lua result:-2
    ------------------lua result:u1_cpugreen_2_20200611162435-644085
    ------------------lua result:u3_cpugreen_1_20200611162435-209653
    ------------------lua result:-1
    ------------------lua result:u2_cpugreen_1_20200611162434-333603
    ------------------lua result:-1
    ------------------lua result:-2
    ------------------lua result:-1
    ------------------lua result:u2_cpugreen_1_20200611162434-220636
    ------------------lua result:-2
    ------------------lua result:-1
    ...

    每个用户的购买数量均未超过2单,秒杀的限制成功

    六,查看spring boot的版本:

      .   ____          _            __ _ _
     /\ / ___'_ __ _ _(_)_ __  __ _    
    ( ( )\___ | '_ | '_| | '_ / _` |    
     \/  ___)| |_)| | | | | || (_| |  ) ) ) )
      '  |____| .__|_| |_|_| |_\__, | / / / /
     =========|_|==============|___/=/_/_/_/
     :: Spring Boot ::        (v2.2.0.RELEASE)
  • 相关阅读:
    jquery笔记(常用技术)
    AutoUpgraderPro 4.X美化版 源码及Demo程序
    读写Unicode和UTF8格式文件
    AutoUpgraderPro 4.X美化版 源码及Demo程序
    买了两套无线键鼠套装
    今天做了一回黑客
    AutoUpgraderPro Ver 4.1.1带源码美化版
    Delphi虚拟键码对照表
    Delphi虚拟键码对照表
    读写Unicode和UTF8格式文件
  • 原文地址:https://www.cnblogs.com/architectforest/p/13094795.html
Copyright © 2011-2022 走看看