PHP秒杀解决方案

秒杀系统的特点/难点

1. 访问量突然增大

突然增加的访问量可能导致原有商城系统响应不过来而崩溃

解决方案:将秒杀活动独立部署在另外的机器上面

2. 带宽问题

假如商品页面的大小为1M,这时有10000个用户并发,那消耗的带宽就是10G,远远超过平时的带宽

解决方案:提前将商品页面缓存在CDN中,可以自己搭建或者直接购买第三方平台的

自己搭建CND可以参考这里:nginx + squid 实现CDN加速

3. 有大部分的请求不会生成订单

既然是秒杀,就意味着不是所有的请求都能成功下单,可以直接在接入层过滤掉大部分的请求

解决方案:在接入层(nginx)做漏桶限流,减轻应用层(PHP、MySQL)的流量压力

4. 请求负载大
  1. 使用队列,将所有请求放入队列中,由另一个脚本按照顺序一个一个的处理

  2. 负载均衡,使用nginx反向代理实现负载均衡,将请求分发到不同的机器上,平摊流量

  3. 接入层限流 + 配置中心限流实现过载保护,用nginx实现限流,拦截大部分请求,降低服务器压力,保护服务不被击溃

5. 超卖问题

一旦存在并发,就很有可能会产生超卖问题,而且这个问题很严重,必须要解决。

解决方案:

  1. MySQL悲观锁

    使用MySQL的锁机制,在查询库存时加排它锁,阻止其他事务对这条数据进行加锁或者修改

    优点:使用MySQL事务锁机制,准确度高

    缺点:比较耗性能,对MySQL的压力比较大

    示例:

    DB::beginTransaction();try {
        $stock = Skill::query()->where('id', $id)->lockForUpdate()->value('stock');
        if ($stock > 0) {
            Skill::query()->where('id', $id)->decrement('stock');
            echo '抢购成功';
        } else {
            echo '库存不足,抢购失败';
        }
        DB::commit();} catch (\Exception $e) {
        echo $e->getMessage();
        DB::rollBack();}
  2. MySQL乐观锁

    乐观锁其实就是不加锁实现锁的效果。MySQL的乐观锁就是MVCC机制,借助version版本号进行控制

    优点:因为不涉及到锁数据,所以它的并发量会比加悲观锁强一些

    缺点:虽然不锁数据,但是还是基于MySQL来实现的,这就意味着他要受到MySQL抗压瓶颈的影响

    示例:

    $info = Skill::query()->where('id', $id)->first(['stock', 'version']);if ($info->stock > 0) {
     $skill = Skill::query()->where(['id' => $id, 'version' => $info->version])->update(['stock' => $info->stock - 1, 'version' => $info->version + 1]);
     echo '抢购成功';} else {
     echo '库存不足,抢购失败';}
  3. PHP + 队列

    将请求序列化存入队列,由另一个脚本排着队挨个执行

    优点:降低了MySQL的压力

    缺点:这种方式每次只处理一个请求,反而降低了程序的并发量

  4. PHP + Redis分布式锁

    Redis分布式锁就是线程锁,通过锁线程来实现,同时只允许一个线程执行,其它线程进入等待状态

    优点:既降低了MySQL压力,又比队列的方式并发性更高

    缺点:因为线程需要排队等待,所以并发量级也不是特别的高

    示例:

    $key = "test:lock:".$id;$uuid = Uuid::uuid1()->getHex();try {
        $ret = Redis::set($key, $uuid, 'EX', 10, 'NX');
        if (!$ret) {
            usleep(10);
            return $this->test($id);
        }
        $stock = Skill::query()->where('id', $id)->value('stock');
        if ($stock > 0) {
            Skill::query()->where('id', $id)->decrement('stock');
            $msg = '抢购成功';
        } else {
            $msg = '库存不足,抢购失败';
        }
        if (Redis::get($key) == $uuid) {
            Redis::del($key);
        }
        return $msg;} catch (\Exception $exception) {
        return '抢购失败';}
  5. PHP + Redis乐观锁

    Redis的乐观锁就是借助Redis事务和watch监控,采用事务打断的方式实现

    优点:并没有锁定任何资源,多线程可以并行,所以比以上几种性能要更好,并发量级更大

    缺点:这是PHP层面的控制,而PHP也是有性能瓶颈的

    示例:

    $key = 'stock:'.$id;Redis::watch($key);$stock = Redis::get($key);if (is_null($stock)) {
     return '没有商品';}if ($stock == 0) {
     return '库存不足';}Redis::multi();Redis::decr($key);$res = Redis::exec();if ($res) {
     Skill::query()->where('id', $id)->decrement('stock');
     return '抢购成功';} else {
     return '抢购失败';}
  6. Nginx结合Lua做漏桶限流 + Redis乐观锁(最优方案)

    这种方案是最优方案,直接绕过应用层,在接入层实现限流和防止超卖的操作,只消耗很少的服务器性能,但是可抗并发量级特别大,性能上远超上述几种方案。

    逻辑分析:先使用 Nginx+Lua 漏桶算法过滤掉大部分请求,再使用Lua连接Redis,使用Redis乐观锁的方式控制库存。假设只有10个秒杀商品,那这里就过滤掉其他,只保留10个请求进入应用层(PHP和MySQL),应用层不需要进行其他操作,直接操作数据库就可以

    实操演示:

  • 安装 LuaJIT

    选择 LuaJIT 而不是标准 Lua 的原因:

    官网下载地址:https://luajit.org/download.html

    PS:本次使用的不是官网的,是 OpenResty 的,因为使用官网版本启动Nginx时会有个警告,让使用 OpenResty 的,虽然不影响使用,但是强迫症还是改了它。

    # 安装依赖
    yum install readline-devel
    # 下载安装包
    wget https://github.com/openresty/luajit2/archive/refs/tags/v2.1-20210510.tar.gz
    tar -zxvf luajit2-2.1-20210510.tar.gz
    cd luajit2-2.1-20210510
    make && make install

    配置 LuaJIT 环境变量

    vi /etc/profile
    
    export LUAJIT_LIB=/usr/local/lib
    export LUAJIT_INC=/usr/local/include/luajit-2.1
    
    source /etc/profile

    测试 Lua 脚本

    [root@localhost ~]# vi test.lua  print("Hello World!")[root@localhost ~]# lua test.lua 
    Hello World!
  • 安装 ngx_devel_kit 和 lua-nginx-module

    ngx_devel_kit 简称NDK,提供函数和宏处理一些基本任务,减轻第三方模块开发的代码量。

    lua-nginx-module 是Nginx的Lua模块

    wget https://github.com/simpl/ngx_devel_kit/archive/v0.3.1.tar.gz
    tar -zxvf ngx_devel_kit-0.3.1.tar.gz
    # 这里选择v0.10.9rc7这个版本,其他版本在nginx启动时都会有各种坑
    wget https://github.com/openresty/lua-nginx-module/archive/v0.10.9rc7.tar.gz
    tar -zxvf lua-nginx-module-0.10.9rc7.tar.gz

    将解压好的文件夹加载到Nginx的模块中,Nginx如何安装就不讲了,这里安装好的版本是 nginx-1.20.1

    # 查看nginx现有的模块,复制configure arguments:后边的内容nginx -V# 进去nginx安装包目录,重新编译,加上刚才解压的两个目录./configure 上边configure arguments:后边的内容... --add-module=/root/ngx_devel_kit-0.3.1 --add-module=/root/lua-nginx-module-0.10.9rc7
    make && make install
    echo "/usr/local/lib" >> /etc/ld.so.conf
    ldconfig

    修改Nginx配置

    vi nginx.conf
    
    server {
            listen  80;
            
            ...
            
           # 加入这段测试代码
            location /lua {
                set $test "hello,world";
                content_by_lua '
                    ngx.header.content_type="text/plain"
                    ngx.say(ngx.var.test)
                ';
            }
    }

    重启Nginx后进行访问测试

    [root@localhost conf]# curl 127.0.0.1/lua
    hello,world[root@localhost conf]#
  • 下载需要用到的模块

    lua-resty-limit-traffic:限流模块

    lua-resty-redis:操作redis模块

    lua-cjson:在lua中操作json数据,方便返回给前端

    mkdir /usr/local/nginx/lua
    cd /usr/local/nginx/lua
    git clone https://github.com/openresty/lua-resty-limit-traffic.git
    git clone https://github.com/openresty/lua-resty-redis.git
    wget https://kyne.com.au/~mark/software/download/lua-cjson-2.1.0.tar.gz
    tar -zxvf lua-cjson-2.1.0.tar.gz
    cd lua-cjson-2.1.0/
    make && make install

    编译cjson报错:

    [root@localhost lua-cjson-2.1.0]# make && make install
    cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include -fpic -o lua_cjson.o lua_cjson.c
    lua_cjson.c:43:17: 致命错误:lua.h:没有那个文件或目录 #include <lua.h>
                     ^编译中断。
    make: *** [lua_cjson.o] 错误 1

    解决:

    [root@localhost lua-cjson-2.1.0]# find / -name lua.h/usr/local/include/luajit-2.1/lua.h[root@localhost lua-cjson-2.1.0]# vi Makefile
       将 LUA_INCLUDE_DIR =   $(PREFIX)/include
       修改为 LUA_INCLUDE_DIR = /usr/local/include/luajit-2.1
       
     [root@localhost lua-cjson-2.1.0]# make && make install

    仍然报错:

    [root@localhost lua-cjson-2.1.0]# make && make install
    cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.1/ -fpic -o lua_cjson.o lua_cjson.c
    lua_cjson.c:1299:1: 错误:对‘luaL_setfuncs’的静态声明出现在非静态声明之后 {
     ^In file included from lua_cjson.c:44:0:/usr/local/include/luajit-2.1/lauxlib.h:88:18: 附注:‘luaL_setfuncs’的上一个声明在此 LUALIB_API void (luaL_setfuncs) (lua_State *L, const luaL_Reg *l, int nup);
                      ^make: *** [lua_cjson.o] 错误 1

    解决:

    # 直接在Makefile所在的目录执行查找字符串命令[root@localhost lua-cjson-2.1.0]# find . -type f -name "*.*" | xargs grep "luaL_setfuncs"./lua_cjson.c: * luaL_setfuncs() is used to create a module table where the functions have./lua_cjson.c:static void luaL_setfuncs (lua_State *l, const luaL_Reg *reg, int nup)./lua_cjson.c:    luaL_setfuncs(l, reg, 1);# 发现只有lua_cjson.c 文件中包含上面字符串,所以编辑 lua_cjson.c[root@localhost lua-cjson-2.1.0]# vi lua_cjson.c
       直接搜索 luaL_setfuncs,去掉此方法的 static 关键字
       
    # 继续编译就成功了[root@localhost lua-cjson-2.1.0]# make && make install
    cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.1/ -fpic -o lua_cjson.o lua_cjson.c
    cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.1/ -fpic -o strbuf.o strbuf.c
    cc -c -O3 -Wall -pedantic -DNDEBUG  -I/usr/local/include/luajit-2.1/ -fpic -o fpconv.o fpconv.c
    cc  -shared -o cjson.so lua_cjson.o strbuf.o fpconv.o
    mkdir -p //usr/local/lib/lua/5.1cp cjson.so //usr/local/lib/lua/5.1chmod 755 //usr/local/lib/lua/5.1/cjson.so
  • 完整的 Lua 脚本示例

    vi /usr/local/nginx/lua/seckill.lua

    -------------------------- 定义json --------------------------------------- 引入 cjson 模块,操作json数据local cjson = require "cjson"local cjson_req = cjson.new()local ret_object = {["code"] = 999, ["msg"] = "很遗憾,手慢了,没抢到"}ret_json = cjson_req.encode(ret_object)-------------------------- 漏桶限流 --------------------------------------- 引入 nginx-lua 限流模块local limit_req = require "resty.limit.req"-- 每秒立即处理的请求数local rate = 50-- 漏桶的最大容量local capacity = 1000-- 限制请求在每秒 rate 次以下并且并发请求每秒 capacity 次-- 也就是延迟处理每秒 rate 次以上 capacity 次以内的请求-- 每秒超过 rate+capacity 次的请求会直接 reject 拒绝掉-- my_limit_req_store 为共享内存区域名称local lim, err = limit_req.new("my_limit_req_store", rate, capacity)if not lim then
        ngx.log(ngx.ERR, "failed to instantiate a resty.limit.req object: ", err)
        return ngx.exit(500)end-- 每个请求,都获取客户端的IP来作为限制的 keylocal key = ngx.var.binary_remote_addr-- 获取每个请求的等待时长,这个时长是通过 resty.limit.req 模块计算出来的local delay, err = lim:incoming(key, true)if (delay < 0 or delay == nil) then
        return ngx.exit(500)end-- 大于 capacity 以外的就溢出if not delay then
        if err == "rejected" then
            return ngx.exit(500)
        end
        ngx.log(ngx.ERR, "failed to limit req: ", err)
        return ngx.exit(500)end-- 如果等待时长超过10s,直接返回超时if (delay > 10) then
        ngx.say(ret_json)
        returnend-------------------------- 实现redis乐观锁 --------------------------------------- 设置关闭redis的函数,在redis使用完后调用它local function close_redis(redis_instance)
        if not redis_instance then
            return
        end
        local ok, err = redis_instance:close()
        if not ok then
            ngx.log(ngx.ERR, "close redis error: ", err)
            return
        endend-- 引入 redis 模块local redis = require("resty.redis");-- 创建一个redis对象实例local redis_instance = redis:new()-- 设置超时时间,单位毫秒redis_instance:set_timeout(1000)-- 建立连接local host = "192.168.241.111"local port = 6379local pass = "root"-- 尝试连接到redis服务器正在侦听的远程主机和端口local ok, err = redis_instance:connect(host, port)if not ok then
        ngx.log(ngx.ERR, "connect redis error: ", err)
        return close_redis(redis_instance);end-- Redis身份验证local auth, err = redis_instance:auth(pass);if not auth then
        ngx.log(ngx.ERR, "redis failed to authenticate: ", err)
        return close_redis(redis_instance);end-- 获取请求参数local request_method = ngx.var.request_methodlocal args, paramif request_method == "GET" then
        args = ngx.req.get_uri_args()elseif request_method == "POST" then
        ngx.req.read_body()
        args = ngx.req.get_post_args()end-- 可通过 args["user_id"] 获取请求的用户id,进行身份等逻辑判断,此处略过-- 从redis中取出当前请求商品sku的库存local redis_key = "sku:"..args["sku_id"]..":stock"local stock = tonumber(redis_instance:get(redis_key))-- 实现redis乐观锁if (stock > 0) then
        redis_instance:watch(redis_key)
        redis_instance:multi()
        redis_instance:decr(redis_key)
        local ans = redis_instance:exec()
        if (tostring(ans) == "userdata: NULL") then
            return ngx.say(ret_json)
        endelse
        return ngx.say(ret_json)end-- 抢购成功,进入下单流程-- 注意:这行代码前面不能执行 ngx.say()ngx.exec("/create_order")
  • 在 nginx.conf 中的配置

    ...
    http {
       ...
       # 设置共享内存区域,大小为100M
       lua_shared_dict my_limit_req_store 100m;
       # 设置Lua扩展库的搜索路径(';;' 表示默认路径)
        lua_package_path "/usr/local/nginx/lua/lua-resty-limit-traffic/lib/?.lua;;/usr/local/nginx/lua/lua-resty-redis-master/lib/?.lua;;";
        
        server {
           listen       80;
           ...
           # 限流及控制库存
           location /seckill {
               # 可有可无
                default_type 'application/x-javascript;charset=utf-8';
                # 引入lua脚本
                content_by_lua_file /usr/local/nginx/lua/seckill.lua;
            }
            # 下订单
            location /create_order {
               # 只允许本地访问
               allow   127.0.0.1;
               deny    all;
               # 反向代理到真实下单的接口
               proxy_pass   http://192.168.241.150/api/create_order;
            }
        }
        ...
    }
  1. LuaJIT 的运行速度比标准 Lua 快数十倍,可以说是一个 Lua 的高效率版本

  2. LuaJIT 被设计成全兼容标准Lua 5.1,因此 LuaJIT 代码的语法和标准 Lua 的语法没多大区别

压测

可以发现,前十个是成功下单的,从第十一个开始就会返回没抢到的信息


版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


文章浏览阅读8.4k次,点赞8次,收藏7次。SourceCodester Online Tours & Travels Management System pay.php sql injectionLine 16 of pay.php invokes a SQL query built using unvalidated input. This call could allow an attacker to modify the statement’s meaning or to execute arbitrary SQL commands.SQL
文章浏览阅读3.4k次,点赞46次,收藏51次。本文为大家介绍在windwos系统搭建typecho博客+cpolar内网穿透工具将博客发布到公共网络环境,实现远程也可以访问和操作。_windows搭建typecho
文章浏览阅读1.1k次。- php是最优秀, 最原生的模板语言, 替代语法,让php更加的优雅的与html生活在一起 -->请放心, 最终生成的,或者说用户最终看到的,仍然是一个html文档, php代码中的内容不会被泄漏的。-- 将php与html代码混编的时候,大括号很容易造成配对错误,最好杜绝它 -->php标签内部代码由php.exe解释, php标签之外的代码原样输出,仍由web服务器解析。-- 所以php的流程控制语句, 都提供了替代语法,用冒号代替大括号 -->php echo '百变鹏仔'?_利用php将静态页面修改为动态页面
文章浏览阅读1.1k次,点赞18次,收藏15次。整理K8s网络相关笔记博文内容涉及 Linux network namespace 认知以及彼此通信Demo,实际中的应用理解不足小伙伴帮忙指正不必太纠结于当下,也不必太忧虑未来,当你经历过一些事情的时候,眼前的风景已经和从前不一样了。——村上春树。_linux network namespace 多端通信 模式认知
文章浏览阅读1.2k次,点赞22次,收藏19次。此网络模型提供了一个逻辑二层(L2)网络,该网络封装在跨 Kubernetes 集群节点的现有三层(L3)网络拓扑上。使用此模型,可以为容器提供一个隔离的 L2 网络,而无需分发路由。封装网络带来了少量的处理开销以及由于覆盖封装生成 IP header 造成的 IP 包大小增加。封装信息由 Kubernetes worker 之间的 UDP 端口分发,交换如何访问 MAC 地址的网络控制平面信息。此类网络模型中常用的封装是 VXLAN、Internet 协议安全性 (IPSec) 和 IP-in-IP。_k8s网络组件对比
文章浏览阅读1.1k次,点赞14次,收藏19次。当我们谈论网络安全时,我们正在讨论的是保护我们的在线空间,这是我们所有人的共享责任。网络安全涉及保护我们的信息,防止被未经授权的人访问、披露、破坏或修改。
文章浏览阅读1.3w次,点赞3次,收藏7次。尽管您可以通过 ping 命令解析出网站的 IP 地址,但是可能在浏览器中访问时仍然遇到问题,这可能是因为浏览器使用的 DNS 解析结果不同于 ping 命令使用的解析结果。可能是因为您的网络或设备上设置了防火墙,阻止了对特定网站的访问。有些国家或组织可能会对特定的域名进行屏蔽,从而阻止访问相关网站。如果您的网络使用代理服务器进行访问控制,可能会由于代理服务器的配置问题导致无法访问某些网站。即使您的网络和设备一切正常,目标网站本身可能也存在问题,例如服务器故障、维护或过载,导致无法访问。_能ping通打不开网页
文章浏览阅读839次,点赞22次,收藏19次。本系统带文档lw万字以上文末可领取本课题的JAVA源码参考。
文章浏览阅读2.1k次,点赞31次,收藏22次。基于微信小程序奶茶点餐外卖系统设计与实现(PHP后台+Mysql)可行性分析毕设源代码毕业设计,数据安全和系统稳定性以及团队能力和资源配备方面都具备较好的条件。因此,该项目的可行性较高。:黄菊华老师《Vue.js入门与商城开发实战》《微信小程序商城开发》图书作者,CSDN博客专家,在线教育专家,CSDN钻石讲师;微信小程序作为一种快捷、方便的移动应用形式,成为很多用户点餐外卖的首选。项目的界面和功能都可以定制,包安装运行!项目配有对应开发文档、开题报告、任务书、PPT、论文模版等。
文章浏览阅读1.8k次,点赞52次,收藏38次。本文主要通过对系统的前台系统和后台管理系统进行了功能性需求分析,对系统的安全性和可扩展性进行了非功能性需求分析。在详细的需求分析的基础上,根据系统的功能设计确定了数据库结构,实现完整的代码编写。Lucky+Baby母婴用品网站使用 Dreamweaver、HBuilder代码编辑器、Apache服务器等开发工具,完成了系统的主要模块的页面设计和功能实现。本文展示了首页页面的实现效果图,并通过代码和页面介绍了用户注册功能、商品搜索功能、生成订单和查看我的订单功能、在线付款功能功能的实现过程。
文章浏览阅读1.5k次,点赞45次,收藏40次。本设计主要实现集人性化、高效率、便捷等优点于一身的人事信息管理系统,完成首页、系统用户、通知公告、部门信息、员工薪资、考勤签到、员工请假、招聘信息、应聘信息等功能模块。
文章浏览阅读1k次。该错误通常出现在数据库读取结果集数据时,比如当我们写好SQL语句从数据库读取数据时,本身应该返回结果集,再给结果集中读取数据。解决思路:这种错误一般是因为echo后面输出了一个数组导致的,或者是数组作为字符串进行拼接运算时导致的。该错误直译为:警告:mysqli_fetch_assoc函数期望参数1是mysqli的结果集,但是给了一个布尔值。这种错误是PHP解析器在解析时遇到了语法错误,直译为:解析错误:语法错误,意料之外的...该错误直译为:提示:未定义的索引:username。_array to string conversion in
文章浏览阅读2.7w次。解决http请求报错context deadline exceeded (Client.Timeout exceeded while awaiting headers)_context deadline exceeded (client.timeout exceeded while awaiting headers)
文章浏览阅读1.3k次,点赞26次,收藏24次。复杂网络是一种由大量相互连接的元素(节点或顶点)组成的网络结构,这些连接通常是非常复杂和动态的。这些网络可以在各种领域中发现,包括社交网络、生物学系统、信息技术和交通系统等。_代理建模
文章浏览阅读2.6k次,点赞76次,收藏71次。epoll详解,事件模型,ET/LT模式,并通过三个示例进行代码实现。
文章浏览阅读3.3k次。罗拉ROLA-IP是一家来自纽约的代理IP提供商,由李嘉诚先生投资建设,韩国人工智能、自动驾驶、虚拟现实方面的领军企业World IT Show投资入股,由美国纽约大学IT管理教授团队研究开发,进入中国市场6年多,全世界设有多个分子公司。接下来,我们要检查代理和防火墙的设置,因为在绝大多数情况下,它们是导致这个错误的原因,尤其是当用户使用免费代理时。对网站的访问受阻实际上是一个非常常见的错误,它既可能是由于物理原因(硬件问题)造成的,也可能是由于软件错误引起的。检查代理设置,并确保其正确配置。_无法访问此网站,检查代理服务器和防火墙
文章浏览阅读1.1k次,点赞14次,收藏20次。本系统带文档lw万字以上文末可领取本课题的JAVA源码参考。_php洗车服务预约管理系统php源码
文章浏览阅读1.1k次。桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。同时,对于桶中元素的排序,选择何种比较排序算法对于性能的影响至关重要。
文章浏览阅读936次,点赞22次,收藏17次。本系统带文档lw万字以上文末可领取本课题的JAVA源码参考。
文章浏览阅读822次,点赞15次,收藏14次。在整个设计过程中,要确定可能的具体解决方案,以实现每一个小的最终目标,对于每一个小目标,我们首先必须了解一些相关的需求分析信息。除了以上作品下面是2023-2024年最新100套计算机专业原创的毕业设计源码+数据库,是近期作品,如果你的题目刚好在下面可以文末领取java源码参考。springboot基于springboot的在线考试系统。springboot基于springboot的商城购物系统。springboot基于微信小程序的智慧校园设计与实现。springboot基于用户的协同过滤算法的话题推荐。