Redis从入门到精通(十八)多级缓存(三)OpenResty请求参数处理、Lua脚本查询Redis和Tomcat

时间:2024-04-20 14:04:33

文章目录

    • 前言
    • 6.5 实现多级缓存
      • 6.5.3 请求参数处理
        • 6.5.3.1 获取参数API
        • 6.5.3.2 获取参数并返回
      • 6.5.4 查询Tomcat
        • 6.5.4.1 发送HTTP请求的API
        • 6.5.4.2 封装HTTP工具
        • 6.5.4.3 实现商品查询
        • 6.5.4.4 使用CJSON工具类
        • 6.5.4.5 基于商品ID实现负载均衡
      • 6.5.5 查询Redis
        • 6.5.5.1 Redis缓存预热
        • 6.5.5.2 封装Redis工具
        • 6.5.5.3 实现Redis查询
        • 6.5.5.4 功能测试

前言

Redis多级缓存系列文章:

Redis从入门到精通(十六)多级缓存(一)Caffeine、JVM进程缓存
Redis从入门到精通(十七)多级缓存(二)Lua语言入门、OpenResty集群的安装与使用

6.5 实现多级缓存

6.5.3 请求参数处理

上一节中,OpenResty集群接收前端请求,但是返回的是假数据。而要返回真实数据,必须根据前端传递来的商品ID,查询商品信息才可以。

6.5.3.1 获取参数API

OpenResty提供了一系列API来获取不同类型的前端请求参数:

6.5.3.2 获取参数并返回

在测试项目中,根据ID查询商品信息的请求是:/api/item/1,可见商品ID是以路径占位符的方式传递的,因此可以利用正则表达式匹配的方式来获取ID。

  • 1)获取商品ID

修改/usr/loca/openresty/nginx/nginx.conf文件中监听/api/item的代码,利用正则表达式获取商品ID:

location /api/item/(\d+) {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}
  • 2)拼接ID并返回

修改/usr/loca/openresty/nginx/lua/item.lua文件,获取商品ID并拼接到结果中返回:

-- 获取商品ID
local id = ngx.var[1]
-- 拼接并返回
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"(集群中的)RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_
jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTim
e":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')
  • 3)功能测试

执行nginx -s reload命令重新加载,并刷新页面:

可见,OpenResty集群已经获取到了前端传递的参数并拼接后返回。

6.5.4 查询Tomcat

OpenResty集群获取到商品ID后,本应该去Nginx本地缓存、Redis缓存中查询商品信息,但目前测试项目还未建立Nginx、Redis缓存。

因此,这里可以先根据商品ID去Tomcat服务器查询商品信息,如图:

需要注意的是,OpenResty集群部署在虚拟机,IP地址是192.168.146.128,而Tomcat是直接运行在Windows系统上的,其IP地址的前三位和虚拟机一致,最后一位改为1即可,即192.168.146.1。

6.5.4.1 发送HTTP请求的API

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:响应体,即响应数据

以该API发送的HTTP请求会被Nginx内部的server监听并处理,因此要把这个请求发送到Tomcat,则需要在server中对这个路径做反向代理。

在Tomcat服务器中,查询商品信息的请求路径前缀是/dzdp/item,那么OpenResty集群中的server就可以这样配置:

location /dzdp/item {
    # Tomcat服务器的IP和端口
    proxy_pass: http://192.168.146.1:8081/dzdp/item;
}

经过这样的配置之后,只要调用ngx.location.capture("/dzdp/item")发起请求,就会被反向代理到Windows上的Tomcat服务器。

6.5.4.2 封装HTTP工具

OpenResty启动时,会加载/usr/local/openresty/lualib目录下的工具文件,因此自定义的HTTP工具也要放在这个目录下。

/usr/local/openresty/lualib目录下,新建一个common.lua文件,内容如下:

-- /usr/local/openresty/lualib/common.lua

-- 封装函数,发送GET请求,并解析响应
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请求查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {
    read_http = read_http
}
return _M

使用的时候,可以利用require('common')来导入该函数库,这里的common就是函数库的文件名。

6.5.4.3 实现商品查询

修改/usr/local/openresty/nginx/lua/item.lua文件,利用刚封装好HTTP工具实现对Tomcat的查询:

-- /usr/local/openresty/nginx/lua/item.lua

-- 1.引入自定义的工具类
local common = require("common")
local read_http = common.read_http
-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
local itemJSON = read_http("/dzdp/item/".. id, nil)
-- 4.返回商品信息
ngx.say(itemJSON)

执行nginx -s reload重新加载,在页面发起请求ID=3的商品信息:

在请求ID=4的商品信息:

可见,Tomcat服务器确实接收到了请求。以上的配置均生效了。

6.5.4.4 使用CJSON工具类

OpenResty提供了一个cjson的模块用来处理JSON的序列化和反序列化。 /usr/local/openresty/lualib目录下已经包含了该模块,可以直接使用:

因此,这里对/usr/local/openresty/nginx/lua/item.lua文件进行进一步优化:

-- /usr/local/openresty/nginx/lua/item.lua

-- 1.引入自定义的工具类和cjson
local common = require("common")
local read_http = common.read_http
local cjson = require('cjson')
-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
local itemJSON = read_http("/dzdp/item/".. id, nil)
-- 4.根据ID发起请求查询商品库存信息
local itemStockJSON = read_http("/dzdp/item/stock/".. id, nil)
-- 5.将JSON转换为table
local item = cjson.decode(itemJSON)
local itemStock = cjson.decode(itemStockJSON)
-- 6.组合数据
item.stock = itemStock.stock
item.sold = itemStock.sold
-- 7.把item序列化为Json,并返回
ngx.say(cjson.encode(item))

执行nginx -s reload重新加载,在页面发起请求ID=5的商品信息:

可见,跟前面比起来,返回的数据中多了库存信息,说明以上配置也生效了。

6.5.4.5 基于商品ID实现负载均衡

在以上测试中,Tomcat是单机部署的。而在实际项目中,Tomcat通常是集群部署。因此,OpeResty需要对Tomcat做负载均衡。

OpenResty默认的负载均衡策略是轮询。但由于Tomcat中的JVM进程缓存是不会共享的,所以对于同一个请求,在一部分Tomcat服务中可以命中JVM进程缓存,在一部分又无法命中,因此缓存的命中率较低。

  • 1)原理分析

那要怎么解决呢?如果能让同一个商品,每次查询都访问同一个Tomcat服务,那么JVM进程缓存就一定能生效。也就是说,要根据商品ID做负载均衡,而不是轮询。

Nginx提供了基于请求路径做负载均衡的算法:根据请求路径做Hash运算,把得到的数值对Tomcat服务的数量取余,余数是几,就访问第几个服务,从而实现负载均衡。

例如:请求路路径为/dzdp/item/1,Tomcat服务总数为2(端口分别是8080、8081),对请求路径做Hash运算并对2取余的结果为1,则访问第一台Tomcat服务器,即8080。

后续请求只要商品ID不变,请求路径就不变,那取余运算结果就不变,最终访问的Tomcat服务就不变,这就实现了根据商品ID做负载均衡的功能。

  • 2)代码实现

修改/usr/local/openresty/nginx/conf/nginx.conf文件,实现基于商品ID做负载均衡:

http {
    # ...
    
    # 定义Tomcat集群,设置基于路径做负载均衡
    upstream tomcat-cluster {
	    hash $request_uri;
	    server 192.168.146.1:8080;
	    server 192.168.146.1:8081;
    }
    
    server {
        listen  8082;
        location /dzdp/item {
            # Tomcat服务器的IP和端口
            # proxy_pass http://192.168.146.1:8081/dzdp/item;
            # 反向代理目标指向Tomcat集群
            proxy_pass http://tomcat-cluster;
        }
        # ...
    }
    server {
        listen  8083;
        location /dzdp/item {
            # Tomcat服务器的IP和端口
            # proxy_pass http://192.168.146.1:8081/dzdp/item;
            # 反向代理目标指向Tomcat集群
            proxy_pass http://tomcat-cluster;
        }
        # ...
    }
    # ...
}

修改完成后,执行nginx -s reload命令重新加载OpenResty。

  • 3)功能测试

利用IDEA,分别启动两个Tomcat服务,端口分别是8080和8081:

在页面发起请求ID=1的商品信息,请求负载到8080端口的Tomcat:

在页面发起请求ID=2的商品信息,请求负载到8081端口的Tomcat:

再在页面发起请求ID=3的商品信息,请求负载到8080端口的Tomcat:

至此,基于商品ID实现负载均衡完成。

6.5.5 查询Redis

根据多级缓存的架构,在查询Tomcat之前,应先查询Redis缓存。

6.5.5.1 Redis缓存预热

Redis缓存会面临冷启动问题:

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

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

在本测试项目中,由于数量量较少,且没有数据统计相关功能,因此可以在启动时将所有数据都放入缓存中。

缓存预热需要在项目启动时完成,可以利用InitializingBean接口来实现,因为InitializingBean接口会在对象被Spring创建并且成员变量全部注入后执行。 代码如下:

// com.star.redis.dzdp.config.RedisHandler

@Slf4j
@Component
public class RedisHandler implements InitializingBean {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private IItemService itemService;

    @Resource
    private IItemStockService itemStockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        log.info("========> begin init Item to Redis.");
        // 1.查询商品信息
        List<Item> itemList = itemService.list();
        // 2.放入缓存
        if(itemList != null && !itemList.isEmpty()) {
            log.info("itemList.size = {}", itemList.size());
            for (Item item : itemList) {
                // 2.1 序列化
                String itemJsonStr = MAPPER.writeValueAsString(item);
                // 2.2 存入Redis
                stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), itemJsonStr);
                log.info("set to Redis: Key = {}, Value = {}", "item:id:" + item.getId(), itemJsonStr);
            }
        }
        // 3.查询商品库存信息
        List<ItemStock> itemStockList = itemStockService.list();
        // 4.放入缓存
        if(itemStockList != null && !itemStockList.isEmpty()) {
            log.info("itemStockList.size = {}", itemStockList.size());
            for (ItemStock itemStock : itemStockList) {
                // 2.1 序列化
                String itemStockJsonStr = MAPPER.writeValueAsString(itemStock);
                // 2.2 存入Redis
                stringRedisTemplate.opsForValue().set("item:stock:" + itemStock.getItemId(), itemStockJsonStr);
                log.info("set to Redis: Key = {}, Value = {}", "item:stock:" + itemStock.getItemId(), itemStockJsonStr);
            }
        }
        log.info("========> end init Item to Redis.");
    }
}

启动项目,查看日志:

可见,在项目启动时会执行InitializingBean口的afterPropertiesSet方法,以加载商品信息到Redis中。

6.5.5.2 封装Redis工具

OpenResty提供了操作Redis的模块,直接引入即可使用。为了使用方便,可以将对Redis的操作封装到之前编写的common.lua工具库中。修改/usr/local/openresty/lualib/common.lua文件:

  • 1)引入Redis模块,并初始化Redis对象
-- 导入Redis模块,并进行初始化
local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
  • 2)封装函数,用于释放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_idel_time, pool_size)
    if not ok then
	ngx.log(ngx.ERR, "[放入redis连接池失败" .. err .. "]")
    end
end
  • 3)封装函数,根据Key查询Redis数据
-- 封装函数,根据key查询Redis数据
local function read_redis(ip, port, key)
    -- 获取连接
    local ok, err = red:connect(ip, port)
    ok, err = red:auth("123321")
    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 "]")
    end
    -- 得到数据为空的处理
    if resp == ngx.null then
	resp = nil
	ngx.log(ngx.ERR, "[查询redis数据为空" .. key .. "]")
    end
    close_redis(red)
    return resp
end
  • 4)导出函数(read_http函数是之前封装的)
-- 将方法导出
local _M = {
    read_http = read_http,
    read_redis = read_redis
}
return _M
6.5.5.3 实现Redis查询

接下来修改/usr/local/openresty/nginx/lua/item.lua文件,实现Redis查询。其查询逻辑是:根据商品ID查询Redis,如果查询失败则继续查询Tomcat,并将查询结果返回。

修改后的/usr/local/openresty/nginx/lua/item.lua文件内容如下:

-- 1.导入组件
local common = require("common")
local read_http = common.read_http
local read_redis = common.read_redis
local cjson = require('cjson')

-- 封装函数,查询Redis数据
function read_data(key, path, params) 
    -- 查询Redis
    local val = read_redis("192.168.146.128", 6379, key)
    -- 判断查询结果
    if not val then
	ngx.log(ngx.ERR, "[Redis查询失败,尝试查询HTTP," .. key .. "]")
	-- Redis查询失败,去查询HTTP
	val = read_http(path, params)
    else
	ngx.log(ngx.ERR, "[Redis查询成功," .. key .. "]")
    end
    -- 返回数据
    return val
end

-- 2.获取商品ID
local id = ngx.var[1]
-- 3.根据ID发起请求查询商品信息
-- local itemJSON = read_http("/dzdp/item/".. id, nil)
local itemJSON = read_data("item:id:" .. id, "/dzdp/item/" .. id, nil)
ngx.log(ngx.ERR, "[查询商品信息结果: " .. itemJSON .. "]")
-- 4.根据ID发起请求查询商品库存信息
-- local itemStockJSON = read_http("/dzdp/item/stock/".. id, nil)
local itemStockJSON = read_data("item:stock:" .. id, "/dzdp/item/stock/" .. id, nil)
ngx.log(ngx.ERR, "[查询库存信息结果: " .. itemStockJSON .. "]")
-- 5.将JSON转换为table
if string.len(itemJSON) > 0 and string.len(itemStockJSON) > 0 then
    ngx.log(ngx.ERR, "查询成功...")
    local item = cjson.decode(itemJSON)
    local itemStock = cjson.decode(itemStockJSON)
    -- 6.组合数据
    item.stock = itemStock.stock
    item.sold = itemStock.sold
    -- 7.把item序列化为Json,并返回
    ngx.say(cjson.encode(item))
else
    ngx.log(ngx.ERR, "查询结果为空...")
    ngx.say({})
end
6.5.5.4 功能测试

所有代码编写完成后,下面进行测试。由于Redis中已经保存了ID为1~5的商品信息,所以在调用在页面发起请求ID=2的商品信息时,会直接从Redis缓存中返回:

在页面发起请求ID=8的商品信息时,会查询Redis缓存失败,然后去Tomcat中查询:

至此,查询Redis功能实现。

本节完,下一节继续进行多级缓存的实现。

本节所涉及的代码和资源可从git仓库下载:https://gitee.com/weidag/redis_learning.git

更多内容请查阅分类专栏:Redis从入门到精通

感兴趣的读者还可以查阅我的另外几个专栏:

  • SpringBoot源码解读与原理分析(已完结)
  • MyBatis3源码深度解析(已完结)
  • 再探Java为面试赋能(持续更新中…)