• redis与Lua整合以及使用lua实现秒杀功能


    一、安装lua

      centos使用以下命令安装

    curl -R -O http://www.lua.org/ftp/lua-5.3.0.tar.gz
    tar zxf lua-5.3.0.tar.gz
    cd lua-5.3.0
    make linux test
    make install

      安装过程中可能出现的异常及解决办法如下:

      问题:

    [root@liconglong-aliyun lua-5.3.0]# make linux test
    
    ......
    make[2]: *** [lua.o] Error 1
    make[2]: Leaving directory `/root/rj/lua/lua-5.3.0/src'
    make[1]: *** [linux] Error 2
    make[1]: Leaving directory `/root/rj/lua/lua-5.3.0/src'
    make: *** [linux] Error 2

      解决方案:

    yum install libtermcap-devel ncurses-devel libevent-devel readline-devel -y

    二、Redis整合lua

      从redis2.6.0版本开始,通过内置的lua编译器和解析器,可以使用eval命令对lua脚本进行求值

      eval命令:eval script numkeys key [key ...] arg[arg ...]

        其中script是一段lua脚本程序,numbers参数是指key参数个数,key参数和arg参数,分别可以使用KEYS[index]和ARGV[index]在script脚本中获取,其中index从1开始

      示例如下:

    127.0.0.1:6380> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 lcl qmm 20 18
    1) "lcl"
    2) "qmm"
    3) "20"
    4) "18"

    三、lua脚本调用redis命令

    1、redis.call()

      返回值就是redis命令执行的返回值,如果出错,返回错误信息,不继续执行

    2、redis.pcall()

      返回值就是redis命令执行的返回值,如果出错了,记录错误信息,继续执行

      在脚本中,使用return语句将返回值返回给客户端,如果没有return,则返回nil

    127.0.0.1:6380> eval "return redis.call('set',KEYS[1],'testvalue')" 1 lclkey
    OK
    127.0.0.1:6380> get lclkey
    "testvalue"

    3、redis-cli --eval

       可以使用redis-cli --eval 命令指定一个lua脚本文件去执行。

    --获取指定值
    local num = redis.call('get',KEYS[1]);
    if not num then
     return num;
    else
     local res = num * KEYS[2] * ARGV[1];
     redis.call('set',KEYS[1],res);
     return res;
    end;

      设置key为luat的值为5(set luat 5)

      然后在linux中执行

    [root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 , 30
    (integer) 3000

      说明,这里要说明一下linux中执行lua脚本参数传值和redis命令执行lua脚本传值的差异问题,如果传入多个参数,那么在redis命令中,需要指定key的个数,所有的key和argv参数之间都使用空格分隔即可,lua脚本执行时,会根据传入的key个数自动区分开key参数和argv参数;但是在linux命令中,key参数和argv参数要用逗号分隔,key和key之间、argv与argv之间用空格分隔,如果key和argv之间不使用逗号,则会抛出异常,并且逗号前后需有空格,否则会被认为是传的一个参数,同样会抛出异常

      linux命令中参数传值异常及正常传值:

    [root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 30
    (error) ERR Error running script (call to f_8444ebd7385d71e3ee4daa6dc99acca626c75f4c): @user_script:6: user_script:6: attempt to perform arithmetic on field '?' (a nil value)
    [root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20, 30
    (error) ERR Error running script (call to f_8444ebd7385d71e3ee4daa6dc99acca626c75f4c): @user_script:6: user_script:6: attempt to perform arithmetic on field '?' (a string value)
    [root@liconglong-aliyun sentinel]# ./redis02/redis-5.0.4/bin/redis-cli -p 6380 --eval test.lua luat 20 , 30
    (integer) 1800000

      redis中传值可以参考第二点(Redis整合lua)

    四、redis+lua秒杀

      先说一下秒杀需求,首先,现在有一个活动,活动ID为123,在这个活动中,可以秒杀或者抢购某件商品(假设商品的SKUID为369),一个用户只允许抢购在该活动中抢购一件商品,同时,该用户名下也最多只能有一件该商品(就是如果通过其他途径买入,也不能参加该抢购活动)。

      了解了需求,那么首先就是编写lua脚本,在项目的resource目录下创建一个lua文件夹,专门放置lua脚本。

      根据需求,捋一下处理逻辑

        1、redis中要存sku的库存总量,以防抢购超库存量;要存用户在该活动中已抢购数量,用来限制用户在同一活动中抢购数量;要存用户已购sku数量,用户限制用户购入sku总量;记录一次活动中秒杀成功的次数,用以记录

        2、校验逻辑:库存是否大于0;用户抢购同一sku数量是否超过限制;用户购买同一sku数量是否超过限制;本次抢购sku数量是否超过了库存

        3、如果校验通过,则抢购成功,sku库存减一;用户购买该sku的数量加一;用户在该活动中抢购sku数量加一;用户秒杀成功数加一

      脚本如下所示:里面已经加了注释,不再一一说明代码逻辑

    --用户Id
    local userId = KEYS[1]
    --用户购买数量
    local buynum = tonumber(KEYS[2])
    --用户购买的SKU
    local skuid = KEYS[3]
    --每人购买此SKU的数量限制
    local perSkuLimit = tonumber(KEYS[4])
    --活动Id
    local actId = KEYS[5]
    --此活动中商品每人购买限制
    local perActLimit = tonumber(KEYS[6])
    --订单下单时间
    local ordertime = KEYS[7]
    --每个用户购买的某一sku数量
    local user_sku_hash = 'sec_'..actId..'_u_sku_hash'
    --每个用户购买的某一活动中商品的数量(已购买)
    local user_act_hash = 'sec_'..actId..'_u_act_hash'
    --sku的库存数
    local sku_amount_hash = 'sec_'..actId..'_sku_amount_hash'
    --秒杀成功的记录数
    local second_log_hash = 'sec_'..actId..'_u_sku_hash'
    
    --判断的流程:
    
    --判断商品库存数(当前sku是否还有库存)
    local skuAmountStr = redis.call('hget',sku_amount_hash,skuid) --获取目前sku的库存量
    if skuAmountStr == false then  --如果没有获取到,则说明商品设置有误,直接返回异常
        redis.log(redis.LOG_NOTICE,'skuAmountStr is nil ')
        return '-3'
    end
    
    local skuAmount = tonumber(skuAmountStr)
    --如果库存不大于0,则说明无库存,不能再抢购
    if skuAmount <= 0 then
        return '0'
    end
    
    local userActKey = userId..'_'..actId
    --判断用户已购买的同一sku数量,
    
    if perActLimit > 0 then  --如果每个人可以抢购的数量大于0,才能进行抢购,否则逻辑错误
        local userActNumInt = 0
        --获取该活动中该用户已经抢购到的数量
        local userActNum = redis.call('hget',user_act_hash,userActKey)
        --如果没有获取到,则说明用户还未抢购到,直接抢购用户下单的数量
        if userActNum == false then
            userActNumInt = buynum
        else   --如果获取到了用户在该活动中已经抢购到的数量,则用户抢购成功后的sku总量=原有数量 + 本次下单数量
            local curUserActNumInt = tonumber(userActNum)
            userActNumInt = curUserActNumInt + buynum
        end
    
        --如果抢购成功后用户在活动中抢购sku的数量大于每个用户限制的数量,则返回异常
        if userActNumInt > perActLimit then
            return '-2'
        end
    end
    
    --判断用户已购买的同一秒杀活动中的商品数量
    local goodsUserKey = userId..'_'..skuid
    if perSkuLimit > 0 then --判断每个用户允许下单该sku的最大数量
        --获取用户已购买的sku数量
        local goodsUserNum = redis.call('hget',user_sku_hash,goodsUserKey)
        local goodsUserNumInt = 0
        --逻辑同上,如果获取异常,说明用户目前没有购买过该sku,那么秒杀成功后购买sku的数量就是本次购买数量,否则就是本次购买数量 + 原有已购sku数量
        if goodsUserNum == false then
            goodsUserNumInt = buynum
        else
            local curSkuUserNumInt = tonumber(goodsUserNum)
            goodsUserNumInt = curSkuUserNumInt + buynum
        end
        --逻辑同上,如果本次购买成功后已购sku数量大于限制值,则返回异常
        if goodsUserNumInt > perSkuLimit then
            return '-1'
        end
    end
    
    --如果库存数量大于秒杀数量,则将sku库存减一;将用户购买该sku的数量加一;将用户在该活动中抢购sku数量加一;将用户秒杀成功数加一;最终返回订单号
    if skuAmount >= buynum then
        local decrNum = 0-buynum
        -- sku库存减一
        redis.call('hincrby',sku_amount_hash,skuid,decrNum)
        -- 用户购买该sku的数量加一
        if perSkuLimit > 0 then
            redis.call('hincrby',user_sku_hash,goodsUserKey,buynum)
        end
        -- 用户在该活动中抢购sku数量加一
        if perActLimit > 0 then
            redis.call('hincrby',user_act_hash,userActKey,buynum)
        end
        local orderKey = userId..'_'..'_'..buynum..'_'..ordertime
        local orderStr = '1'
        -- 用户秒杀成功数加一
        redis.call('hset',second_log_hash,orderKey,orderStr)
        return orderKey
    else
        return '0'
    end

      然后就是对lua脚本的调用工具类

    @Service
    @Slf4j
    public class RedisUtils {
    
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
        
        
        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) {
                log.error("秒杀失败", e);
            }
    
            return result;
        }
    }

      然后就是service层调用

    @Service
    public class LuaService {
        @Autowired
        private RedisUtils redisUtils;
    
        /**
         * 秒杀功能,调用lua脚本
         *
         * @param actId     活动id
         * @param userId    用户id
         * @param buyNum    购买数量
         * @param skuId     skuid
         * @param perSkuLim 每个用户购买当前sku的数量限制
         * @param perActLim 每个用户购买当前活动内所有sku的数量限制
         * @return
         */
        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 = getTime(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 = redisUtils.runLuaScript("order.lua", keyList);
            System.out.println("------------------lua result:" + result);
            return result;
        }
    
        private String getTime(int rand_num) {
            Date d = new Date();
            System.out.println(d);
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            String dateNowStr = sdf.format(d);
            return dateNowStr + "-" + rand_num;
        }
    }

      Controller

    @RestController
    @RequestMapping("/redis")
    public class RedisApi {
    
        @Autowired
        private LuaService luaService;
    
        @GetMapping("/lua")
        public String luaTest(String actId,String userId,int buyNum,String skuId,int perSkuLim,int perActLim){
            return luaService.skuSecond(actId,userId,buyNum,skuId,perSkuLim,perActLim);
        }
    }

      在调用前,先在redis中设置skuid(369)的库存,活动123中用户147258369已抢购sku数量及用户已购买数量

    127.0.0.1:6388> hset sec_123_sku_amount_hash 369 10000
    (integer) 0
    127.0.0.1:6388> hset sec_123_u_act_hash 147258369_123 0
    (integer) 0
    127.0.0.1:6388> hset sec_123_u_sku_hash 147258369_369 0
    (integer) 0

      然后调用:http://localhost:8080/redis/lua?actId=123&userId=147258369&buyNum=1&skuId=369&perSkuLim=1&perActLim=1

      就是在活动123中用户147258369准备抢购1件skuid为369的商品,一个人最多在该活动中抢购一件369商品,一个人最多只能购买一件369商品

      

       调用成功,最后再查看响应的库存等信息

    127.0.0.1:6388> hget sec_123_sku_amount_hash 369
    "9999"127.0.0.1:6388> hget sec_123_u_act_hash 147258369_123
    "1"
    127.0.0.1:6388> hget sec_123_u_sku_hash 147258369_369
    "1"

      其实这里还可以模拟一下在高并发下进行调用,可以使用一些压测工具进行测试,这里就不再说明。

    ------------------------------------------------------------------
    -----------------------------------------------------------
    ---------------------------------------------
    朦胧的夜 留笔~~
  • 相关阅读:
    转载:人家编写的程序:「雀神 AI」Suphx
    一千六百万单表建联合索引对查询效率的提升
    索引对单列极值查询的显著性影响(百万级别表单列最值查询 Cost由1405变成3)
    经典SQL问题:Top 10%
    区间查询与等效minus查询
    『科学计算』L0、L1与L2范数_理解
    『Python』__getattr__()特殊方法
    『Json』常用方法记录
    『Pickle』数据结构持久化模块_常用方法记录
    『Re』知识工程作业_主体识别
  • 原文地址:https://www.cnblogs.com/liconglong/p/14347794.html
Copyright © 2020-2023  润新知