黑马点评18——多级缓存-OpenResty

[复制链接]
发表于 2026-1-24 15:03:01 | 显示全部楼层 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

×
安装OpenResty


安装参考博客
OpenResty快速入门


nginx

是没有业务本事的,我们是把哀求转发到openResty,然后在openResty中摆设集群。
那怎样在openResty中吸收并处理惩罚如许的哀求呢?

  1. #user  nobody;
  2. worker_processes  1;
  3. error_log  logs/error.log;
  4. events {
  5.     worker_connections  1024;
  6. }
  7. http {
  8.     include       mime.types;
  9.     default_type  application/octet-stream;
  10.     sendfile        on;
  11.     keepalive_timeout  65;
  12.     #lua 模块
  13.     lua_package_path "/usr/local/openresty/lualib/?.lua;;";
  14.     #c模块
  15.     lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
  16.     server {
  17.         listen       8081;
  18.         server_name  localhost;
  19.         location /api/item {
  20.             # 默认的响应类型
  21.             default_type application/json;
  22.             # 响应结果由lua/item.lua文件来决定
  23.             content_by_lua_file lua/item.lua;
  24.         }
  25.         location / {
  26.             root   html;
  27.             index  index.html index.htm;
  28.         }
  29.         error_page   500 502 503 504  /50x.html;
  30.         location = /50x.html {
  31.             root   html;
  32.         }
  33.     }
  34. }
复制代码
编写lua文件,但我们这里先不搞那么复杂,先返回一个假数据

假数据直接从页面中拷贝一个,为了以示区别,把此中的数据稍微改一下,我把行李箱尺寸由21改成了26

重启后,重新访问我们的商品详情页

可以看出,我们乐成的把前端哀求通过nginx

, 然后nginx

转发到了配景的openresty上,然后由lua脚本返回了我们设置的参数。
OpenResty获取哀求参数




如许就可以拿到路径参数,这个参数会存到变量里,我们就可以去到了。

返来测试

返回的id确实随着id的改变而改变了
假如在openresty中实行nginx

-s reload没变革,又大概是重新加载不可,我们直接关掉openresty,
  1. nginx
  2. -s stop
复制代码
在启动
  1. nginx
复制代码
但是启动的时间报了个错误
  1. 提示:nginx
  2. : [emerg] bind() to 0.0.0.0:80 failed (98: Address already in use)
复制代码
  1. nginx
  2. : [error] open() "/usr/local/nginx
  3. /logs/nginx
  4. .pid" failed (2: No such file or directory)
复制代码

办理办法,直接杀掉全部的nginx

进程
killall -9 nginx

杀掉nginx

进程 然后重启就行了
在实行启动
  1. nginx
复制代码
封装Http哀求


我们现在的哀求可以打到Openresty中了,但现在我们怎样把Openresty的哀求打进tomcat中去呢?我们的Openresty在假造机中,tomcat也就是我们的springBoot项目在windows情况下,这得在从假造机打出来。
这里有个本事就是,只要你的windows呆板的ip地点和你的假造即的网段设置成一样的,末了的主机号设置成1,就可以把哀求从假造机打到windows主机上。(留意:防火墙得关闭)

nginx

反向署理把哀求发到tomcat中

先给openresty的nginx

设置反向署理(留意联合本身的网段)

由于发哀求的API经常要使用,我们可以给他封装成函数。

  1. -- 封装函数,发送http请求,并解析响应
  2. local function read_http(path, params)
  3.     local resp = ngx.location.capture(path,{
  4.         method = ngx.HTTP_GET,
  5.         args = params,
  6.     })
  7.     if not resp then
  8.         -- 记录错误信息,返回404
  9.         ngx.log(ngx.ERR, "http 查询失败, path: ", path , ", args: ", args)
  10.         ngx.exit(404)
  11.     end
  12.     return resp.body
  13. end
  14. -- 将方法导出
  15. local _M = {  
  16.     read_http = read_http
  17. }  
  18. return _M
复制代码
由于我们在conf文件里设置了
  1. lua_package_path "/usr/local/openresty/lualib/?.lua;;";
复制代码
以是这个lualib包下面的全部.lua文件都会被加载到
向Tomcat发送http哀求

我们这里修改我们的lua脚本,调用这个API,把哀求发给tomcat,先简单返回一个商品的数据,拼接商品和库存的动作以后在做
  1. -- 导入common函数库
  2. local common = require('common')
  3. local read_http = common.read_http
  4. -- 获取路径参数
  5. local id = ngx.var[1]
  6. -- 查询商品信息
  7. local itemJSON = read_http("/item/" .. id, nil)
  8. -- 查询库存信息
  9. local stockJSON = read_http("/item/stock/" .. id, nil)
  10. -- 返回结果
  11. ngx.say(itemJSON)
复制代码
在欣赏器测试一下,发现果然可以返回商品数据了

那该怎么样完成数据的拼接呢?
我们得把数据转换为table,只有转换成了table才气完成数据的拼接这些操纵
cjson地点:https://github.com/openresty/lua-cjson

可以看出在lualib中已经有了cjson, 那就可以直接导入使用了
我们在item.lua脚本中完成组合数据并返回
  1. -- 导入common函数库
  2. local common = require('common')
  3. local read_http = common.read_http
  4. -- 导入cjson库
  5. local cjson = require('cjson')
  6. -- 获取路径参数
  7. local id = ngx.var[1]
  8. -- 查询商品信息
  9. local itemJSON = read_http("/item/" .. id, nil)
  10. -- 查询库存信息
  11. local stockJSON = read_http("/item/stock/" .. id, nil)
  12. -- JSON 转化为lua的table
  13. local item = cjson.decode(itemJSON)
  14. local stock = cjson.decode(stockJSON)
  15. -- 组合数据
  16. item.stock = stock.stock
  17. item.sold = stock.sold
  18. -- 把item序列化为JSON返回结果
  19. ngx.say(cjson.encode(item))
复制代码
回欣赏器再次测试

发现返回的数据已经有销量和库存了
根据商品id对tomcat集群负载平衡

我们刚刚测试的tomcat是单机的,现实中tomcat肯定是个集群。

标题形貌:
由于我们现实的tomcat肯定是集群的,那我们的nginx

会署理到tomcat的某一台服务器上(8081),在tomcat这一台服务器查询后返回效果,会形成一个JVM进程缓存,但是我们的nginx

的默认负载平衡战略是轮询,下一次查询同一个商品id,nginx

就把哀求发到另一个tomcat服务器上(8082),那前次tomcat形成的jvm进程(8081)缓存无法掷中,由于jvm进程缓存是不能共享的,以是,还得让8082服务器再次处理惩罚,那每次都如许,肯定低沉我们的掷中率,低沉性能。
以是,我们要修改nginx

的负载平衡战略,把轮询的负载平衡战略修改为hash,如许每次根据商品id盘算hash值,只要hash值同等,就能包管每次都哀求同一台tomcat服务器,从而进步掷中率。
修改nginx

的设置文件
  1. #user  nobody;worker_processes  1;error_log  logs/error.log;events {    worker_connections  1024;}http {    include       mime.types;    default_type  application/octet-stream;    sendfile        on;    keepalive_timeout  65;    #lua 模块    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
  2.     #c模块         lua_package_cpath "/usr/local/openresty/lualib/?.so;;";      upstream tomcat-cluster {        hash $request_uri;        server 192.168.10.1:8081;        server 192.168.10.1:8082;    }    server {        listen       8081;        server_name  localhost;                location /item {            proxy_pass http://tomcat-cluster;        }                location ~ /api/item/(\d+) {            # 相应范例,这里返回json            default_type application/json;            # 相应数据由 lua/item.lua这个文件决定            content_by_lua_file lua/item.lua;        }        location / {            root   html;            index  index.html index.htm;        }        error_page   500 502 503 504  /50x.html;        location = /50x.html {            root   html;        }    }}
复制代码
然后启动两台tomcat服务器

去欣赏器访问测试,不管访问任何商品,只要访问了一次,都会天生进程缓存,而且确保了进程缓存永久见效,不管访问多少次,都可以直接在缓存中得到数据。
Redis缓存预热


按照我们最初的多级缓存的假想,我们在查询的时间不要直接打到tomcat上,而是应该先打到redis查询,假如redis查询失败在查询tomcat。我们应该到场redis的缓存举行预热。


  1. <dependency>
  2.    <groupId>org.springframework.boot</groupId>
  3.    <artifactId>spring-boot-starter-data-redis</artifactId>
  4. </dependency>
复制代码

我这里是由于假造机里的docker有许多redis,这个开启的redis设置的映射的是6399端口,以是根据本身情况修改
然后我们来编写初始化类
  1. @Component
  2. public class RedisHandler implements InitializingBean {
  3.     @Autowired
  4.     private StringRedisTemplate stringRedisTemplate;
  5.     @Autowired
  6.     private IItemService itemService;
  7.     @Autowired
  8.     private IItemStockService stockService;
  9.     // spring中的默认json处理工具
  10.     private static final ObjectMapper MAPPER = new ObjectMapper();
  11.     // afterPropertiesSet()会在bean创建完,Autowired注入以后执行,实现缓存预热效果
  12.     @Override
  13.     public void afterPropertiesSet() throws Exception {
  14.         // 初始化缓存
  15.         // 1. 查询商品信息,我们查所有的,实际上应该查询热点数据
  16.         List<Item> itemList = itemService.list();
  17.         // 2. 放入缓存
  18.         for (Item item : itemList) {
  19.             // 2.1 item序列化为JSON
  20.             String json = MAPPER.writeValueAsString(item);
  21.             // 2.2 存入redis
  22.             stringRedisTemplate.opsForValue().set("item:id:" + item.getId(), json);
  23.         }
  24.         // 1. 查询库存信息,
  25.         List<ItemStock> stockList = stockService.list();
  26.         // 2. 放入缓存
  27.         for (ItemStock stock : stockList) {
  28.             // 2.1 item序列化为JSON
  29.             String json = MAPPER.writeValueAsString(stock);
  30.             // 2.2 存入redis
  31.             stringRedisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
  32.         }
  33.     }
  34. }
复制代码
启动项目,就可以在redis中完成预热。
查询Redis缓存

OpenResty中操纵redis函数在这里



我们把这个代码封装一下,放到我们的common.lua中去。


修改我们的common
  1. -- 导入redis
  2. local redis = require('resty.redis')
  3. -- 初始化redis
  4. local red = redis:new()
  5. red:set_timeouts(1000,1000,1000)
  6. -- 关闭redis连接的工具方法,其实是放入连接池
  7. local function close_redis(red)
  8.     local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
  9.     local pool_size = 100 --连接池大小
  10.     local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
  11.     if not ok then
  12.         ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
  13.     end
  14. end
  15. -- 查询redis的方法 ip和port是redis地址,key是查询的key
  16. local function read_redis(ip, port, key)
  17.     -- 获取一个连接
  18.     local ok, err = red:connect(ip, port)
  19.     if not ok then
  20.         ngx.log(ngx.ERR, "连接redis失败 : ", err)
  21.         return nil
  22.     end
  23.     -- 查询redis
  24.     local resp, err = red:get(key)
  25.     -- 查询失败处理
  26.     if not resp then
  27.         ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
  28.     end
  29.     --得到的数据为空处理
  30.     if resp == ngx.null then
  31.         resp = nil
  32.         ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
  33.     end
  34.     close_redis(red)
  35.     return resp
  36. end
  37. -- 封装函数,发送http请求,并解析响应
  38. local function read_http(path, params)
  39.     local resp = ngx.location.capture(path,{
  40.         method = ngx.HTTP_GET,
  41.         args = params,
  42.     })
  43.     if not resp then
  44.         -- 记录错误信息,返回404
  45.         ngx.log(ngx.ERR, "http 查询失败, path: ", path , ", args: ", args)
  46.         ngx.exit(404)
  47.     end
  48.     return resp.body
  49. end
  50. -- 将方法导出
  51. local _M = {  
  52.     read_http = read_http,
  53.     read_redis = read_redis
  54. }  
  55. return _M
复制代码
修改我们的item.lua
  1. -- 导入common函数库
  2. local common = require('common')
  3. local read_http = common.read_http
  4. local read_redis = common.read_redis
  5. -- 导入cjson库
  6. local cjson = require('cjson')
  7. -- 封装查询函数
  8. function read_data(key, path, params)
  9.         -- 查询redis
  10.         local resp = read_redis("127.0.0.1", 6399, key)
  11.         -- 判断查询结果
  12.         if not resp then
  13.                 ngx.log("redis查询失败, 产生查询http, key: ", key)
  14.                 -- redis 查询失败,去查询http
  15.                 resp = read_http(path, params)
  16.         end
  17.         return resp
  18. end
  19. -- 获取路径参数
  20. local id = ngx.var[1]
  21. -- 查询商品信息
  22. local itemJSON = read_data("item:id:" .. id,"/item/" .. id, nil)
  23. -- 查询库存信息
  24. local stockJSON = read_data("item:stock:id:" .. id,"/item/stock/" .. id, nil)
  25. -- JSON 转化为lua的table
  26. local item = cjson.decode(itemJSON)
  27. local stock = cjson.decode(stockJSON)
  28. -- 组合数据
  29. item.stock = stock.stock
  30. item.sold = stock.sold
  31. -- 把item序列化为JSON返回结果
  32. ngx.say(cjson.encode(item))
复制代码
这就是使用lua来先查询redis, 当redis查询失效后在查询我们的tomcat服务器。
Nginx本地缓存


现在我们的多级缓存就差OpenResty的本地缓存了。
怎么实现呢?


我们在nginx

.conf中设置共享辞书

在item.lua的业务逻辑中导入我们的共享辞书,而且把我们的查询逻辑举行修改,在查询redis之前,先查询本地缓存。
我们的业务逻辑item.lua要修改成这个样子了
  1. -- 导入common函数库
  2. local common = require('common')
  3. local read_http = common.read_http
  4. local read_redis = common.read_redis
  5. -- 导入cjson库
  6. local cjson = require('cjson')
  7. -- 导入共享词典,本地缓存
  8. local item_cache = ngx.shared.item_cache
  9. -- 封装查询函数
  10. function read_data(key, expire, path, params)
  11.         -- 查询本地缓存
  12.         local val = item_cache:get(key)
  13.         if not val then
  14.                 ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis, key: ", key)
  15.                 -- 查询redis
  16.                 val = read_redis("127.0.0.1", 6399, key)
  17.                 -- 判断查询结果
  18.                 if not val then
  19.                         ngx.log(ngx.ERR, "redis查询失败, 产生查询http, key: ", key)
  20.                         -- redis 查询失败,去查询http
  21.                         val = read_http(path, params)
  22.                 end
  23.                 -- 查询成功, 把数据写入本地缓存
  24.                 item_cache:set(key, val, expire)
  25.         end
  26.         -- 返回数据
  27.         return val
  28. end
  29. -- 获取路径参数
  30. local id = ngx.var[1]
  31. -- 查询商品信息
  32. local itemJSON = read_data("item:id:" .. id, 1800 ,"/item/" .. id, nil)
  33. -- 查询库存信息
  34. local stockJSON = read_data("item:stock:id:" .. id, 60 ,"/item/stock/" .. id, nil)
  35. -- JSON 转化为lua的table
  36. local item = cjson.decode(itemJSON)
  37. local stock = cjson.decode(stockJSON)
  38. -- 组合数据
  39. item.stock = stock.stock
  40. item.sold = stock.sold
  41. -- 把item序列化为JSON返回结果
  42. ngx.say(cjson.encode(item))
复制代码
如许我们在去欣赏器测试一次,如许就完备实现了我们的多级缓存。
so cool!!!
回复

使用道具 举报

登录后关闭弹窗

登录参与点评抽奖  加入IT实名职场社区
去登录
快速回复 返回顶部 返回列表