4.3 多级缓存

一、安装OpenResty

  1. OpenResty是一个基于Nginx的高性能Web平台,用于方便的搭建能够处理高并发。扩展性极高的动态Web应用、Web服务和动态网关。具备下列特点,官网地址:http://openresty.org/cn/

    1. 具备Nginx的完整功能

    2. 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块

    3. 允许使用Lua自定义业务逻辑,自定义库

  2. Linux安装:查看附录

二、OpenResty快速入门

假如反向代理的nginx中有HTML页面,内容从OpenResty中获取

  1. 修改nginx.conf文件

    1. 修改nginx.conf的http下面,添加对OpenResty的Lua模块的加载:

      # 加载lua模块
      lua_package_path "/usr/local/openresty/lualib/?.lua;;";
      # 加载C模块
      lua_package_cpath "/usr/local/openresty/lualib/?.os;;";
    2. 在nginx.conf的server下面,添加对指定路径的监听:

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

      备注:这里的/api/item表示被监听的路径

  2. 在OpenResty的nginx目录下创建一个lua文件,命名item.lua(上一步配置)

  3. 编写文件

    -- 编写假数据,直接返回假数据做测试,ngx.say函数,就是写数据到Response中
    ngx.say('{"id":1001, "name": "testName"}')
  4. 重新加载nginx

    nginx -s reload

三、请求参数处理

  1. OpenResty提供了各种API来获取不同类型的请求参数:

    参数格式
    参数示例
    参数解析代码示例

    路径占位符

    /item/1001

    1. 正则表达式匹配: localtion ~ /item/(\d+) { content_by_lua_file lua/item.lua; } 2.匹配到的参数会存入ngx.var数组中,可以用角标获取 local id = ngx.ver[1]

    请求头

    Id: 1001

    -- 获取请求头返回值是table类型 local headers = ngx.req.get_headers()

    Get请求参数

    ?id=1001

    -- 获取GET请求参数,返回值是table类型 local getParams = ngx.req.get_uri_args()

    Post表单参数

    id=1001

    -- 读取请求体 ngx.req.read_body() -- 获取POST表单参数,返回值是table类型 local postParams = ngx.req.get_post_args()

    JSON参数

    {"id": 1001}

    -- 读取请求体 ngx.req.read_body() -- 获取body中的json参数,返回值是string类型 local jsonBody = ngx.req.get_body_data()

四、查询Tomcat

  1. nginx内部发送Http请求

    1. 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:响应体,就是响应数据

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

      所以,需要在nginx.conf中添加配置,转发到对应的服务器中

      location /path {
      	# 这里是对应服务的ip和Java服务端口
      	proxy_pass http://ip:端口
      }
    3. 因为http请求可能会经常使用,因此,给封装成函数

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

      2. 在内部封装http查询函数

        -- 封装函数,发送http请求,并解析响应
        local function read_http(path, params)
            local resp = ngx.location.capture(path,{  -- 传参路径path
                method = ngx.HTTP_GET,   -- 做GET请求
                args = params,    -- get请求参数
            })
            if not resp then    -- 如果resp为空(nil),not nil = true
                -- 记录错误信息,返回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
      3. 使用方式:

        -- 导入common函数库
        local common = require("common")
        local read_http = common.read_http
        -- 导入cjson库
        local cjson = require("cjson")
        
        -- 获取参数路径
        local id = ngx.var[1]
        
        -- 查询1
        local data = read_http("/item/" .. id, nil)
        
        -- 查询2
        local data2 = …………
        
        -- 拼接数据(结合下一知识点,json解析)
        local data1Table = cjson.decode(data1)
        local data2Table = cjson.decode(data2)
        
        -- 赋值获取数据啥的,直接.名字即可
        data1Table.stock = data2Table.stock
        data1Table.sold = data2Table.sold
        
        -- 返回结果
        ngx.say(cjson.encode(data1Table))

        备注:导包时候需要用变量来接收,require后面的是文件的名字!(如果有目录,需要带上目录名)

    4. OpenResty对多段json结果合并处理

      1. OpenResty提供了cjson模块来进行json的序列化和反序列化,官网地址:https://github.com/openresty/lua-cjson

      2. 引入cjson模块

        local cjson = require("cjson")
      3. 序列化

        local obj = {
            name = 'jack',
            age = 21
        }
        local json = cjson.encode(obj)
      4. 反序列化

        local json = '{"name": "jack", "age": 21}'
        local obj = cjson.decode(json)
  2. Tomcat集群的负载均衡

    1. 配置负载均衡

      # tomcat集群配置
      upstream tomcat-cluster {
      	server ip1:端口;
      	server ip2:端口;
      	…………
      }
      
      
      # 反向代理配置,将请求代理到集群中
      location /item {
      	proxy_pass http://tomcat-cluster;
      }

      备注:因为JVM的进程缓存无法共享,而这样配置Nginx又是造成轮询请求,因此同样的请求可能访问在不同的服务上,造成进程缓存冗余

    2. 上述备注问题解决办法

      # tomcat集群配置
      upstream tomcat-cluster {
      	hash $request_uri;
      	server ip1:端口;
      	server ip2:端口;
      	…………
      }

      备注:对请求的路径做哈希运算,然后对集群数量取余,就可以指定某个请求请求指定服务器

五、Redis预存预热

  1. 冷启动与预热缓存

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

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

  2. 缓存预热

    1. 在Java代码项目中添加Redis的坐标以及链接操作等,在Java服务启动时候,就会将数据存入Redis中

      // 编写初始化缓存的业务逻辑
      @Component
      public class RedisHandler implments InitializingBean {
          
          @Autowirred
          private StringRedisTemplate redisTemplate;
          
          // 注入查询用的service
          
          // 引入Spring自带的json处理工具
          private static final ObjectMapper MAPPER = new ObkectMapper();
          
          @Override
          public void afterPropertiesSet() throes Exception() {
              // 初始化缓存操作
              // 1.查询商品信息(大量数据时,查热点信息)
              
              // 2.放入缓存
              // 2.1 循环遍历,将实体转化为JSON,使用Spring自带的工具转换
              String json = MAPPER.writeValueAsString(循环出来的实体类);
              // 2,2 将JSON存入redis中,将商品id为key添加
              redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
          }
      }

六、查询Redis缓存

  1. OpenResty提供了操作Redis的模块,只要我们引入该模块就可以使用

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

      -- 引入Redis模块
      local redis = require("resty.redis")
      -- 初始化Redis对象
      local red = redis:new()
      -- 设置Redis超时时间
      red:set_timeouts(1000, 1000, 1000)
    2. 封装函数,用来释放Redis链接,其实是放入连接池

      -- 关闭redis连接工具的方法,其实时放入链接池
      lcoal function closr_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
    3. 封装函数,从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

    备注:上面的内容存放到common中,添加暴露read方式

    直接在
    local _M = {  
        read_http = read_http  
    }
    中添加redis的读取函数名即可
  2. OpenResty查询,先查询Redis操作

    -- 封装函数,先查询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

七、Nginx本地缓存——在OpenResty中添加本地缓存

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

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

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

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

最后更新于

这有帮助吗?