• 多级缓存架构(六)


    在信息暴炸的时代,为了在项目中提高数据加载效率,缓存技术是必不可以少的,缓存技术存在于应用场景的方方面面。从浏览器请求,到反向代理服务器,从进程内缓存到分布式缓存。其中缓存策略,算法也是层出不穷,下面要说的就是一套如何实现一套可以对后端服务器形成最小压力的架构。

    一、缓存的解析

     借一下上一篇文章中的图,其实现在问很多人怎么提高数据的访问速度和效率,很多人都能回答出做缓存处理,这句话可以说对也可以说不对,对是因为没错,缓存是可以提高效率,但并不是做了缓存处理他的并发量就一定能提升的上来,就以上图为例,如果说前端请求很高,达到了2000K,如果所有的数据加载在后端redis中做了缓存,如果这时所有用户请求全部从前端传到后端,服务器照样承受不了这么高并发,所以说只在后端提升了加载速度是没有用的,正确的做法是把缓存在到Nginx中而不是存到后台服务器,原因就是Nginx能承受的并发比tomcat要高。

     有了这个思路后,架构就要换一下了,如上图,当第一次请求过来时走后台去后台Redis中查询,并存在Nginx下面的Redis,之后的所有请求就不用走后台查询,这样性能就提升上来了,这时还要考虑一个问题,那就是如果Nginx下的Redis本身没有缓存数据又怎么搞,那就要去Nginx缓存中去找,因为Nginx本身是带有缓存功能的,如果Nginx中存在要的缓存就直接从Nginx中返回给用户,这样一设计后可以几十万个请求就只有几十个落在了后台服务器上了。这种设计除了可以有效提高加载速度、降低后端服务负载之外,还可以防止缓存雪崩,这也就是电商中用的多级缓存架构体系。

    二、应用

    我们在看购物平台时有很多商品优先推荐展示,这些其实都是推广商品,并非真正意义上的热门商品,首页展示这些商品数据需要加载效率极高,并且商城首页访问频率也是极高,所以需要对首页数据做缓存处理,首先想到的就是Redis缓存。

     接下来要作的东西就是设计推广商品的表结构及逻辑设计,其实这推广产品不一定只在首页出现,可能在别的地方也有,所以可以设计一张表存放这玩意,表结构如下

    DROP TABLE IF EXISTS `ad_items`;
    CREATE TABLE `ad_items` (
      `id` int(11) NOT NULL AUTO_INCREMENT,
      `name` varchar(100) DEFAULT NULL,
      `type` int(3) DEFAULT NULL COMMENT '分类,1首页推广,2列表页推广',
      `sku_id` varchar(60) DEFAULT NULL COMMENT '展示的产品',
      `sort` int(11) DEFAULT NULL COMMENT '排序',
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB AUTO_INCREMENT=123 DEFAULT CHARSET=utf8;

    建立实体类:

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    //MyBatisPlus表映射注解
    @TableName(value = "ad_items")
    public class AdItems implements Serializable{
    
        @TableId(type = IdType.AUTO)
        private Integer id;
        private String name;
        private Integer type;
        private String skuId;
        private Integer sort;
    }
    public interface AdItemsMapper extends BaseMapper<AdItems> {
    }
    public interface SkuService extends IService<Sku> {
        /**
         * 根据推广产品分类ID查询Sku列表
         * @param id
         * @return
         */
        List<Sku> typeSkuItems(Integer id);
    }
    @Service
    public class SKuServiceImpl extends ServiceImpl<SkuMapper, Sku> implements SkuService {
    
        @Resource
        private AdItemsMapper adItemsMapper;
    
        @Resource
        private SkuMapper skuMapper;
    
    
        @Override
        public List<Sku> typeSkuItems(Integer id) {
            //1.查询当前分类下的所有列表信息
            QueryWrapper<AdItems> adItemsQueryWrapper = new QueryWrapper<AdItems>();
            adItemsQueryWrapper.eq("type",id);
            List<AdItems> adItems = adItemsMapper.selectList(adItemsQueryWrapper);
    
            //2.根据推广列表查询产品列表信息
            List<String> skuids = adItems.stream().map(adItem->adItem.getSkuId()).collect(Collectors.toList());
            return skuids==null || skuids.size()<=0? null : skuMapper.selectBatchIds(skuids);
        }
    }
    @RestController
    @RequestMapping(value = "/sku")
    public class SkuController {
        @Autowired
        private SkuService skuService;
    
        /****
         * 根据推广分类查询推广产品列表
         *
         */
        @GetMappingpublic List<Sku> typeItems(@RequestParam(value = "id")Integer id){
            //查询
            List<Sku> skus = skuService.typeSkuItems(id);
            return skus;
        }
    }

    经过上面的代码就可以实现热门商品推广操作,但是有个问题,因为前面说过热点数据访问量大,是要放在缓存里面进行缓存处理的,下面就来完成这最后一步

    三、缓存处理

    在改代码前先要说明几个常用的注解:

    @EnableCaching :开关性注解,在项目启动类或某个配置类上使用此注解后,则表示允许使用注解的方式进行缓存操作。
    @Cacheable :可用于类或方法上;在目标方法执行前,会根据key先去缓存中查询看是否有数据,有就直接返回缓存中的key对应的value值。不再执行目标方法;无则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存。
    @CacheEvict :可用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)。
    @CachePut :可用于类或方法上;在执行完目标方法后,并将方法的返回值作为value,并以键值对的形式存入缓存中。
    @Caching :此注解即可作为@Cacheable、@CacheEvict、@CachePut三种注解中的的任何一种或几种来使用。
    @CacheConfig :可以用于配置@Cacheable、@CacheEvict、@CachePut这三个注解的一些公共属性,例如cacheNames、keyGenerator。
    说明完注解接下来就是来实现缓存功能了,首先在配置文件中配置Redis缓存
      #Redis配置
      redis:
        host: 192.168.32.135
        port: 6379

    然后在启动类上开启缓存

     然后修改SKuServiceImpl类,加上红框内内容

     完成这一步缓存就做好了,但是有个问题,前面说过热点商品可能在多个地方都存被调用的情况,针对这个情况就要写feigin接口,在写feigin接口同时我也顺便把修改和删除操作给写了,后面就懒着改这个类了

    在spring-cloud-api的pom文件中先引用以下包

            <!--工具包-->
            <dependency>
                <groupId>com.ghy</groupId>
                <artifactId>spring-cloud-common</artifactId>
                <version>0.0.1-SNAPSHOT</version>
            </dependency>
            <!--openfeign-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
                <version>2.2.5.RELEASE</version>
            </dependency>

    然后在spring-cloud-goods-api服务器写feigin接口

    @FeignClient(value = "spring-cloud-goods-service")   
    public interface SkuFeign {


    /****
    * 根据推广分类查询推广产品列表
    */
    @GetMapping(value = "/sku")
    List<Sku> typeItems(@RequestParam(value = "id")Integer id);

    /****
    * 根据推广分类删除推广产品列表
    *
    */
    @DeleteMapping(value = "/sku")
    RespResult delTypeItems(@RequestParam(value = "id")Integer id);

    /****
    * 根据推广分类修改推广产品列表
    */
    @PutMapping(value = "/sku")
    RespResult updateTypeItems(@RequestParam(value = "id")Integer id);

    }

    四、Lua+Redis实现多级缓存

     前面说过Lua和多级缓存,接下来就把这两个东西结合起来实现;前面说过前端请求过来后第一次会通过Nginx会分发到tomact进行查询,然后将查询的数据放到nginx中去;现在要做的操作就是通过Lua脚本完善后面的操作,操作流程是这样,当请求过来

    nginx会先执行lua脚本数据先从redis中去查询先看下redis有没有对应的数据,如果有则直接把数据响应给用户,没有再经过tomact查询;这时有的人会问,如果一不小心把Redis服务器中的数据全清空了怎么办,其实问题也不大,前面也提过Nginx本身也带有缓存,当Redis没查找数据时,他会去Nginx自身查下,看能不能找到数据;

    下面先写一个lua脚本;

    进入/usr/local/openresty/nginx/lua,在里面创建一个aditem.lua脚本

    --数据响应类型JSON 
    ngx.header.content_type="application/json;charset=utf8"
    --Redis库依赖 
    local redis = require("resty.redis");
    local cjson = require("cjson"); 
    --获取id参数(type) 
    local id = ngx.req.get_uri_args()["id"]; 
    --key组装 
    local key = "ad-items-skus::"..id 
    --创建链接对象 
    local red = redis:new() 
    --设置超时时间 red:set_timeout(2000) 
    --设置服务器链接信息 
    red:connect("192.168.32.135", 6379) 
    --查询指定key的数据 
    local result=red:get(key); 
    --关闭Redis链接 
    red:close()
    if result==nil or result==null or result==ngx.null then 
    return true 
    else
    --输出数据 
    ngx.say(result) 
    end
    修改 nginx.conf 添加如下配置:
    #推广产品查询 
    location /sku{ 
    content_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
     }

     

    然后保存修改,打入nginx -s reload命令重启下再访问 http://www.ljx.com/sku?id=1 就可以拿到数据,这个请求跟你后台服务器开没开都没有关系了。

    五、nginx代理缓存

    前面有提过,当Redis缓存没有数据时,请求会去nginx缓存找数据;在这时就要提到一个东西,那就是proxy_cache ;proxy_cache 是用于 proxy 模式的缓存功能,proxy_cache 在 Nginx 配置的 http 段、server 段中分别写入不同的配置。http 段中的配置用于定义 proxy_cache 空间,server 段中的配置用于调用 http 段中的定义,启用对server 的缓存功能。要想使用它就要完成两个步骤:第一个步骤是定义缓存空间;第二个步骤是在指定地方使用定义的缓存 。

    1、开启Proxy_Cache缓存,这个需要在nginx.conf中配置才能开启缓存:

    proxy_cache_path /usr/local/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
    参数说明:
    【proxy_cache_path】指定缓存存储的路径,缓存存储在/usr/local/openresty/nginx/cache目录 
    【levels=1:2】设置一个两级目录层次结构存储缓存,在单个目录中包含大量文件会降低文件访问速度,因此我们建议对大多数部署使用两级目录层次结构。如果 levels 未包含该参数,Nginx 会将所有文件放在同一目录中。 
    【keys_zone=proxy_cache:10m】设置共享内存区域,用于存储缓存键和元数据,例如使用计时器。拥有内存中的密钥副本,Nginx 可以快速确定请求是否是一个 HIT 或 MISS 不必转到磁盘,从而大大加快了检查速度。1 MB 区域可以存储大约 8,000 个密钥的数据,因此示例中配置的 10 MB 区域可以存储大约80,000 个密钥的数据。
    【max_size=1g】设置缓存大小的上限。它是可选的; 不指定值允许缓存增长以使用所有可用磁盘空间。当缓存大小达到限制时,一个称为缓存管理器的进程将删除最近最少使用的缓存,将大小恢复到限制之下的文件。
    【inactive=60m】指定项目在未被访问的情况下可以保留在缓存中的时间长度。在此示例中,缓存管理器进程会自动从缓存中删除 60 分钟未请求的文件,无论其是否已过期。默认值为 10 分钟(10m)。非活动内容与过期内容不同。Nginx 不会自动删除缓存 header 定义为已过期内容(例如 Cache-Control:max-age=120)。过期(陈旧)内容仅在指定时间内未被访问时被删除。访问过期内容时,Nginx 会从原始服务器刷新它并重置 inactive 计时器。
    【use_temp_path=off】表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据,我们一般关闭该功能。
    Proxy_Cache属性:
    proxy_cache:设置是否开启对后端响应的缓存,如果开启的话,参数值就是zone的名称,比如:proxy_cache。
    proxy_cache_valid:针对不同的response code设定不同的缓存时间,如果不设置code,默认为200,301,302,也可以用any指定所有code。
    proxy_cache_min_uses:指定在多少次请求之后才缓存响应内容,这里表示将缓存内容写入到磁盘。
    proxy_cache_lock:默认不开启,开启的话则每次只能有一个请求更新相同的缓存,其他请求要么等待缓存有数据要么限时等待锁释放;nginx 1.1.12才开始有。配套着proxy_cache_lock_timeout一起使用。
    proxy_cache_key:缓存文件的唯一key,可以根据它实现对缓存文件的清理操作。 
    2、开启Proxy_Cache缓存后修改 nginx.conf;在location /sku中加入以下配置
     #先找Nginx缓存 
     rewrite_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua; 
     #启用缓存openresty_cache 
     proxy_cache proxy_cache; 
     #针对指定请求缓存 
     #proxy_cache_methods GET; 
     #设置指定请求会缓存 
     proxy_cache_valid 200 304 60s; 
     #最少请求1次才会缓存 
     proxy_cache_min_uses 1; 
     #如果并发请求,只有第1个请求会去服务器获取数据 
     #proxy_cache_lock on; 
     #唯一的key 
     proxy_cache_key $host$uri$is_args$args; 
     #动态代理,这是在缓存中都没查到的情况 
    proxy_pass http:
    //192.168.32.32:8081;
    重启nginx或者重新加载配置文件 nginx -s reload ,再次测试,可以发现下面个规律:
     
    1:先查找Redis缓存 
    2:Redis缓存没数据,直接找Nginx缓存 
    3:Nginx缓存没数据,则找真实服务器

    这时还可以发现在cache目录下多了目录个文件,这个东西就是Nginx缓存;这里补充下,如果你的linux是布在云上而你的项目是内外,那么动态代理配置的就是项目的公网地址;

    六、Cache_Purge代理缓存清理

    上面讲了Nginx缓存的保存,但是在很多时候我们如果不想等待缓存的过期,想要主动清除缓存,可以采用第三方的缓存清除模块清除缓存nginx_ngx_cache_purge 。安装nginx的时候,需要添加 purge 模块purge 模块我们已经下载了,这一个步骤我在安装 OpenRestry 的时候已经实现了。在OpenRestry文章中也有说明,安装好了后,我们配置一个清理缓存的地址:http://192.168.32.32/purge/sku?id=1 
     
    #清理缓存 
    location ~ /purge(/.*) {
    #清理缓存
    proxy_cache_purge proxy_cache $host$1$is_args$args;
    }
    此时访问http://www.ljx.com/purge/sku?id=1,表示清除缓存,如果出现如下
    效果表示清理成功: 

    这短短的一生我们最终都会失去,不妨大胆一点,爱一个人,攀一座山,追一个梦
  • 相关阅读:
    初来乍到
    OpenGl基础篇(1.0)
    说说结构化方法和面向对象方法
    浅谈软件项目管理
    测试篇
    结构化与面向对象化之应用比较
    敏捷软件开发VS传统软件开发
    SOSO街景地图 API (Javascript)开发教程(1)- 街景
    阿里实习内推面经
    Android工程开发笔记<一>
  • 原文地址:https://www.cnblogs.com/xing1/p/14939472.html
Copyright © 2020-2023  润新知