分享好友 编程语言首页 频道列表

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

Lua  2023-02-08 23:560

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

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

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

        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,项目结构:

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)  

 

三,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

  添加库存

  如图:

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)  

2,配置jmeter开始测试:

  参见这一篇:   

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

 定义测试用到的变量:

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

定义线程组数量为100

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

定义http请求:

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

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

spring boot:redis+lua实现生产环境中可用的秒杀功能(spring boot 2.2.0)

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)

 

查看更多关于【Lua】的文章

展开全文
相关推荐
反对 0
举报 0
评论 0
图文资讯
热门推荐
优选好物
更多热点专题
更多推荐文章
LUA解析json小demo
需要修改的json数据gui-config.json{"configs": [{"server": "JP3.ISS.TF","server_port": 443,"password": "58603228","method": "aes-256-cfb","remarks": ""},{"serv

0评论2023-03-16958

第二十三篇:在SOUI中使用LUA脚本开发界面
像写网页一样做客户端界面可能是很多客户端开发的理想。做好一个可以实现和用户交互的动态网页应该包含两个部分:使用html做网页的布局,使用脚本如vbscript,javascript做用户交互的逻辑。当需求变化时,只需要在服务端把相关代码调整一下,用户即可看到新的

0评论2023-03-16307

windows下编译lua源码"><转>windows下编译lua源码
因为之前一直使用 lua for windows 来搭建lua的使用环境,但是最新的 lua for windows 还没有lua5.2,我又想用这个版本的lua,所以被逼无奈只能自己编一下lua源码。首先从 lua的官网 下载你想要使用的lua源码,比如我下载的就是lua5.2。解压后内容如下:

0评论2023-03-16723

lua:使用Lua处理游戏数据
在之前lua学习:lua作配置文件里,我们学会了用lua作配置文件。其实lua在游戏开发中可以作为一个强大的保存、载入游戏数据的工具。 比如说,现在我有一份表单:data.xls用什么工具解析这个Excel文件并将数据载入游戏?我们可以使用Lua来完成这个工作。不过要

0评论2023-03-16955

cocos2d-lua 控制台输入Lua指令方便调试
用脚本进行开发,如果不能实时去输入指令,就丧失了脚本的一大特色,所以对cocos2d-x程序稍微修改下,使其可以直接从控制台读入lua指令,方便调试。1 首先在行首加入lua的引用,如下1 #include "main.h"2 #include "AppDelegate.h"3 #include "cocos2d.h"4 #i

0评论2023-02-09995

lua_touserdata
void *lua_touserdata(lua_State*L,intindex);如果给定索引处的值是一个完整的userdata,函数返回内存块的地址。如果值是一个lightuserdata,那么就返回它表示的指针。否则,返回NULL。例如: 在CCLuaStack::executeFunction()函数中有一段代码是用来获取c++

0评论2023-02-09613

Lua 5.2 中文参考手册
闲来无事,发现Lua更新到了5.2.2,参考手册也更到了5.2,在网上发现只有云风翻译的5.1版,花了几天时间翻译了一些。参考手册有点长,又要随时修改,所以在github上建了项目,有需要的朋友可以看看,同时也欢迎指正。中文手册:Lua 5.2中文参考手册

0评论2023-02-09578

lua报错,看到报错信息有tail call,以为和尾调用有关,于是查了一下相关知识
  尾调用是指在函数return时直接将被调函数的返回值作为调用函数的返回值返回,尾调用在很多语言中都可以被编译器优化, 基本都是直接复用旧的执行栈, 不用再创建新的栈帧, 原理上其实也很简单, 因为尾调用在本质上看的话,是整个子过程调用的最后执行语句,

0评论2023-02-09333

lua 实现tableToString
function tableToString(studentNum) local str ="{ " str = str.."\n" for k, v in pairs(studentNum) doif type(v) ~= "table" thenstr = str.."[\""..k.."\"]"str = str..":"str = str..vstr = st

0评论2023-02-09824

Lua类对象和类对象的单例 lua实例
1、Lua的类对象local myClass = {}function myClass:new()local self = {}setmetatable(self,{__index = myClass})endlocal a = 0local b = 0local c = 0return myClass以上类的对象实例化的调用:require "myClass"local _myClass = myClass:new()实例化后 _

0评论2023-02-09653

浅析一个lua文件窥slua工作机制 lua 调用so
slua的东西不是几句话能讲得完,这里只说结论不说原因,原因有空写个Little Slua工程来解释,下面注释中有几个关键点:LuaVar系列类:LuaFunction,LuaTable,LuaDelegate的使用,类型表和实例表,__parent代表继承关系,存ud的表是弱表(可以用来缓存c#中引用

0评论2023-02-09602

lua学习笔记10:lua简单的命令行 lua怎么执行
前面反复使用的命令行,好学喜欢命令行:一 格公式lua [options][script][args]两 详细命令-e 直接命令传递一个lua-l 加载文件-i 进入交互模式比例如。端子输入:lua -e "print(math.sin(12))" 版权声明:本文博主原创文章,博客,未经同意不得转载。

0评论2023-02-09533

Lua中强大的元方法__index详解 lua元表和元方法
今天要来介绍比较好玩的内容——__index元方法1.我是备胎,记得回头看看咳咳,相信每一位女生都拥有或者不知不觉中拥有了一些备胎,啊,当然,又或许是成为过别人的备胎。没有备胎的人,就不是完整的人生。(小若:停!) 我们来想象一下,如果对一个table进

0评论2023-02-09494

Lua调试工具使用及原理 lua运行原理
前言当我们在linux下使用c/c++开发时,可以通过gdb来调试我们编译后的elf文件。gdb支持了attch、单步运行(单行、单指令)、设置断点等非常实用的功能来辅助我们调试。当使用lua开发的时候,一般可能会使用print(打印到屏幕)或是输出日志等稍微简陋的调试方

0评论2023-02-09999

(转)lua protobuffer的实现
转自: http://www.voidcn.com/article/p-vmuovdgn-bam.html (1)lua实现protobuf的简介需要读者对google的protobuf有一定的了解。Protocol buffers are a flexible, efficient, automated mechanism for serializing structured data – think XML, but s

0评论2023-02-09818

lua require路径设置实例
1.首先要强调的是,lua require的路径用的是斜杠"/",而不是从Windows文件属性那里复制来的反斜杠"\"。2.通过 print(pagckage.path) 和print(package.cpath)打印lua系统封装的两个全局属性可以看到当前lua解析器require的时候默认替换的路径3.更改路径的时候

0评论2023-02-09855

更多推荐