传统缓存问题
传统的缓存策略一般是请求到达 Tomcat 后,先查询 Redis,如果未命中则查询数据库,存在下面的问题:
请求要经过 Tomcat 处理,Tomcat 的性能成为整个系统的瓶颈 Redis 缓存失效时,会对数据库产生冲击。
多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻 Tomcat 压力,提升服务性能。
用作缓存的 Nginx 是业务 Nginx,需要部署为集群,再由专门的 Nginx 用来做反向代理。
JVM 进程缓存
本地进程缓存
缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。主要分为两类:
-
分布式缓存。例如:Redis 它的优点存储容量更大、可靠性更好、可以在集群间共享。侧面也反映出访问缓存有一定的网络开销。使用场景一般为缓存数据量较大、可靠性要求较高、需要在集群间共享。
-
进程本地缓存。例如:HashMap、GuavaCache 两者的优点读取本地内存,没有网络开销,速度更快。反之缺点存储容量有限、可靠性较低、无法共享。使用场景一般为性能要求较高,缓存数据量较小。
Caffeine
Caffeine 是一个基于 Java8 开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前 Spring 内部的缓存使用的就是 Caffeine。GitHub地址
Caffeine 示例:
@Configuration
public class CaffeineConfig {
@Bean
public Cache<Long, Item> itemCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
@Bean
public Cache<Long, ItemStock> stockCache(){
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
@GetMapping("/stock/{id}")
public ItemStock findStockById(@PathVariable("id") Long id) {
return stockCache.get(id, key -> stockService.getById(key));
}
OpenResty
OpenResty® 是一个基于 Nginx 的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。官方网站
- 具备 Nginx 的完整功能。
- 基于 Lua 语言进行扩展,集成了大量精良的 Lua 库、第三方模块。
- 允许使用 Lua 自定义业务逻辑、自定义库。
OpenResty 代理访问 Lua 文件
- 在
nginx.conf的 http 下面,添加对 OpenResty 的 Lua 模块的加载:
# 加载 lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
# 加载 c 模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
- 在
nginx.conf的 server 下面,添加对/api/item这个路径的监听:
location /api/item {
# 响应类型,这里返回json
default_type application/json;
# 响应数据由 lua/item.lua这个文件来决定
content_by_lua_file lua/item.lua;
}
编写 item.lua 文件
- 在 nginx 目录创建文件夹:
lua
- 在 lua 文件夹下,新建文件:
item.lua
- 编写
item.lua文件
ngx.say()就是写数据到Response中
ngx.say('{"id":10001,"name":"AIR"}')
- 重新加载配置
nginx -s reload
OpenResty 获取请求参数
OpenResty 提供了各种 API 用来获取不同类型的请求参数:
示例:item.lua
local id = ngx.var[1]
ngx.say('{id: '.. id ..' ,"name":"AIR"}')
Nginx 内部发送 HTTP 请求
Nginx 反向代理
location /path {
# 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
proxy_pass http://192.168.150.1:8081;
}
HTTP 请求封装为一个函数
-
在
/usr/local/openresty/lualib目录下创建common.lua文件: -
在
common.lua中封装 http 查询的函数:
-- 封装函数,发送http请求,并解析响应
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 not found, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end-- 将方法导出
local _M = {
read_http = read_http
}
return _M
引用 common 文件 JSON 结果处理
-- 引入自定义工具模块
local common = require("common")local read_http = common.read_http
-- 引入cjson模块
local cjson = require "cjson"
-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)
local obj = {
name = 'jack',
age = 21
}
local json = cjson.encode(itemStockJSON)
json.name = 'lua'
local json = '{"name": "lua", "age": 21}'
-- 反序列化
local obj = cjson.decode(json);
-- 返回结果
ngx.say(obj)
Nginx 负载均衡
提高 JVM 缓存命中率
进程之间的缓存是无法共享的。
Tomcat 集群配置:
hash $request_uri
# 针对于uri(资源路径)进行哈希运算,得到一个哈希值对Tomcat服务数量取余。
upstream tomcat-cluster{
hash $request_uri;
server 192.168.150.1:8081;
server 192.168.150.1:8082;
}
- 反向代理配置,将
/item路径的请求代理到 tomcat 集群
location /item {
proxy_pass tomcat-cluster;
}
Redis 缓存预热
添加 redis 缓存的需求
冷启动与缓存预热
冷启动:服务刚刚启动时,Redis 中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力。
缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到 Redis 中。
数据量较少,可以在启动时将所有数据都放入缓存中。
编写初始化类
package com.heima.item.config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class RedisHandler 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.1.item序列化为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.1.item序列化为JSON
String json = MAPPER.writeValueAsString(stock);
// 2.2.存入redis
redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
}
}
public void saveItem(Item item) {
try {
String json = MAPPER.writeValueAsString(item);
redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
public void deleteItemById(Long id) {
redisTemplate.delete("item:id:" + id);
}
}
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 = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "放入Redis连接池失败: ", err)
end
end
- 封装函数,从 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 = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
end
close_redis(red)
return resp
end
- 封装函数,先查询 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, params)
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')
封装函数,先查询本地缓存,再查询 redis,再查询 http:
local function read_data(key, expire, path, params)
-- 读取本地缓存
local val = item_cache:get(key)
if not val then
-- 缓存未命中,记录日志
ngx.log(ngx.ERR, "本地缓存查询失败, key: ", key , ", 尝试redis查询")
-- 查询redis
val = read_redis("127.0.0.1", 6379, key)
-- 判断redis是否命中
if not val then
ngx.log(ngx.ERR, "Redis缓存查询失败, key: ", key , ", 尝试http查询")
-- Redis查询失败,查询http
val = read_http(path, params)
end
end
-- 写入本地缓存
item_cache:set(key, val, expire)
return val
end
-- 根据id查询商品
local itemJSON = read_data('item:id:' .. id, 1800, "/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_data('item:stock:id:' .. id, 60, "/item/stock/".. id, nil)