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

Nginx Lua编程-实战案例 nginx luci

liebian365 2024-10-29 15:54 31 浏览 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篇介绍完毕,后面会介绍综合实战~

相关推荐

精品博文嵌入式6410中蓝牙的使用

BluetoothUSB适配器拥有一个BluetoothCSR芯片组,并使用USB传输器来传输HCI数据分组。因此,LinuxUSB层、BlueZUSB传输器驱动程序以及B...

win10跟这台计算机连接的前一个usb设备工作不正常怎么办?

前几天小编闲来无事就跑到网站底下查看粉丝朋友给小编我留言询问的问题,还真的就给小编看到一个问题,那就是win10跟这台计算机连接的一个usb设备运行不正常怎么办,其实这个问题的解决方法时十分简单的,接...

制作成本上千元的键盘,厉害在哪?

这是稚晖君亲自写的开源资料!下方超长超详细教程预警!!全文导航:项目简介、项目原理说明、硬件说明、软件说明项目简介瀚文智能键盘是一把我为自己设计的——多功能、模块化机械键盘。键盘使用模块化设计。左侧的...

E-Marker芯片,USB数据线的“性能中枢”?

根据线缆行业的研究数据,在2019年搭载Type-C接口的设备出货量已达到20亿台,其中80%的笔记本电脑和台式电脑采用Type-C接口,50%的智能手机和平板电脑也使用Type-C接口。我们都知道,...

ZQWL-USBCANFD二次开发通讯协议V1.04

修订历史:1.功能介绍1.1型号说明本文档适用以下型号:  ZQWL-CAN(FD)系列产品,USB通讯采用CDC类实现,可以在PC机上虚拟出一个串口,串口参数N,8,1格式,波特率可以根据需要设置(...

win10系统无法识别usb设备怎么办(win10不能识别usb)

从驱动入手,那么win10系统无法识别usb设备怎么办呢?今天就为大家分享win10系统无法识别usb设备的解决方法。1、右键选择设备管理器,如图:  2、点击更新驱动程序,如图:  3、选择浏览...

微软七月Win8.1可选补丁有内涵,含大量修复

IT之家(www.ithome.com):微软七月Win8.1可选补丁有内涵,含大量修复昨日,微软如期为Win7、Win8.1发布7月份安全更新,累计为6枚安全补丁,分别修复总计29枚安全漏洞,其中2...

如何从零开始做一个 USB 键盘?(怎么制作usb)

分两种情况:1、做一个真正的USB键盘,这种设计基本上不涉及大量的软件编码。2、做一个模拟的USB键盘,实际上可以没有按键功能,这种的需要考虑大量的软件编码,实际上是一个单片机。第一种设计:买现成的U...

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题

电脑识别U盘失败?5个实用小技巧,让你轻松搞定USB识别难题注意:有些方法会清除USB设备里的数据,请谨慎操作,如果不想丢失数据,可以先连接到其他电脑,看能否将数据复制出来,或者用一些数据恢复软件去扫...

未知usb设备设备描述符请求失败怎么解决

出现未知daousb设备设备描述符请求失du败解决办zhi法如下:1、按下Windows+R打开【运行】;2、在版本运行的权限输入框中输入:services.msc按下回车键打开【服务】;2、在服务...

读《飘》47章20(飘每章概括)

AndAhwouldn'tleaveMissEllen'sgrandchildrenfornotrashystep-patobringup,never.Here,Ah...

英翻中 消失的过去 37(消失的英文怎么说?)

翻译(三十七):消失的过去/茱迪o皮考特VanishingActs/JodiPicoult”我能做什么?“直到听到了狄利亚轻柔的声音,我才意识到她已经在厨房里站了好一会儿了。当她说话的时候,...

RabbitMQ 延迟消息实战(rabbitmq如何保证消息不被重复消费)

现实生活中有一些场景需要延迟或在特定时间发送消息,例如智能热水器需要30分钟后打开,未支付的订单或发送短信、电子邮件和推送通知下午2:00开始的促销活动。RabbitMQ本身没有直接支持延迟...

Java对象拷贝原理剖析及最佳实践(java对象拷贝方法)

作者:宁海翔1前言对象拷贝,是我们在开发过程中,绕不开的过程,既存在于Po、Dto、Do、Vo各个表现层数据的转换,也存在于系统交互如序列化、反序列化。Java对象拷贝分为深拷贝和浅拷贝,目前常用的...

如何将 Qt 3D 渲染与 Qt Quick 2D 元素结合创建太阳系行星元素?

Qt组件推荐:QtitanRibbon:遵循MicrosoftRibbonUIParadigmforQt技术的RibbonUI组件,致力于为Windows、Linux和MacOSX提...

取消回复欢迎 发表评论: