zoukankan      html  css  js  c++  java
  • 多级缓存优化实践

    一、分布式部署

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

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

        入口层,使用 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、压测

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

          

    ------------------------------------------------------------------
    -----------------------------------------------------------
    ---------------------------------------------
    朦胧的夜 留笔~~
  • 相关阅读:
    Linux系统:Centos7搭建Redis单台和集群环境
    Linux系统:Centos7安装Jdk8、Tomcat8、MySQL5.7环境
    Linux系统:常用Linux系统管理命令总结
    转--->svn的使用
    开发中常见的问题
    1.NSThread
    用NSOperation和NSOperationQueue实现多线程编程
    很实用的时间比对算法
    简单的IOS6和IOS7通过图片名适配
    nginx完美支持tp框架
  • 原文地址:https://www.cnblogs.com/liconglong/p/15466863.html
Copyright © 2011-2022 走看看