玖叶教程网

前端编程开发入门

Nginx Lua编程-实战案例

案例一: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篇介绍完毕,后面会介绍综合实战~

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言