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

文章目录

    • 前言
    • 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-- 记录错误信息,返回404ngx.log(ngx.ERR, "HTTP请求查询失败, path: ", path , ", args: ", args)ngx.exit(404)endreturn 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 {@Resourceprivate StringRedisTemplate stringRedisTemplate;@Resourceprivate IItemService itemService;@Resourceprivate IItemStockService itemStockService;private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic 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 存入RedisstringRedisTemplate.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 存入RedisstringRedisTemplate.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 = 100local ok, err = red:set_keepalive(pool_max_idel_time, pool_size)if not ok thenngx.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 thenngx.log(ngx.ERR, "[连接redis失败" .. err .. "]")return nilend-- 查询Redislocal resp, err = red:get(key)if not resp thenngx.log(ngx.ERR, "[查询redis失败" .. err "]")end-- 得到数据为空的处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "[查询redis数据为空" .. key .. "]")endclose_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) -- 查询Redislocal val = read_redis("192.168.146.128", 6379, key)-- 判断查询结果if not val thenngx.log(ngx.ERR, "[Redis查询失败,尝试查询HTTP," .. key .. "]")-- Redis查询失败,去查询HTTPval = read_http(path, params)elsengx.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 thenngx.log(ngx.ERR, "查询成功...")local item = cjson.decode(itemJSON)local itemStock = cjson.decode(itemStockJSON)-- 6.组合数据item.stock = itemStock.stockitem.sold = itemStock.sold-- 7.把item序列化为Json,并返回ngx.say(cjson.encode(item))
elsengx.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为面试赋能(持续更新中…)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/624838.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

盲盒商城小程序(有米就出)

一款前端采用uniapp&#xff0c;后端采用Django框架开发的小程序&#xff0c;包含后台管理&#xff0c;如有人需要可联系演示功能&#xff08;个人开发&#xff0c;可商用/学习&#xff09;。 部分截图如下&#xff1a;

记录一下易语言post get使用WinHttp的操作

最近在学易语言&#xff0c;在进行通讯的时候&#xff0c;出现一些问题&#xff0c;现在记录下来&#xff0c;避免以后继续忘记&#xff0c; 先声明文本型变量jsonPostData jsonPostData &#xff1d; “{hostname:” &#xff0b; hostnameTxt &#xff0b; “,hardcode:” &…

游戏前摇后摇Q闪E闪QE闪QA等操作

备注&#xff1a;未经博主允许禁止转载 个人笔记&#xff08;整理不易&#xff0c;有帮助&#xff0c;收藏点赞评论&#xff0c;爱你们&#xff01;&#xff01;&#xff01;你的支持是我写作的动力&#xff09; 笔记目录&#xff1a;学习笔记目录_pytest和unittest、airtest_w…

AR、VR、MR 和 XR——它们的含义以及它们将如何改变生活

我们的工作、娱乐和社交方式正在发生巨大变化。远程工作的人比以往任何时候都多,屏幕已成为学习和游戏的领先平台。这种演变为元宇宙铺平了道路——如今,像 Meta Quest 2 这样的流行设备将您无缝地带入一个身临其境的世界,您可以在其中购物、创作和玩游戏、与同事协作、探索…

RAKsmart:硅谷裸机云多IP服务器性能评测

在云计算领域&#xff0c;裸机云作为一种结合了传统物理服务器与云计算优势的服务模式&#xff0c;近年来备受关注。硅谷裸机云作为业界佼佼者&#xff0c;以其出色的性能和稳定性赢得了众多用户的青睐。今天&#xff0c;我们就来评测一下硅谷裸机云的多IP服务器性能。 首先&am…

vscode i18n Ally插件配置项

.vscode文件&#xff1a; {"i18n-ally.localesPaths": ["src/lang"], //显示语言&#xff0c; 这里也可以设置显示英文为en,// 如下须要手动配置"i18n-ally.keystyle": "nested", // 翻译路径格式 (翻译后变量格式 nested&#xff1a…

C语言100题练习打卡(2)

14&#xff0c;将一个正整数分解质因数。 例如&#xff1a;输入90&#xff0c;打印出902*3*3*5 #include<stdio.h> /*分析&#xff1a; * 1&#xff0c;如果这话质数恰巧等于&#xff08;小于的时候&#xff0c;继续执行循环&#xff09;n&#xff0c; 则说明分解质因数…

会议室预约小程序开源版开发

会议室预约小程序开源版开发 支持设置免费预约和付费预约、积分兑换商城、积分签到等 会议室类目&#xff0c;提供多种类型和设施的会议室选择&#xff0c;满足不同会议需求。 预约日历&#xff0c;展示会议室预约情况&#xff0c;方便用户选择空闲时段。 预约记录&#xff0…

如何使用Git-Secrets防止将敏感信息意外上传至Git库

关于Git-Secrets Git-secrets是一款功能强大的开发安全工具&#xff0c;该工具可以防止开发人员意外将密码和其他敏感信息上传到Git库中。 Git-secrets首先会扫描提交的代码和说明&#xff0c;当与用户预先配置的正则表达式模式匹配时&#xff0c;便会阻止此次提交。该工具的优…

20240416,对象初始化和清理,对象模型和THIS指针

哈哈哈乌龟越狱了 目录 2.5 深拷贝&浅拷贝 2.6 初始化列表 2.7 类对象作为类成员 2.8 静态成员 2.9 成员变量和成员函数分开存储 2.10 THIS指针的用途 2.11 空指针访问成员函数 2.12 COSNT修饰成员函数 2.5 深拷贝&浅拷贝 浅拷贝&#xff1a;简单的赋值拷贝…

什么是邮箱分身?如何快速创建30个邮箱分身?

很多人只知道微信、QQ等应用分身&#xff0c;对于邮箱分身并不是很了解。邮箱分身和他们的不同点在于我们直接在原有邮箱的基础上创立新的虚拟邮箱地址&#xff0c;并且密码一致&#xff0c;在我们需要运营多个社交媒体账号或者管理多个项目的情况下&#xff0c;邮箱分身是一个…

快速切换node.js版本方法(使用开源项目方便切换版本)

1、安装nvm nvm下载地址&#xff1a;https://github.com/coreybutler/nvm-windows/ 2、输入nvm -v 3、查看可以安装的node.js版本 4、安装你想要的版本 5、查看是否安装成功&#xff08;*表示目前你使用的版本&#xff09; 6、切换版本 7、查询当前使用的版本