多级缓存优化实践

一、分布式部署

  在分布式部署项目时一般部署结果如下图所示:

    应用程序,使用集群部署,解决服务层的性能瓶颈

    入口层,使用 LVS + Openresty(Nginx)来解决入口层瓶颈问题

    入口层,使用DNS多机房部署,解决接入层流量问题。

      

 

 

   在解决了服务层面的平静后,数据库就成为了需要解决的性能瓶颈,一般的解决方案:

    1、分表分库(根据业务进行分库,分表)

    2、冷热分离(把热点数据放入一个库,把冷数据放入另一个库,数据归档)

    3、数据库设计(防止多表查询,冗余设计)

    4、防止慢查询 SQL 语句

    5、防止大表,大事务

    6、缓存(多级缓存)----- 落在数据库上请求非常少,90%+请求命中缓存数据,不在访问数据库了;

  分布式部署后,压测结果如下

      

 

   然后从多级缓存来优化项目。

二、本地缓存&分布式缓存

  1、缓存架构

    (1)项目进程内部缓存:jvm 堆内存缓存 ,缓存在项目运行本地服务器

    (2)分布式缓存 : redis 实现分布式缓存

    (3)openresty 接入层缓存: 内存字典

    (4)openresty+redis+lua 实现接入层缓存

      

   缓存的使用流程:

    首先查询OpenResty内存字典,如果有数据就返回;

    OpenResty内存字典没有数据,查询jvm堆内缓存,如果有就返回;

    jvm堆内缓存没有数据,查询分布式缓存redis,如果有数据就返回;

    如果redis中没有数据,就查询数据库。

  说明一点:缓存设计原则,缓存离请求距离越近,缓存性能就越好;

  2、缓存实现

      public TbSeckillGoods findOneByCache(Integer id){
            //1、先从jvm堆缓存中读取数据,使用guva缓存
            TbSeckillGoods seckillGoods = (TbSeckillGoods) guavaCahce.getIfPresent("seckill_goods_"+id);
            //判断jvm堆内缓存是否存在
            if(seckillGoods == null){
                  //2、从分布式缓存中查询
                  seckillGoods = (TbSeckillGoods) redisTemplate.opsForValue().get("seckill_goods_"+id);
                  //判断
                  if(seckillGoods == null){
                        //3、直接从数据库查询
                        seckillGoods = seckillGoodsMapper.selectByPrimaryKey(id);
                        if(seckillGoods != null && seckillGoods.getStatus() == 1){
                              //添加缓存
                              redisTemplate.opsForValue().set("seckill_goods_"+id,seckillGoods,1,TimeUnit.HOURS);
                        }
                  }
                  //添加guava缓存
                  guavaCahce.put("seckill_goods_"+id,seckillGoods);
           }
            //如果缓存存在,返回Redis缓存
            return seckillGoods;
      }

  缓存代码逻辑实现非常之简单的,就是先查询堆内存缓存,然后查询分布式缓存,如果以上 2 级缓存都未命中的话,那么就查询数据库,然后把数据放入分布式缓存,及堆内存缓存即可实现;

  其中本地缓存使用了谷歌提供的GuavaCache,配置如下:

@Configuration
public class GuavaCacheConfig {
    //定义一个guavacache对象
    private Cache<String,Object> commonCache = null;
    @PostConstruct
    public void init(){
        commonCache  = CacheBuilder.newBuilder()
                .initialCapacity(10)
                // 设置缓存中最大的支持存储缓存的key, 超过100个,采用LRU淘汰策略淘汰缓存
                .maximumSize(100)
                // 设置缓存写入后过期时间
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .build();
    }
    @Bean
    public Cache<String,Object> getCommonCache(){
        return commonCache;
    }

 

  那么其实有个问题,就是时候可以使用Map作为本地缓存的问题,其实是不可以的。

    (1)因为虽然Map也是 K-V 结构,但是其对数据脏读问题,对脏数据极度不敏感,通俗的讲,就是不能删除缓存;

    (2)还有一个比较重要的问题,JVM 内存资源非常宝贵,(java 对象,jvm 信息),不能把大量数据放入 jvm 堆内存中,如果使用Map,其实无法管理内存的大小,最终会影响服务性能。

  那么GuavaCache就对此做了很好的处理:

    (1)guavacache 工具可以给堆内存缓存设置过期时间;

    (2)把热点数据放入堆内存缓存;(可以设置缓存的数量,尽量保证热点数据在缓存中)

  使用了本地缓存 + 分布式缓存后,压测结果如下:

      

 

 三、接入层缓存

(一)Openresty 使用简述

  内存字典: openresy 服务器内存实现缓存 (openresty + lua 共同实现的)

  安装:http://openresty.org/cn/linux-packages.html

  安装完毕后,安装目录在:/usr/local/openresty,openresty是集成了nginx和lua,因此它的配置项其实就是nginx的配置项,可以修改nginx/conf/nginx.conf

  对于nginx的配置,有两种方式,分别是使用content_by_lua直接配置输出和使用content_by_lua_file来配置文件

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;

    keepalive_timeout  65;

    server {
        listen       80;
        server_name  lclpc;


        location /lua1 {
            default_type text/html;
            content_by_lua 'ngx.say("hello lua ......")';
        }

        location /lua2 {
            default_type text/html;
            content_by_lua_file lua/test.lua;
        }

        location /lua3 {
            default_type text/html;
            content_by_lua_file lua/detail.lua;
        }

        location / {
            root   html;
            index  index.html index.htm;
        }

 

  配置完毕后,启动  

PATH=/usr/local/openresty/nginx/sbin:$PATH
export PATH
nginx -c conf/nginx.conf

 

   1、使用lua语句

        location /lua1 {
            default_type text/html;
            content_by_lua 'ngx.say("hello lua ......")';
        }

 

  验证 /lua1  

    

 

   2、使用lua文件

  nginx配置

        location /lua2 {
            default_type text/html;
            content_by_lua_file lua/test.lua;
        }

 

  编辑 lua/test.lua 

local args = ngx.req.get_uri_args()
ngx.say("hello openresty! lua is so easy!==="..args.id)

  验证

      

 

  3、使用lua文件转发请求

    编辑lua/detail.lua文件,让其转发到后端服务器

ngx.exec('/seckill/goods/detail/1');

 

    nginx配置

    upstream BACKEND {
        server 172.20.10.2:9000;
        server 172.20.10.3:9000;
    }
    server {
        listen       80;
        server_name  lclpc;
        location /lua3 {
            default_type text/html;
            content_by_lua_file /usr/local/openresty/nginx/conf/lua/detail.lua;
        }

 

     验证

      

 

   4、更多的lua脚本接入方式可以参考:https://www.nginx.com/resources/wiki/modules/lua/#directives

 

(二)Openstry内存字典缓存

  1、开启Openstry内存字典

      修改nginx配置,开启一个名为ngx_cache的128m的内存字典

lua_shared_dict ngx_cache 128m;

 

      

 

   2、lua 脚本的方式实现内存字典缓存

    修改nginx配置文件

        location /goods/get {
            default_type application/json;
            content_by_lua_file /usr/local/openresty/nginx/conf/lua/lua_share.lua;
        }

    新增lua_share.lua文件

-- 基于内存字典实现缓存
-- 添加缓存方法
function set_to_cache(key,value,expritime)
    if not expritime then
        expritime = 0
    end
    -- 获取本地内存字典对象
    local ngx_cache = ngx.shared.ngx_cache
    -- 添加本地缓存数据
    local succ,err,forcible = ngx_cache:set(key,value,expritime)
    return succ
end

-- 获取缓存方法
function get_from_cache(key)
    -- 获取本地内存字典对象
    local ngx_cache = ngx.shared.ngx_cache
    -- 从本地内存字典中获取数据
    local value = ngx_cache:get(key)
    return value
end

-- 实现缓存业务
-- 判断本地内存字典是否具有缓存数据,如果没有访问后端服务
-- 先获取请求参数
local params = ngx.req.get_uri_args()
local id = params.id
-- 先从本地内存字典获取缓存数据
local goods = get_from_cache("seckill_goods_"..id)
-- 如果内存字典缓存数据不存在
if goods == nil then
-- 从后端服务器查询
    local res = ngx.location.capture("/seckill/goods/detail/"..id)
    -- 获取请求数据body 
  goods = res.body -- 添加本地缓存数据 set_to_cache("seckill_goods_"..id,goods,60) end -- 返回缓存结构 ngx.say(goods)

 

  验证

      

 

  压测:

      

 

 (三)Redis + Lua

   1、缓存架构

    原来设计的结构是首先查询Opensty内存字典缓存,如果没有数据,就会走后端项目,然后后端项目再去查询redis数据库。前面提到,离请求越近,缓存效果越好,那么就可以直接在OpenStry中操作redis,如果其内存字典中没有数据,则直接查询redis数据库,如果redis数据库没有数据,再请求到后端服务器。

      

 

     Openresty+lua 集成了 Redis lua 库,使用 Redis+lua 访问方式,只需要引入 redis 的 lua 库即可。

  可以在/usr/local/openresty/lualib/resty中查看Openstry集成的库,可以发现,Openstry集成了redis、mysql、memcached等

      

 

   引入redis库:require "resty.redis"

  2、编写lua脚本

  处理逻辑就是先查询本地内存字典,如果没有数据,在查询redis,没有数据,在查询后端服务。

-- 引入 redis.lua
local redis = require "resty.redis"
-- new 一个 redis 对象
local red = redis:new()

-- 基于内存字典实现缓存
-- 添加缓存方法
function set_to_cache(key,value,expritime)
    if not expritime then
        expritime = 0
    end
    -- 获取本地内存字典对象
    local ngx_cache = ngx.shared.ngx_cache
    -- 添加本地缓存数据
    local succ,err,forcible = ngx_cache:set(key,value,expritime)
    return succ
end

-- 获取缓存方法
function get_from_cache(key)
    -- 获取本地内存字典对象
    local ngx_cache = ngx.shared.ngx_cache
    -- 从本地内存字典中获取数据
    local value = ngx_cache:get(key)
    if not value then
        -- 从 redis 获取缓存数据
        local rev,err = get_from_redis(key)
        if not rev then
            ngx.say("redis cache not exsists",err)
            return
        end
        -- 添加本地缓存数据
        set_to_cache(key,rev,60)
    end
    return value
end



-- 向 redis 添加缓存
function set_to_redis(key,value)
    -- 设置 redis 超时时间
    red:set_timeout(100000)
    -- 连接 redis 服务器
    local ok,err = red:connect("172.20.10.14",6379)
    -- 判断连接是否 OK
    if not ok then
        ngx.say("failed to connect:",err)
        return
    end
    -- 向 redis 添加缓存数据
    local ok,err = red:set(key,value)
    if not ok then
        ngx.say("failed set to redis:",err)
        return
    end
    return ok;
end

-- 从 redis 获取缓存数据
function get_from_redis(key)
    -- 设置 redis 超时时间
    red:set_timeout(100000)
    -- 连接 redis 服务器
    local ok,err = red:connect("172.20.10.14",6379)
    -- 判断连接是否 OK
    if not ok then
        ngx.say("failed to connect:",err)
        return
    end
    -- 从 redis 获取缓存数据
    local res,err = red:get(key)
    if not res then
        ngx.say("failed get to redis:",err)
        return
    end
    -- 打印
    ngx.say("get cache data from redis...............")
    return res
end

-- 实现缓存业务
-- 判断本地内存字典是否具有缓存数据,如果没有访问后端服务
-- 先获取请求参数
local params = ngx.req.get_uri_args()
local id = params.id
-- 先从本地内存字典获取缓存数据
local goods = get_from_cache("seckill_goods_"..id)
-- 如果内存字典缓存数据不存在
if goods == nil then
    -- 从后端服务器查询
    local res = ngx.location.capture("/seckill/goods/detail/"..id)
    -- 获取请求数据body
    goods = res.body
    -- 添加本地缓存数据
    set_to_cache("seckill_goods_"..id,goods,60)
end
-- 返回缓存结构
ngx.say(goods)

 

  3、验证

    重新加载nginx配置后,访问接口,可以看到,第一次是直接从redis中查询的,后面的访问直接走的本地内存字典。

      

 

   4、压测

      这个压测结果与上面的压测结果基本一致,是因为上面用的就是内存字典,因此差异不大。

      

 

 

 

posted @ 2021-10-26 19:53  李聪龙  阅读(374)  评论(0编辑  收藏  举报