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

Stella981
• 阅读 956

     缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库复杂。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。 后来应用系统水平扩展,缓存作为一个独立系统存在,如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
点赞
收藏
评论区
推荐文章
Jacquelyn38 Jacquelyn38
1年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
刚刚好 刚刚好
4个月前
css问题
1、在IOS中图片不显示(给图片加了圆角或者img没有父级)<div<imgsrc""/</divdiv{width:20px;height:20px;borderradius:20px;overflow:h
blmius blmius
1年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
艾木酱 艾木酱
3个月前
快速入门|使用MemFire Cloud构建React Native应用程序
MemFireCloud是一款提供云数据库,用户可以创建云数据库,并对数据库进行管理,还可以对数据库进行备份操作。它还提供后端即服务,用户可以在1分钟内新建一个应用,使用自动生成的API和SDK,访问云数据库、对象存储、用户认证与授权等功能,可专
Stella981 Stella981
1年前
KVM调整cpu和内存
一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid
Easter79 Easter79
1年前
Twitter的分布式自增ID算法snowflake (Java版)
概述分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的。有些时候我们希望能使用一种简单一些的ID,并且希望ID能够按照时间有序生成。而twitter的snowflake解决了这种需求,最初Twitter把存储系统从MySQL迁移
Wesley13 Wesley13
1年前
MySQL查询按照指定规则排序
1.按照指定(单个)字段排序selectfromtable_nameorderiddesc;2.按照指定(多个)字段排序selectfromtable_nameorderiddesc,statusdesc;3.按照指定字段和规则排序selec
Stella981 Stella981
1年前
SpringBoot 2,用200行代码完成一个一二级分布式缓存
缓存系统的用来代替直接访问数据库,用来提升系统性能,减小数据库负载。早期缓存跟系统在一个虚拟机里,这样内存访问,速度最快。后来应用系统水平扩展,缓存作为一个独立系统存在,如redis,但是每次从缓存获取数据,都还是要通过网络访问才能获取,效率相对于早先从内存里获取,还是不够逆天快。如果一个应用,比如传统的企业应用,一次页面显示,要访问数次redis,那效果
Wesley13 Wesley13
1年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
helloworld_28799839 helloworld_28799839
4个月前
常用知识整理
Javascript判断对象是否为空jsObject.keys(myObject).length0经常使用的三元运算我们经常遇到处理表格列状态字段如status的时候可以用到vue
helloworld_34035044 helloworld_34035044
6个月前
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为