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

缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。 后来应用系统水平扩展,缓存作为一个独立系统存在,如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
上一篇:200行代码实现RPC框架


下一篇:200行代码,7个对象——让你了解ASP.NET Core框架的本质[3.x版]