二十六,OpenResty

266 阅读8分钟

一,安装OpenResty

image

安装步骤:

  1. 安装开发库

    yum install -y pcre-devel openssl-devel gcc --skip-broken
    
  2. 安装OpenResty仓库

    在Linux系统中添加OpenResty仓库,这样就可以方便未来安装或更新软件包(通过yum check-update命令)

    yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
    

    如果提示说命令不存在,则先运行

    yum install -y yum-utils
    
  3. 安装OpenResty

    yum install -y openresty
    
  4. 安装opm工具

    opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方lua模块。

    yum install -y openresty-opm
    
  5. 目录结构

    默认情况下,OpenResty安装目录是/usr/local/openresty

    image

  6. 配置nginx的环境变量

    • 打开配置文件

      vi /etc/profile
      
    • 在末尾加上

      export NGINX_HOME=/usr/local/openresty/nginx
      export PATH=${NGINX_HOME}/sbin:$PATH
      
    • 让配置生效

      source /etc/profile
      

三,启动和运行

OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与Window中安装的nginx基本一致

image

所有运行方式与nginx基本一致

#启动nginx
nginx
#重新加载配置
nginx -s reload
#停止
nginx -s stop

四,OpenResty快速入门

我们配置一个Nginx统一接受前端传递的请求,然后发送这个请求到OpenResty集群,将部分转发等逻辑编写到OpenResty集群中。

image

image


  1. 在openresty里面的nginx目录里面的:nginx.conf的http下面,添加对OpenResty的Lua模块的加载

    # 加载Lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    # 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    
  2. nginx.conf的server下面,添加对/api/item这个路径的监听

    location /api/item{
    	#响应类型,这里返回json
    	default_type application/json;
    	#响应数据由lua/item.lua这个文件决定
    	content_by_lua_file lua/item.lua;
    }
    
  3. 在nginx目录下创建lua目录

    mkdir lua
    
  4. 在lua文件夹下创建item.lua

    touch lua/item.lua
    
  5. 编写内容

    -- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
    ngx.say('{"id":1001,"name":"sala"}')
    
  6. 重新加载配置

    nginx -s reload
    

五,请求参数处理

image

六,查询Tomcat

案例:获取请求路径中的商品id信息,根据id向Tomcat查询商品信息

这里要修改item.lua,满足下面的需求:

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

6.1 封装Http查询的函数

Nginx内部发送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:响应头
  • resp.body:响应体

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

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

location /path{
	proxy_pass http://192.168.150.1:8081;
}

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

  1. /usr/local/openresty/lualib目录下创建common.lua文件

    vi /usr/local/openresty/lualib/common.lua
    
  2. 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
    

6.2 使用封装的Http查询函数

编写item.lua

-- 导入common函数库
local common = require('common')
local read_http=common.read_http
-- 引入cjson处理json的序列化与反序列化
local cjson = require("cjson")
-- 获取路径参数
local id = ngx.var[1]

-- 查询商品信息
local itemJSON = read_http("/item" .. id , nil)
-- 查询库存信息
local stockJSON = read_http("/stock" .. id , nil)

--JSON转化为lua的table
local item = cjson.decode(ItemJson)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json返回结果

ngx.say(cjson.encode(item))

6.3 根据商品Id对tomcat集群负载均衡

实际过程中,tomcat是集群,我们tomcat集群中存在JVM进程缓存,但是nginx使用的是轮询负载均衡,这样会导致JVM进程缓存的命中率极低,我们需要将针对1001这个查询一直查询指定的一个tomcat服务器,这个时候只需要加上hash $request_uri,他会对1001%tomcat服务器数量,让1001这个id一直访问指定tomcat服务器。

image

七,Redis缓存预热

上述我们只是完成了对Tomcat集群的查询,但是我们需要优先查询Redis,Redis未命中才查询tomcat

image

7.1 冷启动与缓存预热

冷启动:服务器刚刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前在查询钱保存到Redis中

7.2 缓存预热实现

  1. 安装redis

  2. 在项目中配置redis

  3. 编写初始化类

    @Component
    public class RedisHandler implements InitalizingBean{
        @Autowired
        private StringRdisTemplate redisTemplate;
        @Autowired
        private IItemStockService stockService;
        
        private static final ObjectMapper MAPPER=new ObjectMapper();
        
        @Override
        public void afterPropertiesSet() throw Exception{
            //初始化缓存
            //1.查询商品信息
            List<Item> itemList=itemService.list();
            //2.放入缓存
            for(Item item : itemList){
                //2.1 item序列化为JSON
                String json =MAPPER.writeValueAsString(item);
                //2.2 存入redis
                redisTemplate.opsForValue().set("item:id:"+item.getId(),json);
            }
            //3. 查询商品库存信息
            List<Stock> itemList=stockService.list();
            //4. 放入缓存
            for(ItemStock stock : stockList){
                //2.1 item序列化为JSON
                String json =MAPPER.writeValueAsString(stock);
                //2.2 存入redis
                redisTemplate.opsForValue().set("item:stock:id:"+stock.getId(),json);
            }
        }
        
    }
    

八,查询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 = 1000 -- 连接的空闲时间,单位是毫秒
        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 nill
        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
    
  • 修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,在查询tomcat

  • 修改item.lua,查询商品和库存时都调用read_data这个函数

    -- 导入common函数库
    local common = require('common')
    local read_http=common.read_http
    local read_redis=common.read_redis
    -- 引入cjson处理json的序列化与反序列化
    local cjson = require("cjson")
    -- 获取路径参数
    local id = ngx.var[1]
    
    -- 封装函数,先查询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
    
    -- 查询商品信息
    local itemJSON = read_data("/item" .. id , nil)
    -- 查询库存信息
    local stockJSON = read_data("/stock" .. id , nil)
    
    --JSON转化为lua的table
    local item = cjson.decode(ItemJson)
    local stock = cjson.decode(stockJSON)
    -- 组合数据
    item.stock = stock.stock
    item.sold = stock.sold
    
    -- 把item序列化为json返回结果
    
    ngx.say(cjson.encode(item))
    

九,Nginx本地缓存

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')
    

需求:

  • 修改item.lua中的read_data函数,有限查询本地缓存,未命中再查询Redis,Tomcat
  • 查询Redis或者Tomcat成功后,将数据写入本地缓存,并设置有效期
  • 商品基本信息,有效期30分钟
  • 库存信息,有效期1分钟
-- 导入common函数库
local common = require('common')
local read_http=common.read_http
local read_redis=common.read_redis
-- 引入cjson处理json的序列化与反序列化
local cjson = require("cjson")
-- 获取路径参数
local id = ngx.var[1]

-- 封装函数
local function read_data(key,expire,path,params)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR,"本地缓存查询失败,尝试查询redis,key:",key)
    	-- 查询redis
    	val = read_redis("127.0.0.1",6379,key)
    	-- 判断redis是否命中
    	if not val then
        	-- Redis查询失败,查询http
        	val = read_http(path,params)
    	end
    end
    -- 查询成功,把数据写入本地缓存
    item_cache:set(key,val,expire)
    return resp
end

-- 查询商品信息
local itemJSON = read_data("/item" .. id ,1800, nil)
-- 查询库存信息
local stockJSON = read_data("/stock" .. id , 60,nil)

--JSON转化为lua的table
local item = cjson.decode(ItemJson)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json返回结果

ngx.say(cjson.encode(item))