微服务学习笔记--高级篇--(多级缓存)

多级缓存

  • 安装OpenResty
  • OpenResty快速入门
  • 请求参数处理
  • 查询Tomcat
  • Redis缓存预热
  • 查询Redis缓存
  • Nginx本地缓存

初识OpenResty

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

  • 具备Nginx的完整功能
  • 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块
  • 允许使用Lua自定义业务逻辑、自定义库

官方网站:https://openresty.org/cn/


安装OpenResty


OpenResty快速入门

OpenResty快速入门,实现商品详情页面数据查询

商品详情页面目前展示的是假数据,在浏览器的控制台可以看到查询商品信息的请求:

Request URL:http://localhost/api/item.10001
Request Method:GET
Status Code:502 Bad Gateway
Remote Address:127.0.0.1:80
Referrer Policy:strict-orgin-when-cross-origin

而这个请求最终被反向代理到虚拟机的OpenResty集群:

upstream nginx-cluster{
	server 192.168.150.101:8081; #nginx-cluster集群,就是将来的nginx业务集群
}
server {
	listen       80;
	server_name  localhost;

	location /api {
		proxy_pass http://nginx-cluster; #监听/api路径,反向代理到nginx-cluster集群
	}
}

需求:在OpenResty中接收这个请求,并返回一段商品的假数据。


步骤:步骤一:修改nginx.conf文件

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

# 加载lua模块
lua_package_path lua_package_path "/usr/local/openresty/lualib/?.lua;;"
# 加载c模块
lua_package)cpath "/usr/local/openresty/lualib/?.lua;;"

2.在nginx.conf的server下面,添加对/api/item这个路径的监听

location /api/item {
	# 默认的响应类型-*
	default_type application/json;
	# 响应结果由 lua/item.lua 这个文件来决定
	content_by_lua_file lua/item/lua;
}

步骤:步骤二:修改item.lua文件

1.在nginx目录创建文件夹:lua

nginx]# pwd
/usr/local/openresty/nginx
nginx]#mkdir lua

2.在lua文件夹下,新建文件:item.lua

nginx]#pwd
/usr/local/openresty/nginx
nginx]#touch lua/item/lua

3.内容如下:

-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":1001,"name":"SALSA AIR"}')

4.重新加载配置

nginx -s reload

请求参数处理

OpenResty获取请求参数

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

参数格式 参数示例 参数解析代码示例
路径占位符 /item/1001 # 1.正则表达式匹配: location ~ /item/(\d+) {content_by_lua_file lua/item.lua;} --2、匹配到的参数会存入ngx、var数组中 – 可以用角标获取 local id = ngx.var[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() – 获取bogy中的json参数,返回值是string类型 local jsonBody = ngx.req.get_body_data()

查询Tomcat

案例:获取请求路径中的商品id信息,根据id向Tomcat查询商品信息

这里要修改item.lua,满足下面的需求:
1.获取请求参数中的id
2.根据id向Tomcat服务发送请求,查询商品信息
3.根据id向Tomcat服务发送请求,查询库存信息
4.组装商品信息、库存信息,序列化为JSON格式并返回

nginx内部发送Http请求

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

注意:这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。
但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:

location /path {
	# 这里是windows电脑的ip和java服务端口,需要确保Windows防火墙处于关闭状态
	proxy_pass http://192.168.150.1:8081;
}

封装http查询的函数

我们可以把http查询的请求封装为一个函数,放到OpenResty函数库中,方便后期使用。

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

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

2.在common.lua中封装http查询的函数

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
	local resp = ngx.location.capture(pathm{
		method = ngx.HTTP_GET,
		args = params,
	})
	if not resp then
		-- 记录错误信息,返回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

使用Http函数查询数据

刚才已经把http查询的请求封装为一个函数,放到OpenResty函数库中,接下来就可以使用这个库了。

  • 修改item.lua文件
-- 引入自定义工具模块
local common = require("common")
local read_http = common.read_http

-- 获取路径参数
local id = ngx.var[1]

-- 根据id查询商品
local itemJSON = read_http("/item/" .. id,nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/" .. id,nil)

查询到的是商品、库存的json格式数据,我们需要将两部分数据组装,需要用到JSON处理函数库。

JSON结果处理

OpenResty提供了一个cjson的模块用来处理JSON序列化和反序列化。

官方地址:https://github.com/openresty/lua-cjson/

  • 引入cjson模块:
local cjson = require "cjson"
  • 序列化:
local obj = {
	name = 'jack'
	age = 21
}
local json = cjson.encode(obj)

反序列化:

local json = '{"name": "jack", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
print(obj.name)

item.lua

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson库
local cjson = require('cjson')

-- 获取路径参数
local id = ngx.var[1]

-- 查询商品信息
local itemJSON = read_http("/item/" .. id,nil)
-- 查询库存信息
local stockJSON = read_http("/item/stock/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json返回结果
ngx.say(cjson.encode(item))

Tomcat集群的负载均衡

# 反向代理配置,将/item路径的请求处理到tomcat集群
location /item {
	proxy_pass http://tomcat-cluster}
# tomcat集群配置
upstream tomcat-cluster{
	hash $request_uri;
	server 192.168.150.1:8081;
	server 192.168.150.1:8082;
}

Redis缓存预热

冷启动与缓存预热

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

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

我们数据量较少,可以在启动时将所有数据都放入缓存中。

缓存预热

1.利用Docker安装Redis

docker run --name redis -p 6379:6379 -d redis redis=server --appendonly yes

2.在item-service服务中引入Redis依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3.配置Redis地址

spring:
	redis:
		host: 192.168.150.101

4.编写初始化类

@Component
public class RedisHandle implements InitializingBean {
	@Autowired
	private StringRedisTemplate redisTemplate;

	@Autowired
	private IItemService itemService;
	@Autowired
	private IItemStockService stockService;
	
	private static final ObjectMapper MAPPER = new ObjectMapper();
	
	@Override
	public void afterPropertiesSet() throws Exception {
	// 初始化缓存
	//1.查询商品信息
	List<Item> itemList = itemService.list();
	//2.放入缓存
	for(Item item:itemList) {
		//2.1item序列化为JSON
		String json = MAPPER.writeValueAsString(item);
		//2.2存入redis
		redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
	}

	//3.查询商品库存信息
	List<ItemStock> stockList = stockService.list();
	//4.放入缓存
	for (ItemStock stock : stockList) {
		//2.1item序列化为JSON
		String json = MAPPER.writeValueAsString(stock);
		//2.2存入redis
		redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
	}
  }
}

查询Redis缓存

OpenResty的Redis模块

OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:

  • 引入Redis模块,并初始化Redis对象
-- 引入redis模块
local redis = require("resty.redis")
-- 初始化Redis对象
local red = redis:new()
-- 设置Redis超时时间
red:set_timeouts(1000,1000,1000)
  • 封装函数,用来释放Redis连接,其实是放入连接池
-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
	local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
	local pool_size = 100 -- 连接池大小
	local ok, err = redis:set_keepalive(pool_max_idle_time, pool_size)
	if not ok then
		ngx.log(ngx.ERR,"放入Redis连接池失败:",err)
	end
end

OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:

  • 封装函数,从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 = redis:get(key)
	-- 查询失败处理
	if not resp then
		ngx.log(ngx.ERR, "查询Redis失败:",err,", key = ",key)
	end
	-- 得到的数据为空处理
	if resp == ngx.null then
		resp = null
		ngx.log(ngx.ERR,"查询Redis数据为空,key = ", key)
	end
	close_redis(red)
	return resp
end

案例:查询商品时,优先Redis缓存查询

需求:

  • 修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat
  • 修改item.lua,查询商品和库存时都调用read_data这个函数
-- 封装函数,先查询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, parms)
	end
	return resp
end

nginx本地缓存

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

  • 开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
  • 操作共享字典
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储,指定key、value、过期时间,单位为s,默认为0代表永不过期
item_cache:set('key','value',1000)
--读取
local val = item_cache:get('key')

案例:在查询商品时,优先查询OpenResty的本地缓存

需求:

  • 修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat
  • 查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期
  • 商品基本信息,有效期30分钟
  • 库存信息,有效期1分钟
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

-- 封装函数
local function read_data(key,expire,path,params)
	-- 查询本地缓存
	local val = item_cahche:get(key)
	if not val then
		ngx.log("本地缓存查询失败,尝试查询Redis,key:",key)
		-- 查询redis
		var= read_redis("127.0.0.1",6379,key)
		-- 判断查询结果
		if not val then
			ngx.log("redis查询失败,尝试查询http,key:",key)
			-- redis查询失败,查询http
			val = read_http(path, parms)
		end
	end
	-- 查询成功,把数据写入到本地缓存
	item_cache:set(key, val, expire)
	-- 返回数据
	return val
end

-- 获取路径参数
local id = ngx.var[1]

-- 查询商品信息
local itemJSON = read_data("item:id:" .. id,1800, "/item/" .. id,nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id,60,"/item/stock/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json返回结果
ngx.say(cjson.encode(item))

你可能感兴趣的:(微服务,学习,笔记)