百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分析 > 正文

Nginx Lua编程-实战案例 nginx luci

liebian365 2024-10-29 15:54 7 浏览 0 评论

案例一:Nginx+Redis进行分布式访问统计

得益于Nginx的高并发性能和Redis的高速缓存,基于Nginx+Redis实现的受访统计架构设计比纯Java实现的受访统计架构设计,在性能上高处很多。

参考案例:统计【http://localhost/visitCount】接口在10秒内的访问次数。

在Nginx的配置文件中,定义一个location匹配 /visitCount,并定义一个变量count,用于接收Lua脚本返回的从Redis中获取的访问次数。

location /visitCount {
	#访问次数
  set $count 0;
  
  access_by_lua_file luaScripe/redis/RedisVisitCount.lua;
  echo "10s内访问次数:" $count;
}

RedisVisitCount.lua脚本中的逻辑如下:

  • 根据接口划分存入Redis中的key为demo:visitCount;
  • 每次访问时,访问次数加+1,使用Redis的incr(key)实现;
  • 每次访问时,判断Redis中demo:visitCount的value是否为1,若为1,则设置demo:visitCount10秒自动过期
-- 引入自定义Redis操作模块
local redisOpe = require("luaScript.redis.RedisOperator");
-- 创建自定义的Redis操作对象
local red = redisOpe:new();

-- 获取Redis连接
red:open();

-- 获取访问次数
local visitCount = red:incrValue("demo:visitCount");

if visitCount == 1 then
	-- 设置10秒过期
	red:expire("demo:visitCount", 10);
end

-- 将访问次数设置到Nginx变量中
ngx.var.count = visitCount;

-- 连接归还到连接池
red:clost();

重启Nginx后,浏览器访问http://localhost/visitCount,显示访问的统计次数,10秒内多次刷新,发现统计次数累加,10秒后重新开始计算

案例二:Nginx+Redis+Java容器实现高并发访问

在不需要高速访问的场景下,运行在后端的Java容器会直接从DB数据库中查询数据,然后返回给客户端。但受限于数据库连接数限制、网络传输延迟、数据库的IO频繁等多方面原因,后端Java容器(Tomcat、)直接查询DB的性能会很低,此时对架构进行调整,采用"Java容器+Redis+DB"的查询架构,对数据一致性要求不是特别高但访问频繁 的API接口,可以将DB数据放入Redis缓存中,Java API优先查询Redis,若缓存未命中,则回源到DB查询,最后将成功的查询结果更新到Redis缓存中,此种架构利用Redis分流了大量的查询请求,极大提升了API接口的处理性能,请求架构如下:

但是,常用的后端Java容器的性能其实不高,QPS性能指标一般在1000以内,而Nginx的性能是Java容器的10倍左右,且性能更稳定、还不存在FullGC卡顿。为了应对高并发场景,可以将"Java容器+Redis+DB"架构调整为"Nginx+Redis+Java容器"查询架构。新架构将后端Java容器的缓存查询、缓存判断前移到反向代理Nginx,通过Nginx直接进行Redis缓存查询、缓存判断。如此,不仅为Java容器减少了很多请求,而且充分发挥了Nginx的高并发优势和稳定性优势,请求处理架构如下


下面实现一个"Nginx+Redis+Java"查询架构的案例:可以用于秒杀系统

首先定义两个接口

  • 模拟Java容器的商品查询接口:/java/goods/detail
  • 模拟供外部调用的商品查询接口:/goods/detail

然后提供一个Lua操作缓存的类RedisCacheOpe,定义三个方法:

  • getCache(self, goodsId):根据商品ID获取商品信息;
  • goUpstream(self):通过capture内部请求访问上游接口获取商品数据
  • setCache(self, goodsId, goodsStr):设置商品缓存,此方法用于模拟后台Java代码。


缓存操作类RedisCacheOpe核心代码如下:

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by xx
--- DateTime: 2022/2/19 下午12:50
---
--导入自定义的基础块
local basic = require("luaScript.module.common.basic");

local redisOp = require("luaScript.redis.RedisOperator");

local PREFIX = "GOODS_CACHE_";

local _RedisCacheOpe = { };
_RedisCacheOpe.__index = _RedisCacheOpe;

-- 类的方法
function _RedisCacheOpe.new(self)
    local object = {};
    setmetatable(object, self);
    return object;
end

-- Nginx服务器中使用Lua获取get或post请求参数
function _RedisCacheOpe.getParam(self, paramName)
    local request_method = ngx.var.request_method;
    local args = nil;

    if "GET" == request_method then
        args = ngx.req.get_uri_args();
    elseif "POST" == request_method then
        ngx.req.read_body();
        args = ngx.req.get_post_args();
    end
    if not args then
        return nil;
    end
    return args[paramName];
end

-- 获取缓存数据
function _RedisCacheOpe.getCache(self, goodsId)
    local red = redisOp:new();

    --打开连接
    if not red:open() then
        basic:error("Redis连接失败");
        return nil;
    end

    local json = red:getValue(PREFIX..goodsId);
    red:close();

    if not json or json == ngx.null then
        basic:log("商品【"..goodsId.."】没有命中缓存");
        return nil;
    end
    basic.log("商品【"..goodsId.."】成功命中缓存")
    return json;
end

-- 设置缓存数据,模拟Java后台操作
function _RedisCacheOpe.setCache(self, goodsId, goodsStr)
    local red = redisOp:new();
    if not red:open() then
        basic.error("Redis连接失败")
        return nil;
    end

    red:setValue(PREFIX..goodsId, goodsStr);
    red:expire(PREFIX..goodsId, 60);
    basic:log(goodsId.."缓存设置成功");

    -- 归还Redis连接
    red:close();
    return json;

end

-- 优先从缓存获取,否则访问上游接口
function _RedisCacheOpe.goUpstream(self)
    local request_method = ngx.var.request_method;
    local args = nil;

    if "GET" == request_method then
        args = ngx.req.get_uri_args();
    elseif "POST" == request_method then
        ngx.req.read_body();
        args = ngx.req.get_post_args();
    end

    -- 回源上游接口,
    local res = ngx.location.capture("/java/goods/detail", {
        method = ngx.HTTP_GET,
        args = args
    });
    basic:log("上游数据获取成功");

    --返回上游接口的响应体
    return res.body;
end

return _RedisCacheOpe;

Nginx配置文件通过location配置块,来使用上述Lua脚本:

# Nginx+Redis+Java架构案例
location = /goods/detail {
	content_by_lua_block {
  	local goodsId = ngx.var.arg_goodsId;

    if not goodsId then
    	ngx.say("请输入goodsId");
    	return;
    end

    local RedisCacheOpe = require("luaScript.redis.RedisCacheOpe");
    local redisCache = RedisCacheOpe:new();
    local json = redisCache:getCache(goodsId);

    -- 判断缓存是否被命中
    if not json then
      ngx.say("未命中缓存,回源到上游接口", "<br>");
    json = redisCache.goUpstream();
    else
      ngx.say("缓存已被命中!", "<br>");
    end
      
    ngx.say("商品信息:", json);
  }
}

location = /java/goods/detail {
	internal;
  content_by_lua_block {
  	local RedisCacheOpe = require "luaScript.redis.RedisCacheOpe";

    -- 模拟Java后台从DB中查询的数据
    local json = "{goodsId:商品ID, goodsName:商品名称}";

    -- 将商品信息缓存到Redis
    local redisCache = RedisCacheOpe:new();
    redisCache:setCache(ngx.var.arg_goodsId, json);

    -- 返回商品到下游网关
    ngx.say(json);
  }
}

浏览器中第一次访问http://localhost/goods/detail?goodsId=33,显示缓存未命中

再次刷新浏览器:

案例三:Nginx+Redis实现黑名单拦截

实现IP黑名单拦截可以有以下途径:

  • 操作系统层,配置iptables防火墙规则,拒绝黑名单中IP的网络请求;
  • 使用Nginx网关的deny配置指令,拒绝黑名单中IP的网络请求;
  • 在Nginx网关的access阶段,通过Lua脚本检查客户端IP是否在黑名单中;
  • 在Spring Cloud的内部网关(zuul)的过滤器中检查客户端IP是否在黑名单中

以上检查方式都是基于静态的、提前准备好的黑名单进行的。在系统实际运行中,黑名单往往需要动态计算,系统需要动态识别出大量发起请求的恶意爬虫或者恶意用户,并且将这些恶意请求的IP放入一个动态的IP黑名单中。

Nginx网关可以依据动态黑名单内的IP进行请求拦截并拒绝服务,这里结合Nginx+Redis提供一个基于动态IP黑名单进行请求拦截的实现。假设黑名单IP已经生成并且定期更新到Redis中,Nginx网关可以直接从Redis获取计算好的IP黑名单,单位了提升黑名单的读取速度,并不是每一次请求都从Redis读取IP黑名单,而是从本地的共享内存black_ip_list中获取,


以下为"Nginx+Redis"实现黑名单拦截的参考实现:定义一个black_ip_filter.lua

---
--- Generated by EmmyLua(https://github.com/EmmyLua)
--- Created by xx.
--- DateTime: 2022/2/19 下午2:54
---
-- 导入自定义模块
local basic = require("luaScript.module.common.basic")
local redisOp = require("luaScript.redis.RedisOperator")

local ip = basic.getClientIP();
basic.log("获取到客户端IP: "..ip);

basic.log("首先获取Nginx共享内存中的黑名单列表,并判断...")
-- 此处获取Nginx配置文件中定义的共享内存内存变量:black_ip_list
local black_ip_list = ngx.shared.black_ip_list;
basic.log("Nginx共享内存中的黑名单列表【black_ip_list】 type is "..type(black_ip_list))
basic.log("Nginx共享内存中的黑名单列表【black_ip_list】: "..basic.tableToStr(black_ip_list));

-- 获取本地缓存的刷新时间
local last_update_time = black_ip_list:get("last_update_time");
basic.log("Nginx共享内存中的黑名单列表last_update_time type is "..type(last_update_time))
basic.log("Nginx共享内存中的黑名单列表last_update_time: "..basic.toStringEx(last_update_time))


if last_update_time ~= nil then  -- last_update_time不等于nil
    basic.log("last_update_time不等于nil...")
    local now = ngx.now();
    basic.log("now() result type is ".. type(now)..", and now = "..now);
    local dif_time = ngx.now() - last_update_time;
    basic.log("dif_time = "..dif_time);
    if dif_time < 60 then --缓存1分钟,未过期
        if black_ip_list:get(ip) then -- 命中Nginx本地缓存的黑名单
            basic.log("IP: "..ip.."命中Nginx本地缓存的黑名单!")
            return ngx.exit(ngx.HTTP_FORBIDDEN);
        end
        return;
    end
end

basic.log("未命中Nginx本地缓存黑名单,继续判断是否命中Redis中的黑名单...")
local KEY = "limit:ip:blacklist";
local red = redisOp:new();

red:open();

local ip_blacklist = red:getSmembers(KEY);
red:close();
basic.log("Redis缓存中的黑名单列表【ip_blacklist】 type is "..type(ip_blacklist))
basic.log("Redis缓存中的黑名单列表【ip_blacklist】: "..basic.tableToStr(ip_blacklist));
basic.log("Redis缓存中的黑名单列表【ip_blacklist】长度为: "..basic.table_length(ip_blacklist))

if basic.table_length(ip_blacklist) == 0 then
    --此处这么写有问题,ip_blacklist是table,not ip_blacklist 不能判断出table是空的,改为使用table的长度来获得
    --但是使用table.getn(tableName)方法只能获得有序table的大小,对于无序table,需要遍历累加获得长度
--if not ip_blacklist then
    basic.log("Redis缓存中的黑名单列表为空,不拦截...")
    basic.log("black ip set is null");
    return;
else
    basic.log("Redis缓存中的黑名单列表不为空,刷新本地缓存...")
    --刷新本地缓存
    black_ip_list:flush_all();

    basic.log("同步Redis黑名单到本地缓存...")
    for i, ip in ipairs(ip_blacklist) do
        basic.log("第"..i.."个IP:"..ip.."同步成功");
        black_ip_list:set(ip, true);
    end

    basic.log("设置本地缓存的最新更新时间")
    black_ip_list:set("last_update_time", ngx.now());
end

if black_ip_list:get(ip) then
    return ngx.exit(ngx.HTTP_FORBIDDEN); --直接返回403
end

Nginx配置文件中,通过location配置块,来使用上述脚本:

#Nginx+Redis实现IP黑名单拦截示例
location /black_ip_demo {
  access_by_lua_file luaScript/redis/black_ip_filter.lua;
  echo "恭喜您,没有被拦截!";
}

当在浏览器访问http://localhost/black_ip_demo时,当未从Redis中获取到黑名单时,请求是放行的:

请求一次后,Redis中的黑名单列表会同步到Nginx本地共享变量中,再次访问会拦截

好啦,Nginx篇介绍完毕,后面会介绍综合实战~

相关推荐

快递查询教程,批量查询物流,一键管理快递

作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...

一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递

对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...

快递查询单号查询,怎么查物流到哪了

输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...

3分钟查询物流,教你一键批量查询全部物流信息

很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...

快递单号查询,一次性查询全部物流信息

现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...

快递查询工具,批量查询多个快递快递单号的物流状态、签收时间

最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...

快递查询软件,自动识别查询快递单号查询方法

当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...

教你怎样查询快递查询单号并保存物流信息

商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...

简单几步骤查询所有快递物流信息

在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...

物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号

最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...

连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息

快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...

快递查询教程,快递单号查询,筛选更新量为1的单号

最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...

掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析

在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...

从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息

在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...

物流单号查询,在哪里查询快递

如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...

取消回复欢迎 发表评论: