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

文章目录

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

sh 复制代码
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并拼接到结果中返回:

lua 复制代码
-- 获取商品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请求,其格式如下:

lua 复制代码
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就可以这样配置:

sh 复制代码
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文件,内容如下:

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的查询:

lua 复制代码
-- /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文件进行进一步优化:

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做负载均衡:

sh 复制代码
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创建并且成员变量全部注入后执行。 代码如下:

java 复制代码
// 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对象
lua 复制代码
-- 导入Redis模块,并进行初始化
local redis = require('resty.redis')
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
  • 2)封装函数,用于释放Redis连接
lua 复制代码
-- 封装函数,释放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数据
lua 复制代码
-- 封装函数,根据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函数是之前封装的)
lua 复制代码
-- 将方法导出
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文件内容如下:

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从入门到精通

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

相关推荐
守护者1704 分钟前
JAVA学习-练习试用Java实现“一个词频统计工具 :读取文本文件,统计并输出每个单词的频率”
java·学习
bing_15815 分钟前
Spring Boot 中ConditionalOnClass、ConditionalOnMissingBean 注解详解
java·spring boot·后端
ergdfhgerty17 分钟前
斐讯N1部署Armbian与CasaOS实现远程存储管理
java·docker
lwb_011828 分钟前
RabbitMq详解
分布式·rabbitmq
勤奋的知更鸟30 分钟前
Java性能测试工具列举
java·开发语言·测试工具
三目君34 分钟前
SpringMVC异步处理Servlet
java·spring·servlet·tomcat·mvc
用户05956611920934 分钟前
Java 基础篇必背综合知识点总结包含新技术应用及实操指南
java·后端
fie888934 分钟前
Spring MVC扩展与SSM框架整合
java·spring·mvc
不太可爱的叶某人42 分钟前
【学习笔记】深入理解Java虚拟机学习笔记——第3章 垃圾收集器与内存分配策略
java·笔记·学习
YuTaoShao42 分钟前
Java八股文——JVM「类加载篇」
java·开发语言·jvm