OpenResty

予早 2024-11-29 01:15:23
Categories: Tags:

OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty 接管了缓存和Tomcat,将缓存的压力转移到了自身。由以下步骤实现:

  1. Nginx 反向代理 OpenResty 集群
  2. OpenResty 解析请求尝试从本地缓存获取对应数据,命中缓存则直接返回数据
  3. 未命中本地缓存尝试从Redis 获取对应缓存
  4. 命中缓存返回对应数据
  5. 未命中缓存则请求 Tomcat 获取数据

商品详情页请求

商品详情页面中查询商品信息的请求

Nginx 反向代理

请求最终被反向代理到OpenResty集群

OpenResty

一、在nginx.conf的http下面,添加对OpenResty的Lua模块的加载

# 加载lua 模块  
 lua_package_path "/usr/local/openresty/lualib/?.lua;;";  
 # 加载c模块 
 lua_package_cpath "/usr/local/openresty/lualib/?.so;;";

二、在nginx.conf的server下面,添加对/api/item这个路径的监听

 location /api/item {
     # 响应类型,这里返回json
     default_type application/json;
     # 响应数据由 lua/item.lua这个文件来决定
     content_by_lua_file lua/item.lua;
 }

三、实现lua/item.lua

openresty/nginx下,建立lua目录,新建item.lua文件

-- 返回假数据,ngx.say()用于写数据到response中
ngx.say('{"id": 10001, "name": "豆干"}')

需要的Lua脚本的功能:

  1. 获取请求参数中的id
  2. 根据id向Tomcat服务发送请求,查询商品信息
  3. 根据id向Tomcat服务发送请求,查询库存信息
  4. 组装商品信息、库存信息,序列化为JSON格式并返回

获取请求参数中的id

nginx内部发送Http请求

nginx提供了内部API用以发送http请求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 请求方式
    args = {a=1,b=2},  -- get方式传参数
    body = "c=3&d=4" -- post方式传参数
})

返回的响应内容包括:

resp.status:响应状态码

resp.header:响应头,是一个table

resp.body:响应体,就是响应数据

注意:这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。

但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:

 location /path {
     # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
     proxy_pass http://192.168.150.1:8081; 
 }

我们可以把http查询的请求封装为一个函数,放到OpenResty函数库中,方便后期使用。

在/usr/local/openresty/lualib目录下创建common.lua文件,在common.lua中封装http查询的函数:

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
--- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M
-- 引入自定义工具模块
local common = require("common")
local read_http = common.read_http
--- 获取路径参数
local id = ngx.var[1]
--- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。

官方地址: https://github.com/openresty/lua-cjson/

引入cjson模块:

local cjson = require "cjson"

序列化:

local obj = {
    name = 'jack',
    age = 21
}
local json = cjson.encode(obj)

反序列化:

local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)

OpenResty 操作 Redis

OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:

引入Redis模块,并初始化Redis对象

-- 引入redis模块
local redis = require("resty.redis")
-- 初始化Redis对象
local red = redis:new()
-- 设置Redis超时时间
red:set_timeouts(1000, 1000, 1000)

封装函数,用来释放Redis连接,其实是放入连接池

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)  
    local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒  
    local pool_size = 100 --连接池大小  
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)  
    if not ok then  
        ngx.log(ngx.ERR, "放入Redis连接池失败: ", err)  
    end  
end  

封装函数,从Redis读数据并返回

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)  
    -- 获取一个连接
    local ok, err = red:connect(ip, port)  
    if not ok then  
        ngx.log(ngx.ERR, "连接redis失败 : ", err)  
        return nil
    end
    -- 查询redis
    local resp, err = red:get(key) 
    -- 查询失败处理
    if not resp then  
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end  
    --得到的数据为空处理  
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end  
    close_redis(red)  
    return resp
end
-- 封装函数,先查询redis,再查询http
local function read_data(key, path, params)
    -- 查询redis
    local resp = read_redis("127.0.0.1", 6379, key)
    -- 判断redis是否命中
    if not resp then
        -- Redis查询失败,查询http
        resp = read_http(path, params)
    end
    return resp
end

本地缓存

OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能。

开启共享字典,在nginx.conf的http下添加配置:

 # 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 

操作共享字典:

-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

最终实现

先查询本地缓存,再查询redis,再查询http

-- 封装函数,先查询本地缓存,再查询redis,再查询http
local function read_data(key, expire, path, params)
    -- 读取本地缓存
    local val = item_cache:get(key)
    if not val then
        -- 缓存未命中,记录日志
        ngx.log(ngx.ERR, "本地缓存查询失败, key: ", key , ", 尝试redis查询")
        -- 查询redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判断redis是否命中
        if not val then
            ngx.log(ngx.ERR, "Redis缓存查询失败, key: ", key , ", 尝试http查询")
            -- Redis查询失败,查询http
            val = read_http(path, params)
        end
    end
    -- 写入本地缓存
    item_cache:set(key, val, expire)
    return val
end
-- 根据id查询商品,30min缓存实践
local itemJSON = read_data('item:id:' .. id, 1800, "/item/".. id, nil)
-- 根据id查询商品库存,1min缓存时间
local itemStockJSON = read_data('item:stock:id:' .. id, 60, "/item/stock/".. id, nil)