缓存冒险记:Nginx、Redis 和 Tomcat 缓存的神奇故事

334 阅读7分钟

传统缓存问题

传统的缓存策略一般是请求到达 Tomcat 后,先查询 Redis,如果未命中则查询数据库,存在下面的问题:
请求要经过 Tomcat 处理,Tomcat 的性能成为整个系统的瓶颈 Redis 缓存失效时,会对数据库产生冲击。

多级缓存方案

多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻 Tomcat 压力,提升服务性能。

图片.png

用作缓存的 Nginx 是业务 Nginx,需要部署为集群,再由专门的 Nginx 用来做反向代理。

图片.png

JVM 进程缓存

本地进程缓存

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。主要分为两类:

  1. 分布式缓存。例如:Redis 它的优点存储容量更大、可靠性更好、可以在集群间共享。侧面也反映出访问缓存有一定的网络开销。使用场景一般为缓存数据量较大、可靠性要求较高、需要在集群间共享。

  2. 进程本地缓存。例如:HashMap、GuavaCache 两者的优点读取本地内存,没有网络开销,速度更快。反之缺点存储容量有限、可靠性较低、无法共享。使用场景一般为性能要求较高,缓存数据量较小。

Caffeine

Caffeine 是一个基于 Java8 开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前 Spring 内部的缓存使用的就是 Caffeine。GitHub地址

图片.png

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 文件

  1. nginx.conf的 http 下面,添加对 OpenResty 的 Lua 模块的加载:
# 加载 lua 模块 
lua_package_path "/usr/local/openresty/lualib/?.lua;;"; 
# 加载 c 模块 
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
  1. 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 文件

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

图片.png

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

图片.png

  1. 编写item.lua文件
ngx.say()就是写数据到Response中
ngx.say('{"id":10001,"name":"AIR"}')
  1. 重新加载配置
nginx -s reload

OpenResty 获取请求参数

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

图片.png

示例: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 请求封装为一个函数

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

  2. 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 缓存命中率

进程之间的缓存是无法共享的。

图片.png

Tomcat 集群配置:

  1. hash $request_uri
# 针对于uri(资源路径)进行哈希运算,得到一个哈希值对Tomcat服务数量取余。
upstream tomcat-cluster{
    hash $request_uri;
    server 192.168.150.1:8081;
    server 192.168.150.1:8082;
}
  1. 反向代理配置,将/item路径的请求代理到 tomcat 集群  
location /item {
    proxy_pass tomcat-cluster;
}

Redis 缓存预热

添加 redis 缓存的需求

图片.png

冷启动与缓存预热

冷启动:服务刚刚启动时,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的模块,我们只要引入该模块就能直接使用:

  1. 引入 Redis 模块,并初始化 Redis 对象。
-- 引入redis模块
local redis = require("resty.redis")
-- 初始化Redis对象
local red = redis:new()
-- 设置Redis超时时间
red:set_timeouts(100010001000)
  1. 封装函数,用来释放 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  

  1. 封装函数,从 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  

  1. 封装函数,先查询 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)