文档章节

SpringBoot 2,用200行代码完成一个一二级分布式缓存

闲大赋
 闲大赋
发布于 2017/12/21 21:13
字数 3485
阅读 2523
收藏 3

缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库负载。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存获取数据,都还是要通过网络访问才能获取,效率相对于早先从内存里获取,还是不够逆天快。如果一个应用,比如传统的企业应用,一次页面显示,要访问数次redis,那效果就不是特别好,性能不够快不说,还容易使得Reids负载过高,Redis的主机出现各种物理故障。因此,现在有人提出了一二级缓存。即一级缓存跟系统在一个虚拟机内,这样速度最快。二级缓存位于redis里,当一级缓存没有数据的时候,再从redis里获取,并同步到一级缓存里。这跟CPU的一级缓存,二级缓存是一个道理。当然也面对同样的问题。

缓存概念

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,堆外内存,对象存放到在虚拟机分配的堆外内存,因此不受垃圾回收管理的管理,不影响系统系统,但堆外内存的对象要被使用,还要序列化成堆内对象。很多缓存工具会把不常用的对象放到堆外,把热点数据放到堆内。

Spring Boot 缓存

Spring Boot 本身提供了一个基于ConcurrentHashMap 的缓存机制,也集成了EhCache2.x,JCache(JSR-107,EhCache3.x,Hazelcast,Infinispan),还有Couchbase,Redies等。Spring Boot应用通过注解的方式使用统一的使用缓存,只需在方法上使用缓存注解即可,其缓存的具体实现依赖于你选择的目标缓存管理器。如下使用@Cacheable

    [@Service](https://my.oschina.net/service)
    public class MenuServiceImpl implements MenuService {
    	
    	@Cacheable("menu")
    	public Menu getMenu(Long id) {...}
     		
    }

MenuService实例作为一个容器管理bean,Spring将会生成代理类,在实际调用MenuService.getMenu方法前,会调用缓存管理器,取得名"menu"的缓存,此时,缓存的key就是方法参数id,如果缓存命中,则返回此值,如果没有找到,则进入实际的MenuService.getMenu方法,在返回调用结果给调用者之前,还会将此查询结果缓存以备下次使用。

集成Spring cache

集成Spring Cache,只需要在pom中使用如下依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

如果你使用Spring自带的内存的缓存管理器,需要在appliaction.properties里配置属性

spring.cache.type=Simple

Simple只适合单机应用或者开发环境使用或者是一个小微系统,通常你的应用是分布式应用,Spring Boot 还支持集成更多的缓存服务器。

  • simple: 基于ConcurrentHashMap实现的缓存,适合单机或者开发环境使用。

  • none:关闭缓存,比如开发阶段先确保功能正确,可以先禁止使用缓存

  • redis:使用redis作为缓存,你还需要在pom里增加redis依赖。本章缓存将重点介绍redis缓存以及扩展redis实现一二级缓存

  • Generic,用户自定义缓存实现,用户需要实现一个org.springframework.cache.CacheManager的实现

  • 其他还有JCache,EhCache 2.x,Hazelcast等,为了保持本书的简单,将不在这里一一介绍。

最后,需要使用注解 @EnableCaching 打开缓存功能。

@SpringBootApplication
@EnableCaching
public class Ch14Application {
  public static void main(String[] args) {
    SpringApplication.run(Ch14Application.class, args);
  }
}

实现Redis 俩级缓存

SpringBoot自带的Redis缓存非常容易使用,但由于通过网络访问了Redis,效率还是比传统的跟应用部署在一起的一级缓存略慢。本章中,扩展RedisCacheManager和RedisCache,在访问Redis之前,先访问一个ConcurrentHashMap实现的简单一级缓存,如果有缓存项,则返回给应用,如果没有,再从Redis里取,并将缓存对象放到一级缓存里

当缓存项发生变化的时候,注解@CachePut 和 @CacheEvict会触发RedisCache的put( Object key, Object value)和evict(Object key)操作,俩级缓存需要同时更新ConcurrentHashMap和Redis缓存,且需要通过Redis的Pub发出通知消息,其他Spring Boot应用通过Sub来接收消息,同步更新Spring Boot应用自身的一级缓存。

为了简单起见,一级缓并没有缓存过期策略,用户系统如果会有大量数据需要放到一级缓存,需要再次扩展这里的代码,比如使用LRUHashMap代替Map

实现 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;
@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 Boot 2精髓:从构建小系统到架构分布式大系统>, 此例子可以直接从gitee上下载 https://gitee.com/xiandafu/Spring-Boot-2.0-Samples 欢迎反馈

© 著作权归作者所有

共有 人打赏支持
闲大赋

闲大赋

粉丝 1160
博文 94
码字总数 87660
作品 10
西城
架构师
私信 提问
加载中

评论(9)

闲大赋
闲大赋

引用来自“FrendLin”的评论

部分实际服务器分布中缓存服务器也是应用服务器本机的话,这种一二级缓存架构是不是就不适合了?
如果是本机,那不走网络会好很多,但肯定不如在一个JVM里快啊
FrendLin
FrendLin
部分实际服务器分布中缓存服务器也是应用服务器本机的话,这种一二级缓存架构是不是就不适合了?
tianxia007
tianxia007
666
闲大赋
闲大赋

引用来自“红心A”的评论

如果多个节点同时调用clearOtherJVM对同一个缓存操作,会不会收到影响?
没有,删除多次呗,业务如果从本地缓存取不到,就去取redis缓存,所以没有影响
红心A
如果多个节点同时调用clearOtherJVM对同一个缓存操作,会不会收到影响?
OSC首席酱油党
OSC首席酱油党
Redis怎么会写成Redies?
闲大赋
闲大赋

引用来自“风摆残荷”的评论

TowLevelCacheManager,这里是不是应该是:TwoLevelCacheManager?
😅
风摆残荷
TowLevelCacheManager,这里是不是应该是:TwoLevelCacheManager?
红薯
红薯
哈哈 ~ #J2Cache#
SpringBoot,用200行代码完成一个一二级分布式缓存

缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。 后来应用系统水平扩展,缓存作为一个独立系统存在,如red...

闲大赋
2017/02/27
0
37
恒宇少年/spring-boot-chapter

简书整套文档以及源码解析 专题 专题名称 专题描述 001 Spring Boot 核心技术 讲解SpringBoot一些企业级层面的核心组件 002 Spring Cloud 核心技术 对Spring Cloud核心技术全面讲解 003 Quer...

恒宇少年
04/19
0
0
Spring Boot 1 和 Spring Boo 2的差别

有差别,但差别不大。基本上基于SpringBoot的代码不需要改动,但有些配置属性和配置类,可能要改动,改动原因是 配置已经不存在或者改名类已经不存在改名 听着挺吓人,但我实际切换过程中改动...

闲大赋
2017/11/29
0
34
springboot中使用自定义两级缓存

  工作中用到了springboot的缓存,使用起来挺方便的,直接引入redis或者ehcache这些缓存依赖包和相关缓存的starter依赖包,然后在启动类中加入@EnableCaching注解,然后在需要的地方就可以...

泪o滴
05/23
0
0
企业级 SpringCloud+SpringBoot(一) 服务的注册与发现(Eureka)

一、spring cloud简介 spring cloud 为开发人员提供了快速构建分布式系统的一些工具,包括配置管理、服务发现、断路器、路由、微代理、事件总线、全局锁、决策竞选、分布式会话等等。它运行环...

itcloud
12/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

flutter Expanded用法

使用的地方:一个分类,类似京东的,左右两边都可以滑动 Widget build(BuildContext context) { return Row(children: [ Column( children: <Widget>[ Ex......

大灰狼wow
13分钟前
2
0
Java8 Map中新增的方法使用总结

前言 得益于 Java 8 的 default 方法特性,Java 8 对 Map 增加了不少实用的默认方法,像 getOrDefault, forEach, replace, replaceAll, putIfAbsent, remove(key, value), computeIfPresent,......

kaixin_code
23分钟前
1
0
@TransactionConfiguration

@TransactionConfiguration过时与替代写法 @TransactionConfiguration 替代写法

miaojiangmin
25分钟前
0
0
浅谈Vue响应式(数组变异方法)

很多初使用Vue的同学会发现,在改变数组的值的时候,值确实是改变了,但是视图却无动于衷,果然是因为数组太高冷了吗? 查看官方文档才发现,不是女神太高冷,而是你没用对方法。 看来想让女...

开元中国2015
27分钟前
2
0
Elasticsearch通关教程(五):如何通过SQL查询Elasticsearch

  这篇博文本来是想放在全系列的大概第五、六篇的时候再讲的,毕竟查询是在索引创建、索引文档数据生成和一些基本概念介绍完之后才需要的。当前面的一些知识概念全都讲解完之后再讲解查询是...

SEOwhywhy
46分钟前
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部