文档章节

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

闲大赋
 闲大赋
发布于 2017/02/27 16:36
字数 943
阅读 7604
收藏 0

     缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。 后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存获取数据,都还是要通过网络访问才能获取,效率相对于早先从内存里获取,还是差了点。如果一个应用,比如传统的企业应用,一次页面显示,要访问数次redis,那效果就不是特别好,因此,现在有人提出了一二级缓存。即一级缓存跟系统在一个虚拟机内,这样速度最快。二级缓存位于redis里,当一级缓存没有数据的时候,再从redis里获取,并同步到一级缓存里。

现在实现这种一二级缓存的也挺多的,比如 hazelcast,新版的Ehcache..不过,实际上,如果你用spring boot,手里又一个Redis,则不需要搞hazelcastEhcache,只需要200行代码,就能在spring boot基础上,提供一个一二级缓存,代码如下:


import java.io.UnsupportedEncodingException;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.bind.RelaxedPropertyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.data.redis.cache.RedisCache;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCachePrefix;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.connection.MessageListener;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.listener.PatternTopic;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.listener.adapter.MessageListenerAdapter;



@Configuration
@Conditional(StarterCacheCondition.class)
public class CacheConfig {
	
	@Value("${springext.cache.redis.topic:cache}")
	String topicName ;
	
	
	
	@Bean
	public MyRedisCacheManager cacheManager(RedisTemplate<Object, Object> redisTemplate) {
		MyRedisCacheManager cacheManager = new MyRedisCacheManager(redisTemplate);
		cacheManager.setUsePrefix(true);
		return cacheManager;
	}

@Bean
    RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory,
            MessageListenerAdapter listenerAdapter) {

        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(connectionFactory);
        container.addMessageListener(listenerAdapter, new PatternTopic(topicName));

        return container;
    }

    @Bean
    MessageListenerAdapter listenerAdapter(MyRedisCacheManager cacheManager ) {
        return new MessageListenerAdapter(new MessageListener(){

			@Override
			public void onMessage(Message message, byte[] pattern) {
				byte[] bs = message.getChannel();
				try {
					String type = new String(bs,"UTF-8");
					cacheManager.receiver(type);
				} catch (UnsupportedEncodingException e) {
					e.printStackTrace();
					// 不可能出错
				}
			
				
				
			}
        	
        });
    }
	
	
	
	class MyRedisCacheManager extends RedisCacheManager{
		
		
		public MyRedisCacheManager(RedisOperations redisOperations) {
			super(redisOperations);
			
		}
		
		
		@SuppressWarnings("unchecked")
		@Override
		protected RedisCache createCache(String cacheName) {
			long expiration = computeExpiration(cacheName);
			return new MyRedisCache(this,cacheName, (this.isUsePrefix()? this.getCachePrefix().prefix(cacheName) : null), this.getRedisOperations(), expiration);
		}
		
		/**
		 * get a messsage for update cache
		 * @param cacheName
		 */
		public void receiver(String cacheName){
			MyRedisCache cache = (MyRedisCache)this.getCache(cacheName);
			if(cache==null){
				return ;
			}
			cache.cacheUpdate();
			
		}
		
		//notify other redis clent to update cache( clear local cache in fact)
		public void publishMessage(String cacheName){
			this.getRedisOperations().convertAndSend(topicName, cacheName);
		}
		
	}
	
	class MyRedisCache extends RedisCache{
		//local cache for performace
		ConcurrentHashMap<Object,ValueWrapper> local = new ConcurrentHashMap<>();
		MyRedisCacheManager cacheManager;
		public MyRedisCache(MyRedisCacheManager cacheManager,String name, byte[] prefix,
				RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration) {
			super(name, prefix, redisOperations, expiration);
			this.cacheManager = cacheManager;
		}
		@Override
		public ValueWrapper get(Object key) {
			ValueWrapper wrapper = local.get(key);
			if(wrapper!=null){
				return wrapper;
			}else{
				wrapper =   super.get(key);
				if(wrapper!=null){
					local.put(key, wrapper);
				}
				
				return wrapper;
			}
			
		}
		
		@Override
		public void put(final Object key, final Object value) {

			super.put(key, value);
			cacheManager.publishMessage(super.getName());
		}
		
		@Override
		public void evict(Object key) {
			super.evict(key);
			cacheManager.publishMessage(super.getName());
		}
		
		
		@Override
		public ValueWrapper putIfAbsent(Object key, final Object value){
			ValueWrapper wrapper = super.putIfAbsent(key, value);
			cacheManager.publishMessage(super.getName());
			return wrapper;
		}
		
		public void cacheUpdate(){
			//clear all cache for simplification 
			local.clear();
		}
		
	}
	

}

class StarterCacheCondition implements Condition {

	
	@Override
	public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(
				context.getEnvironment(), "springext.cache.");
		
		String env = resolver.getProperty("type");
		if(env==null){
			return false;
		}
		return "local2redis".equalsIgnoreCase(env.toLowerCase());
	
	}

}

代码的核心在于spring boot提供一个概念CacheManager&Cache用来表示缓存,并提供了多达8种实现,但由于缺少一二级缓存,因此,需要在Redis基础上扩展,因此实现了MyRedisCacheManger,以及MyRedisCache,增加一个本地缓存。

一二级缓存需要解决的的一个问题是缓存更新的时候,必须通知其他节点的springboot应用缓存更新。这里可以用Redis的 Pub/Sub 功能来实现,具体可以参考listenerAdapter方法实现。

使用的时候,需要配置如下,这样,就可以使用缓存了,性能杠杠的好

 

springext.cache.type=local2redis

# Redis服务器连接端口
spring.redis.host=172.16.86.56
spring.redis.port=6379  

 

© 著作权归作者所有

共有 人打赏支持
下一篇: 我的故事
闲大赋

闲大赋

粉丝 1164
博文 95
码字总数 88056
作品 10
西城
架构师
私信 提问
加载中

评论(37)

not3
not3

引用来自“not3”的评论

Pub/Sub 功能。发布的消息,多久会失效呢?还是一直会存在与redis中?

引用来自“闲大赋”的评论

一直存在,对于缓存是元数据,不用担心。量不大,但对于业务数据,就需要定时清理了
如果再次pub消息到同一个topic,会把前面的消息覆盖吗
如果多次读同样topic的消息,会多次触发监听器吗?
闲大赋
闲大赋

引用来自“not3”的评论

Pub/Sub 功能。发布的消息,多久会失效呢?还是一直会存在与redis中?
一直存在,对于缓存是元数据,不用担心。量不大,但对于业务数据,就需要定时清理了
not3
not3
Pub/Sub 功能。发布的消息,多久会失效呢?还是一直会存在与redis中?
掉尾巴狼
此为吸星大法,到最后会撑爆自己
Fly的狐狸
Fly的狐狸

引用来自“雪之舞”的评论

很早以前就跟 @红薯 提过,为什么用group,不用redis的pub/sub呢
pub sub的代码我提交过啊
OSC闲人
OSC闲人
用spring boot重写一遍开源中国
a
azure2011
不错
SmileTower
SmileTower
去存在过期时长的缓存 无法处理
如梦技术
如梦技术
是否可以大胆一点,两级都基于Spring-cache封装呢?:neckbeard:
紫电清霜
紫电清霜
踢馆的~:bowtie:
恒宇少年/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
【SpringBoot2.0系列08】SpringBoot之redis数据缓存管理

【SpringBoot2.0系列01】初识SpringBoot 【SpringBoot2.0系列02】SpringBoot之使用Thymeleaf视图模板 【SpringBoot2.0系列03】SpringBoot之使用freemark视图模板 【SpringBoot2.0系列04】Spr...

余空啊
08/20
0
0
企业级 SpringCloud+SpringBoot(一) 服务的注册与发现(Eureka)

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

itcloud
12/04
0
0

没有更多内容

加载失败,请刷新页面

加载更多

使用Autowired和Qualifier解决多个相同类型的bean如何共存的问题

注意: 实现类UserServiceImpl,MyUserServiceImpl 需要区分:@Service("userServicel") @Service("myUserService") https://blog.csdn.net/russle/article/details/80287763......

qimh
21分钟前
1
0
SQL 语句使用to_char函数时,检索结果有空格

小疯在使用Oracle过程中,使用to_char函数检索表数据时发现检索结果前面会有一个空格,对后续开发有影响。问题很好解决,比较直接对可以做一下trim处理。但是小疯很疑惑为什么会有空格呢,于...

野小疯
22分钟前
1
0
对接比特币钱包的PHP开发包

BtcTool是一个基于第三方服务和离线裸交易实现的PHP比特币应用开发包,适合不希望部署本地 节点旳PHP开发者,开发包主要包含以下特性: 利用第三方服务获取指定地址的utxo集合 离线生成消费裸...

汇智网教程
40分钟前
1
0
【自用】 VHD to VHDX

VHDX: 在VHD 2TB 的基础上提供 64TB的容量。 支持逻辑扇区大小为 4KB,和每块的大小为 256MB,来优化虚拟磁盘性能。 比VHD提供更高的安全性、可靠性和性能。 convert-VHD –path d:\Hyper-v...

Tensor丨思悟
53分钟前
3
0
30 岁转行做Python开发晚吗?而且是零基础

最近有小伙伴问小编,30 岁转行做Python开发晚吗? 小编想说,其实无论男女,只要想学,有这个动力,就直接去行动。无论年龄,无论性别,只要你想一直勇往直前,那么想做的就去做吧~这里有一...

糖宝lsh
今天
11
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部