精华内容
下载资源
问答
  • 先简单解释下什么是分布式多级缓存,所谓分布式简单理解就是异地跨机房服务应用部署;所谓多级缓存,这里狭义语义指定的是应用服务级别的缓存,通常泛指Redis、Memcached等;所谓多级缓存,这里是将JVM级的驻留缓存...

    设计背景

    概念

    先简单解释下什么是分布式多级缓存,所谓分布式简单理解就是异地跨机房服务应用部署;所谓多级缓存,这里狭义语义指定的是应用服务级别的缓存,通常泛指Redis、Memcached等;所谓多级缓存,这里是将JVM级的驻留缓存和外部依赖的缓存服务相比而言的。Redis、Memcached等都提供了性能优越的缓存服务,在高并发场景下作为提高吞吐量、优化服务性能的利器立下了汗马功劳。

    场景

    一般情况下,缓存我们只使用Redis作为唯一缓存就可以满足大多数业务场景。这里我们不考虑一般的业务场景,现在试图将服务场景复杂化去进行设计,进一步提高对服务性能的追求。
    首先概述下业务场景, 我们的应用服务每天需要提供亿级别调用量的查询业务,在最原始阶段,外部业务提供有效入参请求服务接口返回业务数据即可,然而在之后需求迭代中,增加了对调用方权限校验(渠道校验、授权码校验、入参许可校验)和对返回业务数据的保护(涉及脱敏和非授权字段的过滤排除),业务逻辑瞬间丰富和复杂起来。

    一般场景

    Request
    Fetch Data
    Response

    复杂场景

    Request
    Param Check
    - 1.入参校验
    - 2.渠道校验
    - 3.授权校验
    Fetch Data
    Handle Data
    - 1.脱敏处理
    - 2.字段屏蔽
    - 3.数据包装
    Response

    以上复杂场景下,需要解决如下问题:

    问题解决方向
    数据校验对比哈希表存储避免遍历,O(1)时间复杂度
    校验数据读取预加载配置数据到缓存
    JVM作为一级缓存,减少网络消耗
    Redis作为二级缓存兜底
    数据存储配置数据量小,变更低频,读取高频,适合驻留JVM一级缓存
    业务数据量巨大,变更不可控,读取高频,适合存储Redis二级缓存

    技术调研

    一级缓存 · JVM缓存

    通常,我们会选择HashMap或线程安全的ConcurrentHashMap作为JVM缓存容器来存储数据。

    HashMap & ConcurrentHashMap & Caffeine

    这三者都是Key-Value形式存储,具体的实现细节不同,JDK自带的HashMap、ConcurrentHashMap操作和实现简单、Caffeine则是一套封装良好天生为本地缓存服务的框架,提供了很多缓存特性。

    本地缓存设计

    定义本地缓存服务的能力定义

    /**
     * @author: guanjian
     * @date: 2020/07/06 16:11
     * @description: 本地缓存接口定义
     */
    public interface LocalCache {
        Object get(Object key);
    
        void put(Object key, Object value);
    
        void putIfAbsent(Object key, Object value);
    
        void put(Map map);
    
        void remove(Object key);
    
        Collection<?> getKeys();
    
        void clear();
    
        boolean hasKey(Object key);
    
        void destroy();
    
        long size();
    
        boolean isEmpty();
    
        String getRegion();
    
        Map asMap();
    }
    

    由于缓存都是Key-Value形式存储,只能支持Key单维度数据存储,为了提供更为便捷和可扩展的数据存储与读取场景,引入了Region分区使得缓存支持多维度业务。其实这里每个缓存实现内部都持有一个可见性的Map<Region,LocalCache<Object,Object>>,每个Region都是单例的只会被初始化一次,可以简单理解为两个嵌套Map的数据结构,数据的存取都是基于Region分区来进行读取的,一般拆分两个维度可以满足大部分场景,如果复杂的数据结构可以考虑对Value进行序列化。
    在这里插入图片描述

    ConcurrentHashMap本地缓存的实现

    /**
     * @author: guanjian
     * @date: 2020/07/06 16:15
     * @description: 通过ConcurrentHashMap构建本地缓存
     */
    public class ConcurrentHashMapCache implements LocalCache {
    
        /**
         * 多分区单例
         * {@String region 缓存分区标识}
         */
        private static volatile Map<String, ConcurrentHashMapCache> INSTANCES = Maps.newConcurrentMap();
    
        /**
         * 缓存分区标识
         */
        private String region;
    
        /**
         * 缓存容器
         * {@code Map<Object,Object> 缓存信息}
         */
        private Map<Object, Object> cache = Maps.newConcurrentMap();
    
        @Override
        public Object get(Object key) {
            return cache.get(key);
        }
    
        @Override
        public void put(Object key, Object value) {
            cache.put(key, value);
        }
    
        @Override
        public void putIfAbsent(Object key, Object value) {
            cache.putIfAbsent(key, value);
        }
    
        @Override
        public void put(Map map) {
            cache.putAll(map);
        }
    
        @Override
        public void remove(Object key) {
            cache.remove(key);
        }
    
        @Override
        public Collection<?> getKeys() {
            return cache.keySet();
        }
    
        @Override
        public void clear() {
            cache.clear();
        }
    
        @Override
        public boolean hasKey(Object key) {
            return cache.containsKey(key);
        }
    
        @Override
        public void destroy() {
            INSTANCES.remove(region);
        }
    
        @Override
        public long size() {
            return cache.size();
        }
    
        @Override
        public boolean isEmpty() {
            return cache.isEmpty();
        }
    
        @Override
        public String getRegion() {
            return this.region;
        }
    
        @Override
        public Map asMap() {
            return cache;
        }
    
        public static ConcurrentHashMapCache getInstance(String region) {
            if (INSTANCES.containsKey(region)) {
                return INSTANCES.get(region);
            }
    
            ConcurrentHashMapCache instance = null;
            if (!INSTANCES.containsKey(region)) {
                synchronized (INSTANCES) {
                    if (!INSTANCES.containsKey(region)) {
                        instance = new ConcurrentHashMapCache(region);
                        INSTANCES.putIfAbsent(region, instance);
                    }
                }
            }
            return instance;
        }
    
        private ConcurrentHashMapCache(String region) {
            this.region = region;
        }
    }
    

    Caffeine本地缓存的实现

    /**
     * @author: guanjian
     * @date: 2020/07/08 9:17
     * @description: 通过Caffeine构建本地缓存
     */
    public class CaffeineCache implements LocalCache {
    
        private final static Logger LOGGER = LoggerFactory.getLogger(CaffeineCache.class);
        /**
         * 多分区单例
         * {@String region 缓存分区标识}
         */
        private static volatile Map<String, CaffeineCache> INSTANCES = Maps.newConcurrentMap();
    
        /**
         * 缓存分区标识
         */
        private String region;
    
        /**
         * 缓存容器
         * {@code Cache<Object,Object> 缓存信息}
         */
        private Cache<Object, Object> cache = Caffeine.newBuilder()
                .recordStats()
                .initialCapacity(2 << 2)
                .build();
    
        private Object synLock = new Object();
    
        @Override
        public Object get(Object key) {
            Object value = cache.getIfPresent(key);
            LOGGER.debug("[CaffeineCache] region={}, key={},value={} getted.", region, key, JSON.toJSONString(value));
            return value;
        }
    
        @Override
        public void put(Object key, Object value) {
            cache.put(key, value);
            LOGGER.debug("[CaffeineCache] region={}, key={},value={} putted.", region, key, JSON.toJSONString(value));
        }
    
        @Override
        public void putIfAbsent(Object key, Object value) {
            synchronized (synLock) {
                if (null != cache.getIfPresent(key)) return;
                cache.put(key, value);
                LOGGER.debug("[CaffeineCache] region={}, key={},value={} putted.", region, key, JSON.toJSONString(value));
            }
        }
    
        @Override
        public void put(Map map) {
            cache.putAll(map);
            LOGGER.debug("[CaffeineCache] region={}, map={} putted.", region, JSON.toJSONString(map));
        }
    
        @Override
        public void remove(Object key) {
            cache.cleanUp();
        }
    
        @Override
        public Collection<?> getKeys() {
            return cache.asMap().keySet();
        }
    
        @Override
        public void clear() {
            cache.invalidateAll();
        }
    
        @Override
        public boolean hasKey(Object key) {
            LOGGER.debug("[CaffeineCache] region={}, key={}, map={}.", region, key, JSON.toJSONString(cache.asMap()));
            return null != cache.getIfPresent(key);
        }
    
        @Override
        public void destroy() {
            INSTANCES.remove(region);
        }
    
        @Override
        public long size() {
            return cache.asMap().keySet().size();
        }
    
        @Override
        public boolean isEmpty() {
            return 0 == cache.asMap().keySet().size();
        }
    
        @Override
        public String getRegion() {
            return region;
        }
    
        @Override
        public Map asMap() {
            return cache.asMap();
        }
    
        public static CaffeineCache getInstance(String region) {
            if (INSTANCES.containsKey(region)) {
                return INSTANCES.get(region);
            }
    
            CaffeineCache instance = null;
            if (!INSTANCES.containsKey(region)) {
                synchronized (INSTANCES) {
                    if (!INSTANCES.containsKey(region)) {
                        instance = new CaffeineCache(region);
                        INSTANCES.putIfAbsent(region, instance);
                        LOGGER.debug("[CaffeineCache] region={} is established.", region);
                    }
                }
            } else {
                instance = INSTANCES.get(region);
            }
            return instance;
        }
    
        private CaffeineCache(String region) {
            this.region = region;
        }
    }
    

    二级缓存 · Redis

    由于Redis提供了非常高效、便捷的数据结构,数据存储及选取的数据结构如下

    数据名称数据类型存储数据结构
    渠道值配置数据Hash
    授权码配置数据Hash
    业务授权配置数据Hash
    授权字段配置数据String(JSON序列化)
    脱敏字段配置数据String(JSON序列化)
    业务信息业务数据String(JSON序列化)

    设计方案

    缓存架构设计

    在这里插入图片描述

    • 我们将全视图从上到下拆分为调用方→缓存层→持久层→数据库的核心数据交互主线,此外还有涉及业务数据变更的用户操作、涉及配置或运营数据变更的管理员操作,以及对缓存服务监控的定时服务等。
    • 缓存层是整个缓存架构方案的核心。主要依赖JVM做配置数据的一级缓存存储,依赖Redis做业务数据存储及配置数据的兜底。
      由于应用部署是分布式的,JVM的数据一致性依赖Zookeeper进行实现,通过对path进行监听,数据变更都会触发path变化从而产生event驱动JVM重新拉去数据以保证JVM缓存数据一致。虽然Zookeeper是一个CP的实现,但是JVM分布式缓存这里采用一种AP实现,由于Zookeeper是JVM缓存与DB存储数据唯一通信的信道,一旦出现网络或中间件异常,会出现无法通信无法变更数据的情况,对于这种极端情况,目前采用两种策略进行控制,一是应用启动后会有一个定时轮询的守护线程监控数据情况保证即使在脏数据下服务也部分可用,二是JVM由于监听了Zookeeper的Path变更及Session事件,对于失联情况可以选择异常报警或超时失联做服务下线保护,这里分布式通信是一个非常复杂的业务场景,仅提供一个较为可行的实现思路,具体实现可以根据业务场景做更为精细化、高可用保障的实现逻辑。
    • 持久层主要做业务数据变更的缓存移除,确保缓存数据保持一致。这里通过切面环绕业务方法实现缓存移除或更新。

    缓存拦截流程

    Created with Raphaël 2.2.0 开始 是否命中缓存 结束 设置empty,防止缓存穿透 请求数据库 请求分布式锁 获取锁是否成功 是否存在数据 更新缓存 释放分布式锁 等待异步获取 yes no yes no yes no
    • 业务请求先请求缓存是否存在业务数据,若存在直接返回
    • 若缓存中为empty则说明业务数据为空,这里是为了防止缓存穿透做的空值缓存
    • 若缓存值为空,避免缓存击穿会首先设置缓存为empty,而后请求DB,为了避免多个请求同一时刻穿透到DB,需要竞态获取分布式锁,获取锁成功的请求可以顺利抵达数据库进行数据获取,如果查询到数据则立刻更新缓存,无数据则不修改缓存继续保持empty并返回空数据,释放分布式锁
    • 当业务方法涉及业务数据的变更时,进行切面环绕,保持第一时间清除缓存,保证缓存与DB数据一致性

    缓存加载流程

    Created with Raphaël 2.2.0 开始 Redis缓存获取 JVM缓存加载 结束 分布式锁获取 数据库读取 Redis缓存加载 分布式锁释放 等待异步轮询加载 yes no yes no
    • 数据加载首先判断Redis缓存中是否存在数据,若存在直接将Redis作为数据源进行数据获取加载JVM
    • 若Redis数据为空则请求DB进行数据拉取,为了避免同一时刻集群JVM频繁请求和拉取DB数据,这里做了分布式锁控制,同一时刻只发起一次数据拉取操作之后更新Redis,未获取分布式锁JVM进行异步轮询Redis完成最终数据加载

    缓存更新流程

    在这里插入图片描述

    • Redis缓存更新直接通过业务方法触发进行存储、移除设置即可。
    • JVM缓存的更新主要通过Zookeeper来做分布式协调,当数据库配置数据产生变化,随机触发Zookeeper迭代数据版本,JVM集群订阅Zookeeper数据变更事件触发版本对比后进行数据拉取,进入缓存加载流程保持数据更新
    展开全文
  • 这篇想聊的话题是:「分布式多级缓存架构的终章」,如何解决大流量、高并发这样的业务场景,取决于你能不能成为这个领域金字塔上层的高手? 能不能把这个问题思考清楚决定了你的成长速度。很多人在一...

    这篇想聊的话题是:「分布式多级缓存架构的终章」,如何解决大流量、高并发这样的业务场景,取决于你能不能成为这个领域金字塔上层的高手? 能不能把这个问题思考清楚决定了你的成长速度。

    很多人在一个行业5年、10年,依然未达到这个行业的中层甚至还停留在底层,因为他们从来不关心这样的话题。作为砥砺前行的践行者,我觉得有必要给大家来分享一下。

    开篇

    服务端缓存是整个缓存体系中的重头戏,从开始的网站架构演进中,想必你已看到服务端缓存在系统性能的重要性。

    但数据库确是整个系统中的“半吊子|慢性子”,有时数据库调优却能够以小搏大,在不改变架构和代码逻辑的前提下,缓存参数的调整往往是条捷径。

    在系统开发的过程中,可直接在平台侧使用缓存框架,当缓存框架无法满足系统对性能的要求时,就需要在应用层自主开发应用级缓存。

    缓存常用的就是Redis这东西,那到底什么是平台级、应用级缓存呢?

    后面给大家揭晓。但有一点可表明,「平台级就是你所选择什么开发语言来实现缓存」,而应用级缓存,则是通过应用程序来达到目的。

    01数据库缓存

    为何说数据库是“慢性子”呢?对现在喜欢「快」的你来说,慢是解决不了问题的。就好像总感觉感觉妹子回复慢

    因为数据库属于「IO密集型应」用,「主要负责数据的管理及存储」。数据一多查询本身就有可能变慢, 这也是为啥数据上得了台面时,查询爱用索引提速的原因。当然数据库自身也有“缓存”来解决这个问题。

    数据多了查询不应该都慢吗?小白说吒吒辉你不懂额

    。。。这个,你说的也不全是,还得分情况。例如:数据有上亿行

    「原因:」

    1. 因为简单的SQL的结果不会特别多。你请求也不大,磁盘跟的上

    2. 并发总量超过磁盘吞吐上限,是谁都没招

    就算你们不喜欢吒吒辉,我也要奋笔疾书

    数据库缓存是自身一类特殊的缓存机制。大多数数据库不需要配置就可以快速运行,但并没有为特定的需求进行优化。在数据库调优的时候,缓存优化你可以考虑下。

    以MySQL为例,MySQL中使用了查询缓冲机制,将SELECT语句和查询结果存放在缓冲区中,以键值对的形式存储。以后对于同样的SELECT语句,将直接从缓冲区中读取结果,以节省查询时间,提高了SQL查询的效率。

    1.1.MySQL查询缓存

    Query cache作用于整个MySQL实例,「主要用于缓存MySQL中的ResultSet」,也就是一条SQL语句执行的结果集,所以它只针对select语句。

    当打开 Query Cache 功能,MySQL在接收到一条select语句的请求后,如果该语句满足Query Cache的条件,MySQL会直接根据预先设定好的HASH算法将接收到的select语句以字符串方式进行 hash,然后到Query Cache中直接查找是否已经缓存。

    如果结果集已经在缓存中,该select请求就会直接将数据返回,从而省略后面所有的步骤(如SQL语句的解析,优化器优化以及向存储引擎请求数据等),从而极大地提高了性能。

    当然,若数据变化非常频繁的情况下,使用Query Cache可能会得不偿失。

    这是为啥,用它不是提速吗?咋还得不偿失

    因为MySQL只要涉及到数据更改,就会重新维护缓存。

    1. 如果SQL请求量比较大,你在维护的时候,就透过缓存走磁盘检索。这样数据库的压力肯定大。

    2. 重建缓存数据,它需要mysql后台线程来工作。也会增加数据库的负载。

    所以在MySQL8已经取消了它。故一般在读多写少,数据不怎么变化的场景可用它,例如:博客

    Query Cache使用需要多个参数配合,其中最为关键的是query_cache_size和query_cache_type, 前者用于设置缓存ResultSet的内存大小,后者设置在何种场景下使用Query Cache。

    这样可以通过计算Query Cache的命中率来进行调整缓存大小。

    1.2.检验Query Cache的合理性

    检查Query Cache设置的是否合理,可以通过在MySQL控制台执行以下命令观察:

    • SHOW VARIABLES LIKE '%query_cache%';

    • SHOW STATUS LIKE 'Qcache%';  通过检查以下几个参数可以知道query_cache_size设置得是否合理:

      • Qcache_inserts:表示Cache多少次未命中然后插入到缓存

      • Qcache_hits: 表示命中多少次,它可反映出缓存的使用效果。

    如果Qcache_hits的值非常大,则表明查询缓冲使用非常频繁,如果该值较小反而会影响效率,那么可以考虑不用查询缓存;

    • Qcache_lowmem_prunes: 表示多少条Query因为内存不足而被清除出Query_Cache。

    如果Qcache_lowmem_prunes的值非常大,则表明经常出现缓冲不够的情况,因增加缓存容量。

    • Qcache_free_blocks:表示缓存区的碎片

    Qcache_free_blocks值非常大,则表明缓存区中的碎片很多,可能需要寻找合适的机会进行整理。

    通过 「Qcache_hits」「Qcache_inserts」 两个参数可以算出Query Cache的命中率:

    通过 Qcache_lowmem_prunes 和 Qcache_free_memory 相互结合,能更清楚地了解到系统中Query Cache的内存大小是否真的足够,是否频繁的出现因内存不足而有Query被换出的情况。

    1.3.InnoDB的缓存性能

    当选择 InnoDB 时,「innodb_buffer_pool_size」 参数可能是影响性能的最为关键的一个参数,它用来设置缓存「InnoDB索引及数据块、自适应HASH、写缓冲等内存区域大小」,更像是Oracle数据库的 db_cache_size。

    简单来说,当操作InnoDB表的时候,「返回的所有数据或者查询过程中用到的任何一个索引块,都会在这个内存区域中去查询一遍」

    「MyISAM引擎中的 key_buffer_size」 一样,innodb_buffer_pool_size设置了 InnoDB 引擎需求最大的一块内存区域,直接关系到InnoDB存储引擎的性能,所以如果有足够的内存,尽可将该参数设置到足够大,将尽可能多的InnoDB的索引及数据都放入到该缓存区域中,直至全部。

    说到缓存肯定少不了,缓存命中率。那innodb该如何计算?

    计算出缓存命中率后,在根据命中率来对 innodb_buffer_pool_size 参数大小进行优化

    「除开查询缓存。数据库查询的性能也与MySQL的连接数有关」

    table_cache 用于设置 table 高速缓存的数量。

    show global status like 'open%_tables'; # 查看参数

    由于每个客户端连接都会至少访问一个表,因此该参数与max_connections有关。当某一连接访问一个表时,MySQL会检查当前已缓存表的数量。

    如果该表已经在缓存中打开,则会直接访问缓存中的表以加快查询速度;如果该表未被缓存,则会将当前的表添加进缓存在进行查询。

    在执行缓存操作之前,table_cache参数用于限制缓存表的最大数目:

    如果当前已经缓存的表未达到table_cache数目,则会将新表添加进来;若已经达到此值,MySQL将根据缓存表的最后查询时间、查询率等规则释放之前的缓存。

    02平台级缓存

    什么是平台级缓存,说的这个玄乎?

    平台级缓存是指你所用什么开发语言,具体选择的是那个平台,毕竟缓存本身就是提供给上层调用。主要针对带有缓存特性的应用框架,或者可用于缓存功能的专用库。

    如:

    • PHP中的Smarty模板库

    • Java中,缓存框架更多,如Ehcache,Cacheonix,Voldemort,JBoss Cache,OSCache等等。

    Ehcache是现在最流行的纯Java开源缓存框架,配置简单、结构清晰、功能强大,是从hibernate的缓存开始被广泛使用起来的。EhCache有如下特点:

    Ehcache的系统结构如图所示:

    什么是分布式缓存呢?好像我还没搞明白,小吒哥

    首先得看看恒古不变的“分布式”,即它是独立的部署到多个服务节点上或者独立的进程,彼此之间仅仅通过消息传递进行通信和协调。

    「也就是说分布式缓存,它要么是在单机上有多个实例,要么就独立的部署到不同服务器,从而把缓存分散到各处」

    最后通过客户端连接到对应的节点来进行缓存操作。

    Voldemort是一款基于Java开发的分布式键-值缓存系统,像JBoss的缓存一样,Voldemort同样支持多台服务器之间的缓存同步,以增强系统的可靠性和读取性能。

    Voldemort有如下特点:

        Voldemort的逻辑架构图

    Voldemort相当于是Amazon Dynamo的一个开源实现,LinkedIn用它解决了网站的高扩展性存储问题。

    简单来说,就平台级缓存而言,只需要在框架侧配置一下属性即可,而不需要调用特定的方法或函数。

    系统中引入缓存技术往往就是从平台级缓存开始,平台级缓存也通常会作为一级缓存使用。

    既然平台级缓存都使用框架配置来实现,这咋实现缓存的分布式呢?节点之间都没有互相的消息通讯了

    如果单看,框架缓存的调用,那确实没办法做到分布式缓存,因为自身没得像Redis那样分布式的部署方式,通过网络把各节点连接 。
    但本地平台缓存可通过远程过程调用,来操作分布在各个节点上的平台缓存数据。

    在 Ehcache 中:

    03应用级缓存

    当平台级缓存不能满足系统的性能时,就要考虑使用应用级缓存。应用级缓存,需要开发者通过代码来实现缓存机制。

    有些许 一方有难,八方支援 的感觉。自己搞不定 ,请教别人

    这是NoSQL的战场,不论是Redis还是MongoDB,以及Memcached都可作为应用级缓存的技术支持。
    「一种典型的方式是每分钟或一段时间后统一生成某类页面存储在缓存中,或者可以在热数据变化时更新缓存。」

    为啥平台缓存还不能满足系统性能要求呢?它不是还可以减少应用缓存的网络开销吗 那你得看这几点:

    3.1面向Redis的缓存应用

    Redis是一款开源的、基于BSD许可的高级键值对缓存和存储系统,例如:新浪微博有着几乎世界上最大的Redis集群。

    为何新浪微博是世界上最大的Redis集群呢?

    微博是一个社交平台,其中用户关注与被关注、微博热搜榜、点击量、高可用、缓存穿透等业务场景和技术问题。Redis都有对应的hash、ZSet、bitmap、cluster等技术方案来解决。

    在这种数据关系复杂、易变化的场景上面用到它会显得很简单。比如:

    「用户关注与取消」:用hash就可以很方便的维护用户列表,你可以直接找到key,然后更改value里面的关注用户即可。

    如果你像  memcache ,那只能先序列化好用户关注列表存储,更改在反序列化。然后再缓存起来,像大V有几百万、上千万的用户,一旦关注/取消。当前任务的操作就会有延迟。

    Reddis主要功能特点

    • 主从同步
      Redis支持主从同步,数据可以从主服务器向任意数量的从服务器同步,「从服务器可做为」关联其他从服务器的主服务器。这使得Redis可执行单层树状复制。

    • 发布/订阅
      由于实现了「发布/订阅机制」,使得从服务器在任何地方同步树的时候,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

    • 集群
      Redis 3.0版本加入cluster功能,「解决了Redis单点无法横向扩展的问题」。Redis集群采用无中心节点方式实现,无需proxy代理,客户端直接与Redis集群的每个节点连接,根据同样的哈希算法计算出key对应的slot,然后直接在slot对应的Redis上执行命令。

    从Redis视角来看,响应时间是最苛刻的条件,增加一层带来的开销是不能接受的。因此,Redis实现了客户端对节点的直接访问,为了去中心化,节点之间通过Gossip协议交换相互的状态,以及探测新加入的节点信息。Redis集群支持动态加入节点,动态迁移slot,以及自动故障转移。

    Redis集群的架构示意如图所示。

    那什么是 Gossip 协议呢?感觉好高大上,各种协议频繁出现

    Gossip 协议是一个多播协议,基本思想是:
    一个节点想要分享一些信息给网络中的其他的一些节点。于是,它周期性的随机选择一些节点,并把信息传递给这些节点。这些收到信息的节点接下来会做同样的事情,即把这些信息传递给其他一些随机选择的节点。直至全部的节点。

    即,Redis集群中添加、剔除、选举主节点,都是基于这样的方式。

    例如:当加入新节点时(meet),集群中会随机选择一个节点来邀请「新节点」,此时只有邀请节点和被邀请节点知道这件事,其余节点要等待 ping 消息一层一层扩散。除了 Fail 是立即全网通知的,其他诸如新节点、节点重上线、从节点选举成为主节点、槽变化等,都需要等待被通知到,所以Gossip协议也是最终一致性的协议。

    这种多播的方式,是不是忽然有种好事不出门,坏事传千里的感脚

    然而,Gossip协议也有不完美的地方,例如,拜占庭问题(Byzantine)。即,如果有一个恶意传播消息的节点,Gossip协议的分布式系统就会出问题。

    注:Redis集群节点通信消息类型

    所有的Redis节点通过PING-PONG机制彼此互联,内部使用二进制协议优化传输速度和带宽。

    这个ping为啥能提高传输速度和带宽?感觉不大清楚,小吒哥。那这里和OSI网络层级模式有关系了

    在OSI网络层级模型下,ping协议隶属网络层,所以它会减少网络层级传输的开销,而二进制是用最小单位0,1表示的位。

    带宽是固定的,如果你发送的数据包都很小,那传输就很快,并不会出现数据包很大还要拆包等复杂工作。
    相当于别人出差1斤多MacPro。你出差带5斤的战神电脑。

    Redis的瓶颈是什么呢?  吒吒辉给安排

    Redis本身就是内存数据库,读写I/O是它的强项,瓶颈就在单线程I/O上与内存的容量上。目前已经有多线程了,

    例如:Redis6具备网络传输的多线程模式,keydb直接就是多线程。
    啥?还没了解多Redis6多线程模式,后面单独搞篇来聊聊

    集群节点故障如何发现?

    「节点故障」是通过「集群中超过半数的节点检测失效时才会生效」。客户端与Redis节点直连,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。

    Redis Cluster把所有的物理节点映射到slot上,cluster负责维护node、slot和value的映射关系。当节点发生故障时,选举过程是集群中所有master参与的,如果半数以上master节点与当前master节点间的通信超时,则认为当前master节点挂掉。

    这为何不没得Slave节点参与呢?

    集群模式下,请求在集群模式下会自动做到读写分离,即读从写主。但现在是选择主节点。只能由主节点来进行身份参与。

    毕竟集群模式下,主节点有多个,每个从节点只对应一个主节点,那这样,你别个家的从节点能够参与选举整个集群模式下的主节点吗?

    就好像小姐姐有了对象,那就是名花有主,你还能在有主的情况下,去选一个?小心遭到社会的毒打

    如果集群中超过半数以上master节点挂掉,无论是否有slave集群,Redis的整个集群将处于不可用状态。

    当集群不可用时,所有对集群的操作都不可用,都将收到错误信息:

    [(error)CLUSTERDOWN The cluster is down]。

    支持Redis的客户端编程语言众多,可以满足绝大多数的应用,如图所示。

    3.2.多级缓存实例

    一个使用了Redis集群和其他多种缓存技术的应用系统架构如图所示

    负载均衡

    首先,用户的请求被负载均衡服务分发到Nginx上,此处常用的负载均衡算法是轮询或者一致性哈希,轮询可以使服务器的请求更加均衡,而一致性哈希可以提升Nginx应用的缓存命中率。

    什么是一致性hash算法?

    hash算法计算出的结果值本身就是唯一的,这样就可以让每个用户的请求都落到同一台服务器。
    默认情况下,用户在那台在服务器登录,就生成会话session文件到该服务器,但如果下次请求重新分发给其他服务器就又需要重新登录。

    而有了一致性hash算法就可以治愈它,它把请求都专心交给同一台服务器,铁打的专一,从而避免上述问题。 当然这里的一致性hash原理就没给大家讲了。后面安排

    nginx本地缓存

    请求进入到Nginx应用服务器,首先读取本地缓存,实现本地缓存的方式可以是Lua Shared Dict,或者面向磁盘或内存的 Nginx Proxy Cache,以及本地的Redis实现等,如果本地缓存命中则直接返回。

    这本地缓存怎么感觉那么特别呢?好像你家附近的小姐姐,离得这么近,可惜吃不着。呸呸呸,跑题啦

    • Lua Shard Dict是指在nginx上,通过lua开辟一块内存空间来存储缓存数据。相当于用的是nginx的进程资源

    • nginx Cache指nginx获取上游服务的数据缓存到本地。

    • 本地Redis指nginx和Redis部署在同一台服务上,由nginx直接操作Redis

    啥!nginx还可直接操作Redis呀,听我细细到来

    这些方式各有千秋,Lua Shard Dict 是通过Lua脚本控制缓存数据的大小并可以灵活的通过逻辑处理来修改相关缓存数据。

    而Nginx Proxy Cache开发相对简单,就是获取上游数据到本地缓存处理。而「本地Redis则需要通过lua脚本编写逻辑来设置」,虽然操作繁琐了,但解决了本地内存局限的问题。
    所以nginx操作Redis是需要借助于 Lua 哒

    nginx本地缓存有什么优点?

    Nginx应用服务器使用本地缓存可以提升整体的吞吐量,降低后端的压力,尤其应对热点数据的反复读取问题非常有效。

    本地缓存未命中时如何解决?

    如果Nginx应用服务器的本地缓存没有命中,就会进一步读取相应的分布式缓存——Redis分布式缓存的集群,可以考虑使用主从架构来提升性能和吞吐量,如果分布式缓存命中则直接返回相应数据,并回写到Nginx应用服务器的本地缓存中。

    如果Redis分布式缓存也没有命中,则会回源到Tomcat集群,在回源到Tomcat集群时也可以使用轮询和一致性哈希作为负载均衡算法。

    那我是PHP技术栈咋办?都不会用到java的Tomcat呀
    nginx常用于反向代理层。而这里的Tomcat更多是属于应用服务器,如果换成PHP,那就由php-fpm或者swoole服务来接受请求。即不管什么语言,都应该找对应语言接受请求分发的东西。

    当然,如果Redis分布式缓存没有命中的话,Nginx应用服务器还可以再尝试一次读主Redis集群操作,目的是防止当从Redis集群有问题时可能发生的流量冲击。

    这样的设计方案我在下表示看不懂

    如果你网站流量比较大,如果一次在Redis分布式缓存中未读取到的话,直接透过到数据库,那流量可能会把数据库冲垮。这里的一次读主也是考虑到Redis集群中的主从延迟问题,为的就是防止缓存击穿。

    在Tomcat | PHP-FPM集群应用中,首先读取本地平台级缓存,如果平台级缓存命中则直接返回数据,并会同步写到主Redis集群,在由主从同步到从Redis集群。

    此处可能存在多个Tomcat实例同时写主Redis集群的情况,可能会造成数据错乱,需要注意缓存的更新机制和原子化操作。

    如何保证原子化操作执行呢?

    当多个实例要同时要写Redis缓存时,为了保持原子化,起码得在涉及这块业务多个的 Key 上采用lua脚本进行封装,然后再通过分布式锁或去重相同请求并入到一个队列来获取,让获取到锁或从队列pop的请求去读取Redis集群中的数据。

    如果所有缓存都没有命中,系统就只能查询数据库或其他相关服务获取相关数据并返回,当然,我们已经知道数据库也是有缓存的。是不是安排得明明白白。

    这就是多级缓存的使用,才能保障系统具备优良的性能。

    什么时候,小姐姐也能明白俺的良苦心。。。。默默的独自流下了泪水

    3.3.缓存算法

    缓存一般都会采用内存来做存储介质,使用索引成本相对来说还是比较高的。所以在使用缓存时,需要了解缓存技术中的几个术语。

    缓存淘汰算法

    替代策略的具体实现就是缓存淘汰算法。

    使用频率:
    1. Least-Recently-Used(LRU) 替换掉最近被请求最少的对象。

    在CPU缓存淘汰和虚拟内存系统中效果很好。然而在直接应用与代理缓存中效果欠佳,因为Web访问的时间局部性常常变化很大。
    浏览器就一般使用了LRU作为缓存算法。新的对象会被放在缓存的顶部,当缓存达到了容量极限,底部的对象被去除,方法就是把最新被访问的缓存对象放到缓存池的顶部。

    1. Least-Frequently-Used(LFU) 替换掉访问次数最少的缓存,这一策略意图是保留最常用的、最流行的对象,替换掉很少使用的那些数据。

    然而,有的文档可能有很高的使用频率,但之后再也不会用到。传统的LFU策略没有提供任何移除这类文件的机制,因此会导致“缓存污染”,即一个先前流行的缓存对象会在缓存中驻留很长时间,这样,就阻碍了新进来可能会流行的对象对它的替代。

    1. Pitkow/Recker 替换最近最少使用的对象

    除非所有对象都是今天访问过的。如果是这样,则替换掉最大的对象。这一策略试图符合每日访问Web网页的特定模式。这一策略也被建议在每天结束时运行,以释放被“旧的”、最近最少使用的对象占用的空间。

    1. Adaptive Replacement Cache(ARC) ARC介于LRU和LFU之间,为了提高效果,由2个LRU组成。

    第一个包含的条目是最近只被使用过一次的,而第二个LRU包含的是最近被使用过两次的条目,因此,得到了新的对象和常用的对象。ARC能够自我调节,并且是低负载的。

    1. Most Recently Used(MRU) MRU与LRU是相对,移除最近最多被使用的对象。

    当一次访问过来的时候,有些事情是无法预测的,并且在存系统中找出最少最近使用的对象是一项时间复杂度非常高的运算,这时会考虑MRU,在数据库内存缓存中比较常见。

    访问计数
    1. Least Recently Used2  (LRU2)

    LRU的变种,把被两次访问过的对象放入缓存池,当缓存池满了之后,会把有两次最少使用的缓存对象去除。

    因为需要跟踪对象2次,访问负载就会随着缓存池的增加而增加。

    1. Two Queues(2Q) Two Queues是LRU的另一个变种。

    把被访问的数据放到LRU的缓存中,如果这个对象再一次被访问,就把他转移到第二个、更大的LRU缓存,使用了多级缓存的方式。去除缓存对象是为了保持第一个缓存池是第二个缓存池的1/3。

    当缓存的访问负载是固定的时候,把LRU换成LRU2,就比增加缓存的容量更好。

    缓存容量算法
    1. SIZE 替换占用空间最大的对象,这一策略通过淘汰一个大对象而不是多个小对象来提高命中率。不过,可能有些进入缓存的小对象永远不会再被访问。SIZE策略没有提供淘汰这类对象的机制,也会导致“缓存污染”。

    2. LRU-Threshold 不缓存超过某一size的对象,其他与LRU相同。

    3. Log(Size)+LRU 替换size最大的对象,当size相同时,按LRU进行替换。

    缓存时间
    1. Hyper-G LFU的改进版,同时考虑上次访问时间和对象size。

    2. Lowest-Latency-First 替换下载时间最少的文档。显然它的目标是最小化平均延迟。

    缓存评估
    1. Hybrid Hybrid 有一个目标是减少平均延迟。

    对缓存中的每个文档都会计算一个保留效用,保留效用最低的对象会被替换掉。位于服务器S的文档f的效用函数定义如下:

    Cs是与服务器s的连接时间;
    bs是服务器s的带宽;frf代表f的使用频率;sizef是文档f的大小,单位字节。K1和K2是常量,Cs和bs是根据最近从服务器s获取文档的时间进行估计的。

    1. Lowest Relative Value(LRV) LRV也是基于计算缓存中文档的保留效用,然后替换保留效用最低的文档。

    随机与队列算法
    1. First in First out(FIFO)

    FIFO通过一个队列去跟踪所有的缓存对象,最近最常用的缓存对象放在后面,而更早的缓存对象放在前面,当缓存容量满时,排在前面的缓存对象会被踢走,然后把新的缓存对象加进去。

    1. Random Cache 随机缓存就是随意的替换缓存数据,比FIFO机制好,在某些情况下,甚至比LRU好,但是通常LRU都会比随机缓存更好些。

    还有很多的缓存算法,例如Second Chance、Clock、Simple time-based、Extended time-based expiration、Sliding time-based expiration……各种缓存算法没有优劣之分,不同的实际应用场景,会用到不同的缓存算法。在实现缓存算法的时候,通常会考虑**使用频率、获取成本、缓存容量和时间等因素。 **

    04.使用公有云的缓存服务

    国内的共有云服务提供商如阿里云、青云、百度云等都推出了基于Redis的云存储服务,这些服务的有如下特点:

    • 动态扩容:

    用户可以通过控制面板升级所需Redis的存储空间,「扩容过程中服务不需要中断或停止」,整个扩容过程对用户是透明且无感知的,而自主使用集群解决Redis平滑扩容是个很烦琐的任务,现在需要用你的小手按几下鼠标就能搞定,大大减少了运维的负担。

    • 数据多备:
      数据保存在一主一备两台机器中,其中一台机器宕机了,数据还在另外一台机器上有备份。

    • 自动容灾:
      主机宕机后系统能自动检测并切换到备机上,实现了服务的高可用性。

    • 成本较低:
      在很多情况下,为使Redis的性能更好,需要购买一台专门的服务器用于Redis的存储服务,但这样会导致某些资源的浪费,购买Redis云存储服务就能很好地解决这样的问题。

    有了Redis云存储服务,能使后台开发人员从烦琐的运维中解放出来。应用后台服务中,如果自主搭建一个高可用、高性能的Redis集群服务,是需要投入相当的运维成本和精力。

    如果使用云服务,就没必要投入这些成本和精力,可以让后台应用的开发人员更专注于业务。

    特别推荐一个分享架构+算法的优质内容,还没关注的小伙伴,可以长按关注一下:

    
    
    
    
    长按订阅更多精彩▼
    
    如有收获,点个在看,诚挚感谢
    
    展开全文
  • 分布式多级缓存

    千次阅读 2018-07-17 10:38:42
    所谓分布式多级缓存,就是指在整个系统的不同层级进行数据的缓存,以提升系统的访问速度。通常情况下,分布式系统的访问流程如下所示: 接入层Nginx将请求负载均衡到应用层Nginx,常用的负载均衡算法是轮询/一致...

    基本概念

    所谓分布式多级缓存,就是指在整个系统的不同层级进行数据的缓存,以提升系统的访问速度。通常情况下,分布式系统的访问流程如下所示:

    这里写图片描述

    1. 接入层Nginx将请求负载均衡到应用层Nginx,常用的负载均衡算法是轮询/一致性哈希。轮询可以是请求更加的平均,一致性哈希可以提升应用层Nginx的缓存命中率。
    2. 应用层Nginx首先访问Local Cache(Lua Shared Dict、Nginx Proxy Cache等),如果Local Cache命中,则直接返回。Nginx本地缓存应对热点问题非常有效。
    3. 如果Local Cache未命中,则会读取分布式缓存(如Redis)。如果命中,则返回结果,并写入应用层Nginx本地缓存。
    4. 如果分布式缓存未命中,则会回源到Tomcat集群。
    5. 此时首先读取Tomcat本地缓存,若有则返回,并异步的写入Redis集群。若无则回源到DB中去查询,然后再写入到Redis中。这里面需要注意的一点是,Tomcat的Local Cache可以有效的缓解缓存失效风暴的问题。详情请见:《分布式缓存击穿

    上述缓存主要分为了三大类:应用层Nginx本地缓存分布式缓存Tomcat应用服务器Local Cache。并且每一层都是用来解决不同的问题:应用层Nginx用来解决缓存热点问题;分布式缓存用来减少回源率;Tomcat本地缓存用于解决缓存失效风暴的问题。

    缓存数据的方式

    是否过期

    不过期缓存

    通常情况下我们使用缓存都会设置一个过期时间,但在某些场景下我们需要设置不过期的缓存。这个时候通常情况下是在事务结束后去写入缓存的,此时需要注意的是不要同步的去写入缓存,以免阻塞主流程。对于这种常驻缓存的数据,一定要合理的管理,一般情况下可以定期的全量更新缓存。

    对于长尾缓存(缓存相对集中,但是有可能两会很大的),如以时间为维度的数据(订单/流水等),可以考虑使用LRU Cache,来使不常用的数据剔除缓存。

    过期缓存

    对于过期缓存,我们通常的用法是”懒加载”的方式。数据变更的时候去删除缓存,当读取数据时,缓存中没有,回源DB之后再写入缓存。这里面需要根据业务场景去设置一个合理的超时时间,对于比较热点的数据,可以根据使用场景让缓存有一定的延迟,在用户的忍受范围之内就行了(如商品库存/火车票库存等)。

    细粒度缓存

    对于一个电商系统,一个商品可能拆分成基础属性,图片列表、上下架、规格参数等。当商品信息变更时,此时就需要控制缓存的粒度了,尽量小成本的去更新缓存。如商品上下架,就只更新上下架维度的缓存即可。

    大Value缓存

    当使用Redis缓存时,应尽量避免使用大Value存储,这样会拖慢读取的速度。可以考虑的是:将其拆分为更小维度的数据,然后再组合返回前端。

    热点缓存

    对于热点缓存,通常情况下需要设置多级缓存,尽量避免数据通过网络去获取。当分布式缓存挂掉时,此时还需要考虑到”缓存击穿”的问题,此时可以使用白名单/布隆过滤器来作为缓存的最后一道防线。


    参考:《亿级流量网站架构核心技术》
    链接:http://moguhu.com/article/detail?articleId=99

    展开全文
  • layering-cache是一个支持分布式环境的多级缓存框架,使用方式和spring-cache类似。使用Caffeine作为一级本地缓存,使用redis作为二级集中式缓存。一级缓存和二级缓存的数据一致性是通过推和拉两种模式相结合的方式...
  • 分布式多级缓存实践

    2016-12-21 21:43:00
    此文可借鉴作为分布式缓存中间件实现方案
  • #资源达人分享计划#
  • #资源达人分享计划#
  • 基于Redis封装一个分布式多级缓存框架 1.技术栈: 1、Redis缓存的使用 2、ehcache缓存的使用 3、使用aop切面编程 4、Redis发布与订阅 5、自定义注解 + spel表达式 2.封装步骤 2.1 创建Maven工程 2.1.1导入相关依赖...

    基于Redis封装一个分布式多级缓存框架

    1.技术栈:

    1、Redis缓存的使用
    2、ehcache缓存的使用
    3、使用aop切面编程
    4、Redis发布与订阅
    5、自定义注解 + spel表达式

    2.封装步骤

    2.1 创建Maven工程

    2.1.1导入相关依赖
    <parent>
            <artifactId>spring-boot-starter-parent</artifactId>
            <groupId>org.springframework.boot</groupId>
            <version>2.3.0.RELEASE</version>
        </parent>
    
        <groupId>com.feilong.cache</groupId>
        <artifactId>cache-spring-boot-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
    
        <properties>
          <java.version>1.8</java.version>
        </properties>
        <dependencies>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
    
            <!--ehcache缓存-->
            <dependency>
                <groupId>net.sf.ehcache</groupId>
                <artifactId>ehcache</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-aop</artifactId>
            </dependency>
    
        </dependencies>
    
    

    2.2编写多级缓存的核心配置类

    @Configuration
    @ComponentScan("com.feilong.cache")
    public class MyCacheConfiguration {
    
        /**
         * 配置Ehcache(本地缓存====》》》一级缓存)
         *
         * @return
         */
        @Bean
        public CacheManager getCacheManager() {
            //创建一个缓存配置对象
            CacheConfiguration cacheConfiguration = new CacheConfiguration();
            //设置缓存名
            cacheConfiguration.setName("JVM-CACHE");
            //设置最大缓存数量
            cacheConfiguration.setMaxElementsInMemory(100000);
            //设置缓存的存活时间
            cacheConfiguration.setTimeToLiveSeconds(300);
            //设置缓存的空闲时间
            cacheConfiguration.setTimeToIdleSeconds(300);
    
            //配置Ehcache的配置类
            net.sf.ehcache.config.Configuration configuration = new net.sf.ehcache.config.Configuration();
            configuration.addCache(cacheConfiguration);
    
            CacheManager cacheManager = new CacheManager(configuration);
            return cacheManager;
        }
    
    
        @Bean
        public RedisMessageListenerContainer getRedisMessage(RedisConnectionFactory connectionFactory, CacheDelListener cacheDelListener){
            RedisMessageListenerContainer container = new RedisMessageListenerContainer();
            container.setConnectionFactory(connectionFactory);
            container.addMessageListener(cacheDelListener, new ChannelTopic("cacheChannel"));
            return container;
        }
    }
    
    

    2.3 编写自定义注解

    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 16:24
     *
     * 缓存使用的注解
     */
    @SuppressWarnings("ALL")
    @Target(ElementType.METHOD)
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CacheUsing {
    
        /**
         * 缓存的名字 键
         * @return
         */
        String key();
    
        /**
         * 缓存的超时时间 默认为5分钟  300秒
         * @return
         */
        int timeout() default 300;
    }
    
    
    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 16:27
     * 删除缓存的注解
     */
    @SuppressWarnings("ALL")
    @Target(ElementType.METHOD)
    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    public @interface CacheDel {
        /**
         * 要删除的缓存名字  键
         * @return
         */
        String key();
    
        /**
         * 是否后置删除  默认为true
         * @return
         */
        boolean isAfterDel() default true;
    }
    
    

    2.4创建一级(本地)缓存的工具类

    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 16:32
     * 一级缓存的工具类
     */
    @SuppressWarnings("ALL")
    @Component
    public class EhCacheUtil {
        @Autowired
        private CacheManager cacheManager;
    
    
        public Cache getCache() {
            return cacheManager.getCache("JVM-CACHE");
        }
    
        /**
         * 添加缓存方法重载 默认过期时间为5分钟
         *
         * @param key
         * @param value
         */
        public void putCache(String key, Object value) {
            this.putCache(key, value, null, null);
        }
    
        /**
         * 添加缓存
         *
         * @param key
         * @param value
         * @param timeout
         * @param timeUnit
         */
        public void putCache(String key, Object value, Integer timeout, TimeUnit timeUnit) {
            //从本地缓存中获取cache对象
            Cache cache = getCache();
            Element element = new Element(key, value);
            //判断超时时间
            if (timeout != null) {
                //设置转换最大空闲时间
                element.setTimeToIdle((int) timeUnit.toSeconds(timeout));
                //设置转换最大存活时间
                element.setTimeToLive((int) timeUnit.toSeconds(timeout));
            }
            //添加缓存到本地一级
            cache.put(element);
        }
    
        /**
         * 删除缓存
         * @param key
         * @return
         */
        public Object delCache(String key) {
            Cache cache = getCache();
            Object value = queryCache(key);
            if (value != null) {
                cache.remove(key);
                return value;
            }
            return null;
        }
    
        /**
         * 查询缓存
         *
         * @param key
         * @return
         */
        public Object queryCache(String key) {
            Cache cache = getCache();
            Element element = cache.get(key);
            if (element != null) {
                return element.getObjectValue();
            }
            return null;
        }
    }
    

    2.5创建二级(redis)缓存的工具类

    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 16:48
     * 二级缓存工具类
     */
    @SuppressWarnings("ALL")
    @Component
    public class RedisCacheUtil {
        @Autowired
        private RedisTemplate redisTemplate;
    
    
        /**
         * 添加缓存  使用默认的过期时间
         * @param key
         * @param value
         */
        public void putCache(String key, Object value) {
            this.putCache(key, value, null, null);
        }
    
        /**
         * 设置缓存
         *
         * @param key
         * @param value
         * @param timeout
         * @param unit
         */
        public void putCache(String key, Object value, Integer timeout, TimeUnit unit) {
            //超时时间为空  默认为5分钟
            if (timeout == null) {
                timeout = 5;
                unit = TimeUnit.MINUTES;
            }
            redisTemplate.opsForValue().set(key, value, timeout, unit);
        }
    
        /**
         * 删除缓存
         *
         * @param key
         * @return
         */
        public Object delCache(String key) {
            Object value = getCache(key);
            if (value != null) {
                redisTemplate.delete(key);
            }
            return value;
        }
    
        /**
         * 查找缓存
         *
         * @param key
         * @return
         */
        public Object getCache(String key) {
            return redisTemplate.opsForValue().get(key);
        }
    }
    

    2.6 创建 Spel解析表达式工具类

    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 17:00
     * <p>
     * Spel解析表达式工具类
     */
    @SuppressWarnings("ALL")
    @Component
    public class SpelUtil {
    
        /**
         * 解析Spel表达式
         *
         * 例如 : method(String name,int age)----》method("小明",20)
         *
         * 表达式为  'person_' + #name ->>person_小明
         *
         * @param spel  spel表达式
         * @param paramsName 方法上面的形参
         * @param paramsValue 方法上面的实参
         * @return
         */
        public static String parseSpel(String spel, String[] paramsName, Object[] paramsValue) {
            //创建数值上下文对象
            EvaluationContext evaluationContext = new StandardEvaluationContext();
            //循环参数列表
            for (String paramName : paramsName) {
                evaluationContext.setVariable(paramName, paramsValue);
            }
    
            //创建一个表达式解析器
            ExpressionParser expressionParser = new SpelExpressionParser();
            //根据表达式获得表达式对象
            Expression expression = expressionParser.parseExpression(spel);
            //解析表达式
            Object value = expression.getValue(evaluationContext);
            return (String) value;
        }
    }
    
    

    2.7 编写缓存增强的AOP

    /**
     * @author FeiLong
     * @version 1.8
     * @date 2020/11/26 17:20
     * 缓存的切面
     */
    @SuppressWarnings("ALL")
    @Aspect
    @Component
    public class CacheAop {
        @Autowired
        private RedisCacheUtil redisCacheUtil;
        @Autowired
        private EhCacheUtil ehCacheUtil;
        @Autowired
        private StringRedisTemplate stringRedisTemplate;
    
        /**
         * 使用缓存的增强
         * <p>
         * 环绕增强返回值必须和目标方法一致
         *
         * @param joinPoint
         * @return
         */
        @Around("@annotation(com.feilong.cache.annotation.CacheUsing)")
        public Object cacheUsing(ProceedingJoinPoint joinPoint) {
            //获得CacheUsing注解,并且拿到参数
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //获得反射对象的方法对象
            Method method = signature.getMethod();
            //通过反射获得指定方法的注解
            CacheUsing cacheUsing = method.getAnnotation(CacheUsing.class);
            //获得缓存的key--》spel表达式
            String key = cacheUsing.key();
            //获得缓存的超时时间
            int timeout = cacheUsing.timeout();
            //解析spel表达式
            //获得目标方法的形参列表
            LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
            String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
    
            //获得实际参数
            Object[] args = joinPoint.getArgs();
    
            //进行spel解析
            key = SpelUtil.parseSpel(key, parameterNames, args);
    
            //查询一二级缓存是否有数据  有直接返回,没有在调用核心业务进行数据库的直接查询
            Object value = ehCacheUtil.queryCache(key);
            if (value == null) {
                //再次查询二级缓存
                value = redisCacheUtil.getCache(key);
                if (value == null) {
                    //调用核心业务方法进行直接查询数据库
                    try {
                        System.out.println("业务方法执行!!!");
                        //目标方法的返回值是什么  value就是什么
                        value = joinPoint.proceed();
                    } catch (Throwable throwable) {
                        throwable.printStackTrace();
                    }
                } else {
                    System.out.println("二级缓存中标!!!!");
                }
            } else {
                System.out.println("一级缓存中标!!!!");
            }
    
            //不为空进行缓存重建,刷新
            if (value != null) {
                ehCacheUtil.putCache(key, value, timeout, TimeUnit.SECONDS);
                redisCacheUtil.putCache(key, value, timeout, TimeUnit.SECONDS);
            }
            //将查询的缓存数据返回给调用端
            return value;
        }
    
        /**
         * 删除缓存的增强
         *
         * @param joinPoint
         * @return
         */
        @Around("@annotation(com.feilong.cache.annotation.CacheDel)")
        public Object cacheDel(ProceedingJoinPoint joinPoint) {
            //获得CacheUsing注解,并且拿到其中的参数(缓存的key)
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            //获得增强方法的反射对象
            Method method = signature.getMethod();
    
            //通过反射获得指定方法上的注解
            CacheDel cacheDel = method.getAnnotation(CacheDel.class);
            //获得缓存的key - spel表达式
            String key = cacheDel.key();
            //是否为后置删除
            boolean isAfterDel = cacheDel.isAfterDel();
    
            //解析spel表达式
            //获得目标方法的形式参数名称列表
            LocalVariableTableParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
            String[] parameterNames = parameterNameDiscoverer.getParameterNames(method);
    
            //joinPoint.getArgs() - 获得目标方法的实际参数
            key = SpelUtil.parseSpel(key, parameterNames, joinPoint.getArgs());
    
            //前置删除
            if (!isAfterDel) {
                //删除一二级缓存
                redisCacheUtil.delCache(key);
                //发布一级缓存删除的消息
                stringRedisTemplate.convertAndSend("cacheChannel", key);
            }
            //调用核心方法目标业务
            Object result = null;
            try {
                result = joinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
            }
            //后置删除
            if (isAfterDel) {
                //删除一二级缓存
                redisCacheUtil.delCache(key);
                //发布一级缓存删除的消息
                stringRedisTemplate.convertAndSend("cacheChannel", key);
            }
            return result;
        }
    
    

    注:一级缓存使用的是redis的消息发布与订阅机制进行删除

    2.8 编写一级缓存订阅机制

    @Component
    @SuppressWarnings("ALL")
    public class CacheDelListener implements MessageListener {
        @Autowired
        private EhCacheUtil ehCacheUtil;
    
        @Override
        public void onMessage(Message message, byte[] bytes) {
            String key = new String(message.getBody());
            System.out.println("接收到了一级缓存删除的消息");
            //执行一级缓存删除
            ehCacheUtil.delCache(key);
        }
    }
    
    

    3.编写springboot的自动配置文件

    在这里插入图片描述

    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
      com.feilong.cache.config.MyCacheConfiguration
    

    4.进行install整个项目(下载到本地仓库以便其他项目引用)

    在这里插入图片描述

    5.在其他项目中导入下载好的依赖

     <dependency>
                <groupId>com.feilong.cache</groupId>
                <artifactId>cache-spring-boot-starter</artifactId>
                <version>1.0-SNAPSHOT</version>
     </dependency>
    

    6.在业务方法添加注解进行使用缓存

    @CacheDel(key = "'hotellist'")
        public boolean save(Hotel entity) {
            boolean result = super.save(entity);
            if (result) {
                //添加酒店信息成功后需要通知城市服务进行这个区域的酒店数量增加
                eventUtil.sendMsg(EventTypeConstantUtil.HOTEL_INSERT, entity);
                log.info("【insertHotel】 添加酒店信息发布成功!" + entity);
            }
            //
            return result;
        }
    
        /**
         * 查询酒店数据时--连带着房间信息以及房间价格信息一并查询
         *
         * @return
         */
        @Override
        @CacheUsing(key = "'hotellist'")
        public List<Hotel> list() {
            List<Hotel> hotels = super.list();
            hotels.stream().forEach(hotel ->
            {
                QueryWrapper<Room> wrapper = new QueryWrapper<Room>()
                        .eq("hid", hotel.getId());
                List<Room> rooms = roomService.list(wrapper);
                rooms.stream().forEach(room -> {
                    List<RoomPrice> prices = priceService.list(new QueryWrapper<RoomPrice>()
                            .eq("rid", room.getId())
                    );
                    room.setRoomPrices(prices);
                });
                hotel.setRoomList(rooms);
            });
            return hotels;
        }
    
    
        @Override
        @CacheUsing(key = "'hotel_' + #id")
        public Hotel getById(Serializable id) {
            Hotel hotel = super.getById(id);
            //根据酒店id查询所有客房
            List<Room> rooms = roomService.list(new QueryWrapper<Room>()
                    .eq("hid", hotel.getId())
            );
            rooms.stream().forEach(room -> {
                //根据客房id查询价格
                List<RoomPrice> roomPrices = priceService.list(new QueryWrapper<RoomPrice>()
                        .eq("rid", room.getId())
                );
                room.setRoomPrices(roomPrices);
            });
            hotel.setRoomList(rooms);
            return hotel;
        }
    

    注:业务方法中的全部的查询操作都是CacheUsing注解 增删改都是CacheDel注解

    至此,结束多级缓存的封装!!!!!

    展开全文
  • 一谈缓存,内心顿时豁然开朗。迫于key-value的形式,总感觉轻风扶面,杨柳依依,一切都尽在我掌握之中。犹如那一眼相中佳人的冲动,脑子里尽是佳人的容颜。 那缓存如果站在网站架构的角度,你知道它的设计原理和影响...
  • 此文可借鉴作为分布式缓存中间件实现方案 但有不足:若上生产就必须解决,比如如何实现方法级二级缓存过期,如何实现可配置编码级等等 下载地址: http://download.csdn.net/detail/luozhonghua2014/9717987
  • 这篇想聊的话题是:分布式多级缓存架构的终章,如何解决大流量、高并发这样的业务场景,取决于你能不能成为这个领域金字塔上层的高手? 能不能把这个问题思考清楚决定了你的成长速度。 很多人在一个行业5年、10年,...
  • 基于Spring cloud Alibaba,Oauth2,基于VUE的后台权限管理框架,集成了基于MQ的可靠消息的分布式事务解决方案。 感兴趣可以加QQ群:591363691 目前项目已经部署到k8s集群上,暂时只部署了3个微服务,由于怕有人乱删除...
  • 缓存 发发发 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 新的改变 我们对Markdown编辑器进行了一些...
  • 分布式多级缓存

    2021-03-06 13:21:43
    所谓分布式多级缓存,就是指在整个系统的不同层级进行数据的缓存,以提升系统的访问速度。通常情况下,分布式系统的访问流程如下所示: 1、接入层Nginx将请求负载均衡到应用层Nginx,常用的负载均衡算法是轮询/...
  • 然而,在数据业务中,我们时常听到数据需求方和数据开发方对性能慢的不满,所以,如何高效响应海量且迫切的数据需求,是大数据平台需要面对的一个关键问题,本文将介绍如何基于Alluxio建设分布式多级缓存系统对数据...
  • 一谈缓存,内心顿时豁然开朗。迫于key-value的形式,总感觉轻风扶面,杨柳依依,一切都尽在我掌握之中。犹如那一眼相中佳人的冲动,脑子里尽是佳人的容颜。 那缓存如果站在网站架构的角度,你知道它的设计原理和...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 12,366
精华内容 4,946
关键字:

分布式多级缓存