使用SpringBoot 2实现分布式缓存

说明

应用系统需要通过Cache来缓存不经常改变得数据来提高系统性能和增加系统吞吐量,避免直接访问数据库等低速存储系统。缓存的数据通常存放在访问速度更快的内存里或者是低延迟存取的存储器,服务器上。应用系统缓存,通常有如下作用:

  • 缓存web系统的输出,如伪静态页面。
  • 缓存系统的不经常改变的业务数据,如用户权限,字典数据.配置信息等

Cache 组件和概念

Cache 通常有如下组件构成

  • CacheManager,用来创建,管理,管理多个命名唯一的Cache。如可以有组织机构缓存,菜单项的缓存,菜单树的缓存等
  • Cache类似Map那样的Key—Value存储结构,Value部分 通常包含了缓存的对象,通过Key来取得缓存对象
  • 缓存项,存放在缓存里的对象,常常需要实现序列化接口,以支持分布式缓存。
  • Cache存储方式,缓存组件的可以将对象放到内存,也可以是其他缓存服务器,Spring Boot 提供了一个基于ConcurrentMap的缓存,同时也集成了Redis,EhCache 2.x,JCache缓存服务器等
  • 缓存策略,通常Cache 还可以有不同的缓存策略,如设置缓存最大的容量,缓存项的过期时间等
  • 分布式缓存,缓存通常按照缓存数据类型存放在不同缓存服务器上,或者同一类型的缓存,按照某种算法,不同key的数据放在不同的缓存服务器上。
  • Cache Hit,当从Cache中取得期望的缓存项,我们通常称之为缓存命中。如果没有命中我们称之为Cache Miss,意味着需要从数据来源处重新取出并放回Cache中
  • Cache Miss:缓存丢失,根据Key没有从缓存中找到对应的缓存项
  • Cache Evication:缓存清除操作。
  • Hot Data,热点数据,缓存系统能调整算法或者内部存储方式,使得将最有可能频繁访问的数据能尽快访问到。
  • On-Heap,Java分配对象都是在堆内存里,有最快的获取速度。由于虚拟机的垃圾回收管理,缓存放过多的对象会导致垃圾回收时间过长,从而有可能影响性能。
  • Off-Heap,堆外内存,对象存放到在虚拟机分配的堆外内存,因此不受垃圾回收管理的管理,不影响系统系统,但堆外内存的对象要被使用,还要序列化成堆内对象。很多缓存工具会把不常用的对象放到堆外,把热点数据放到堆内。

Cache的单机应用

单机应用,Cache可以与应用系统位于一个虚拟机,这样的好处是访问速度最快。如果缓存数量大,可以通过一定的策略删除访问较少的缓存项,如 ehcache 就提供了删除访问较少数据,或则将较少访问数据暂存堆外或者硬盘上的功能

image.png

单机应用与缓存系统放在一起的缺点是缓存系统会占用大量内存,因此,对有些缓存项,如伪静态页面等,也可以使用分布式缓存,用来缓存较多的数据。应用通过高速局域网来访问,架构图如下

使用专有的Cache服务器

应用系统一般都是分布式应用,如果缓存位于虚拟机内,需要解决的问题是,当数据改变的时候,需要通知其他应用系统的缓存系统让其缓存失效或者重新加载缓存,ehcache就是通过Terracotta来组件一个缓存集群 。使用reids作为缓存,reids作为单独的缓存服务器,分布式应用系统通过局域网访问redis,如下图


image.png

使用一二级缓存服务器

使用Reids缓存,通过网络访问还是不如从内存中获取性能好,所以通常称为二级缓存,从内存中取得缓存数据称之为一级缓存。当应用系统需要查询缓存的时候,先从一级缓存里查找,如果有,则返回,如果没有查找到,则再查询二级缓存,架构图如下


image.png

Spring Boot 2 自带了前面俩种缓存的实现方式,本问将简单实现第三种,高速一二级缓存实现

高速一二级缓存实现

实现 TowLevelCacheManager

首先,创建创建一个新的缓存管理器,命名为TowLevelCacheManager,继承了Spring Boot的RedisCacheManager,重载decorateCache方法。返回的是我们新创建的LocalAndRedisCache 缓存实现。


    class TowLevelCacheManager extends RedisCacheManager {
        RedisTemplate redisTemplate;
        public TowLevelCacheManager(RedisTemplate redisTemplate,RedisCacheWriter cacheWriter, RedisCacheConfiguration defaultCacheConfiguration) {
            super(cacheWriter,defaultCacheConfiguration);
            this.redisTemplate = redisTemplate;
        }
        //使用RedisAndLocalCache代替Spring Boot自带的RedisCache
        @Override
        protected Cache decorateCache(Cache cache) {
            return new RedisAndLocalCache(this, (RedisCache) cache);
        }
    
      public void publishMessage(String cacheName) {
        this.redisTemplate.convertAndSend(topicName, cacheName);
      }
      // 接受一个消息清空本地缓存
      public void receiver(String name) {
        RedisAndLocalCache cache = ((RedisAndLocalCache) this.getCache(name));
        if(cache!=null){
          cache.clearLocal();
        }
      }
    
    }

在Spring Cache中,在缓存管理器创建好每个缓存后,都会调用decorateCache方法,这样缓存管理器子类有机会实现自己的扩展,在这段代码,返回了自定义的RedisAndLocalCache实现。 publishMessage方法提供个给Cache,用于当缓存更新的时候,使用Redis的消息机制通知其他分布式节点的一级别缓存。receiver方法对应于publishMessage方法,当收到消息后,会清空一节缓存。

创建RedisAndLocalCache

RedisAndLocalCache 是我们系统的核心,他实现了Cache接口,类,会实现如下操作。

  • get操作,通过Key取对应的缓存项,在调用父类RedisCache之前,会先检测本地缓存是否存在,存在则不需要调用父类的get操作。如果不存在,调用父类的get操作后,将Redis返回的ValueWrapper放到本地缓存里待下次用。
  • put,调用父类put操作更新Redis缓存,同时广播消息,缓存改变。我们将在下一章讲如何使用Redis的Pub/Subscribe 来同步缓存
  • evict ,同put操作一样,调用父类处理,清空对应的缓存,同时广播消息
  • putIfAbsent,同put操作一样,调用父类实现,同时广播消息

RedisAndLocalCache 的构造如下


    class RedisAndLocalCache implements Cache {
      // 本地缓存提供
      ConcurrentHashMap<Object, Object> local = new ConcurrentHashMap<Object, Object>();
      RedisCache redisCache;
      TowLevelCacheManager cacheManager;
    
      public RedisAndLocalCache(TowLevelCacheManager cacheManager, RedisCache redisCache) {
        this.redisCache = redisCache;
        this.cacheManager = cacheManager;
      }
    
      @Override
      public String getName() {
        return redisCache.getName();
      }
    
      @Override
      public Object getNativeCache() {
        return redisCache.getNativeCache();
      }
    
      //其他get put evict方法参考后面代码到吗片段说明
    }

如上代码所示,RedisAndLocalCache 实现了Cache接口,并使用了真正的RedisCache作为其实现方法。其关键的get和put方法如下

    @Override
    public ValueWrapper get(Object key) {
      // 一级缓存先取
      ValueWrapper wrapper = (ValueWrapper) local.get(key);
      if (wrapper != null) {
        return wrapper;
      } else {
        // 二级缓存取
        wrapper = redisCache.get(key);
        if (wrapper != null) {
          local.put(key, wrapper);
        }
        return wrapper;
      }
    }
    
    @Override
    public void put(Object key, Object value) {
      System.out.println(value.getClass().getClassLoader());
      redisCache.put(key, value);
      //通知其他节点缓存更新
      clearOtherJVM();
    }
    @Override
    public void evict(Object key) {
      redisCache.evict(key);
      //通知其他节点缓存更新
      clearOtherJVM();
    }
    protected void clearOtherJVM() {
        cacheManager.publishMessage(redisCache.getName());
    }
    // 提供给CacheManager清空一节缓存
    public void clearLocal() {
      this.local.clear();
    }

变量local代表了一个简单的缓存实现, 使用了ConcurrentHashMap。其get方法有如下逻辑实现

  • 通过key从本地取出 ValueWrapper
  • 如果ValueWrapper存在,则直接返回
  • 如果ValueWrapper不存在,则调用父类RedisCache取得缓存项
  • 如果缓存项为空,则说明暂时无此项,直接返回空,等@Cacheable 调用业务方法获取缓存项

put方法实现逻辑如下

  • 先调用redisCache,更新二级缓存
  • 调用clearOtherJVM方法,通知其他节点缓存更新
  • 其他节点(包括本节点)的TowLevelCacheManager收到消息后,会调用receiver方法从而实现一级缓存

为了简单起见,一级缓存的同步更新 仅仅是清空一级缓存而并非采用同步更新缓存项。一级缓存将在下一次get方法调用时会再次从Reids里加载最新数据。

一节缓存仅仅简单使用了Map实现,并未实现缓存的多种策略。因此,如果你的一级缓存如果需要各种缓存策略,还需要用一些第三方库或者自行实现,但大部分情况下TowLevelCacheManager都足够使用

缓存同步说明

当缓存发生改变的时候,需要通知分布式系统的TowLevelCacheManager的,清空一级缓存.这里使用Redis实现消息通知,关于Redis消息发布和订阅,参考Redis一章。
为了实现Redis的Pub/Sub 模式,我们需要在CacheConfig里添加一些代码,创建一个消息监听器

    //定义一个redis 的频道,默认叫cache,用于pub/sub
    @Value("${springext.cache.redis.topic:cache}")
    String topicName;
    //Redis message,参考Redis一章
    @Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
                                            MessageListenerAdapter listenerAdapter) {
      RedisMessageListenerContainer container = new RedisMessageListenerContainer();
      container.setConnectionFactory(connectionFactory);
      container.addMessageListener(listenerAdapter, new PatternTopic(topicName));
      return container;
    }

如上所示,需要配置文件配置 springext.cache.redis.topic,指定一个频道的名字,如果没有配置,默认的频道名称是cache。

配置一个监听器很简单,只需要实现MessageListenerAdapter,并注册到RedisMessageListenerContainer即可。

MessageListenerAdapter 需要实现onMessage方法,我们只需要获取消息内容,这里是指要清空的缓存名字,然后交给MyRedisCacheManager 来处理即可

    @Bean
    MessageListenerAdapter listenerAdapter(final TowLevelCacheManager cacheManager) {
      return new MessageListenerAdapter(new MessageListener() {
        public void onMessage(Message message, byte[] pattern) {
          byte[] bs = message.getChannel();
          try {
            //Sub 一个消息,通知缓存管理器,这里的type就是Cache的名字
            String type = new String(bs, "UTF-8");
            cacheManager.receiver(type);
          } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            // 不可能出错,忽略
          }
        }
      });
    }

将代码组合在一起

前三节分别实现了缓存管理器,缓存,还有缓存之间的同步,现在需要将缓存管理器配置为应用的缓存管理器,通过搭配@Configuration和@Bean实现

    @Configuration
    public class CacheConfig {
      @Bean
      public TowLevelCacheManager cacheManager(RedisTemplate redisTemplate) {
        //RedisCache需要一个RedisCacheWriter来实现读写Redis
        RedisCacheWriter writer = RedisCacheWriter.lockingRedisCacheWriter(redisTemplate.getConnectionFactory());
        /*SerializationPair用于Java和Redis之间的序列化和反序列化,我们这里使用自带的JdkSerializationRedisSerializer,并在反序列化过程中,使用当前的ClassLoader*/
        SerializationPair pair = SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(this.getClass().getClassLoader()));
        /*构造一个RedisCache的配置,比如是否使用前缀,比如Key和Value的序列化机制(*/
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(pair);
        /*创建CacheManager,并返回给Spring 容器*/
        TowLevelCacheManager cacheManager = new TowLevelCacheManager(redisTemplate,writer,config);
        return cacheManager;
      }
    }

构造一个TowLevelCacheManager较为复杂,这是因为构造RedisCacheManager复杂导致的,构造RedisCacheManager需要如下俩个参数

  • RedisCacheWriter,一个实现Redis操作的接口,SpringBoot提供了NoLock和Lock俩种实现,在缓存写操作的时候,前者有较高性能,而后者实现了Redis锁。
  • RedisCacheConfiguration 用于设置缓存特性,比如缓存项目的TTL(存活时间),缓存Key的前缀等,默认情况是TTL为0,不使用前缀。你可以为缓存管理器设置默认的配置,也可以为每一个缓存设置一个配置。
    最为重要的配置是SerializationPair,用于Java和Redis的序列化和反序列化操作,这里我们使用我们这里使用自带的JdkSerializationRedisSerializer作为序列化机制,这个类在Reids一章有详细介绍。

如上代码实现了一二级缓存,行数不到200行代码。相对于自带的RedisCache来说,缓存效率更高。相对于专业的一二级缓存服务器来说,如Ehcache+Terracotta组合,更加轻量级

国内第一本讲述Spring Boo 2的书已经由本人编著出版,有兴趣的可以访问
http://item.jd.com/12214143.html 了解详情

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,829评论 1 331
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,603评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,846评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,600评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,780评论 3 272
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,695评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,136评论 2 293
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,862评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,453评论 0 229
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,942评论 2 233
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,347评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,790评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,293评论 3 221
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,839评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,448评论 0 181
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,564评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,623评论 2 249

推荐阅读更多精彩内容