zoukankan      html  css  js  c++  java
  • spring(三、spring中的eheche缓存、redis使用)

    spring(三、spring中的eheche缓存、redis使用)

    本文主要介绍为什么要构建ehcache+redis两级缓存?以及在实战中如何实现?思考如何配置缓存策略更合适?这样的方案可能遗留什么问题?JUST DO IT! GO!


    问题描述

    场景:我们的应用系统是分布式集群的,可横向扩展的。应用中某个接口操作满足以下一个或多个条件: 
    1. 接口运行复杂代价大, 
    2. 接口返回数据量大, 
    3. 接口的数据基本不会更改, 
    4. 接口数据一致性要求不高(只需满足最终一致)。

    此时,我们会考虑将这个接口的返回值做缓存。考虑到上述条件,我们需要一套高可用分布式的缓存集群,并具备持久化功能,备选的有ehcache集群或redis主备(sentinel)。

    • ehcache集群因为节点之间数据同步通过组播的方式,可能带来的问题:节点间大量的数据复制带来额外的开销,在节点多的情况下此问题越发严重,N个节点会出现N-1次网络传输数据进行同步。(见下图,缓存集群中有三台机器,其中一台机器接收到数据,需要拷贝到其他机器,一次input后需要copy两次,两次copy是需要网络传输消耗的) 
      这里写图片描述
    • redis主备由于作为中心节点提供缓存,其他节点都向redis中心节点取数据,所以,一次网络传输即可。(当然此处的一次网络代价跟组播的代价是不一样的)但是,随着访问量增大,大量的缓存数据访问使得应用服务器和缓存服务器之间的网络I/O消耗越大。(见下图,同样三台应用服务器,redis sentinel作为中心节点缓存。所谓中心,即所有应用服务器以redis为缓存中心,不再像ehcache集群,缓存是分散存放在应用服务器中,需要互相同步的,任何一台应用服务器的input,都会经过一次copy网络传输到redis,由于redis是中心共享的,那么就可以不用同步的步骤,其他应用服务器需要只需去get取即可。但是,我们会发现多了N台服务器的get的网络开销。)

    这里写图片描述

    提出方案

    那么要怎么处理呢?所以两级缓存的思想诞生了,在redis的方案上做一步优化,在缓存到远程redis的同时,缓存一份到本地进程ehcache(此处的ehcache不用做集群,避免组播带来的开销),取缓存的时候会先取本地,没有会向redis请求,这样会减少应用服务器<–>缓存服务器redis之间的网络开销。(见下图,为了减少get这几条网络传输,我们会在每个应用服务器上增加本地的ehcache缓存作为二级缓存,即第一次get到的数据存入ehcache,后面output输出即可从本地ehcache中获取,不用再访问redis了,所以就减少了以后get的网络开销。get开销只要一次,后续不需要了,除非本地缓存过期需要再get。) 
    这里写图片描述 
    如果用过j2cache的都应该知道,oschina用j2cache这种两级缓存,实践证明了该方案是可行的。该篇使用spring+ehcache+redis实现更加简洁。


    方案实施

    1、 spring和ehcache集成

    主要获取ehcache作为操作ehcache的对象。

    ehcache.xml 代码如下:

    
    <ehcache updateCheck="false" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:noNamespaceSchemaLocation="http://ehcache.sf.net/ehcache.xsd">
    
        <diskStore path="java.io.tmpdir/ehcache"/>
    
       <!--  默认的管理策略 
        maxElementsOnDisk: 在磁盘上缓存的element的最大数目,默认值为0,表示不限制。 
        eternal:设定缓存的elements是否永远不过期。如果为true,则缓存的数据始终有效,如果为false那么还要根据timeToIdleSeconds,timeToLiveSeconds判断。
        diskPersistent: 是否在磁盘上持久化。指重启jvm后,数据是否有效。默认为false。 
        diskExpiryThreadIntervalSeconds:对象检测线程运行时间间隔。标识对象状态(过期/持久化)的线程多长时间运行一次。 
        -->
        <defaultCache maxElementsInMemory="10000"
                      eternal="false"
                      timeToIdleSeconds="3600"
                      timeToLiveSeconds="3600"
                      overflowToDisk="true"
                      diskPersistent="false"
                      diskExpiryThreadIntervalSeconds="120"
                      memoryStoreEvictionPolicy="LRU"/>
    
        <!-- 对象无过期,一个1000长度的队列,最近最少使用的对象被删除 -->
         <cache name="userCache"
               maxElementsInMemory="1000"
               eternal="true"
               overflowToDisk="false"
               timeToIdleSeconds="0"
               timeToLiveSeconds="0"
               memoryStoreEvictionPolicy="LFU">
         </cache>
    
        <!-- 组播方式:multicastGroupPort需要保证与其他系统不重复,进行端口注册  -->
        <!-- 若因未注册,配置了重复端口,造成权限缓存数据异常,请自行解决  -->
        <cacheManagerPeerProviderFactory
                class="net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory"
                properties="peerDiscovery=automatic,
                            multicastGroupAddress=230.0.0.1,
                            multicastGroupPort=4546, timeToLive=1"/>
    
    <!-- replicatePuts=true | false – 当一个新元素增加到缓存中的时候是否要复制到其他的peers. 默认是true。 -->
    <!-- replicateUpdates=true | false – 当一个已经在缓存中存在的元素被覆盖时是否要进行复制。默认是true。 -->
    <!-- replicateRemovals= true | false – 当元素移除的时候是否进行复制。默认是true。 -->
    <!-- replicateAsynchronously=true | false – 复制方式是异步的(指定为true时)还是同步的(指定为false时)。默认是true。 -->
    <!-- replicatePutsViaCopy=true | false – 当一个新增元素被拷贝到其他的cache中时是否进行复制指定为true时为复制,默认是true。 -->
    <!-- replicateUpdatesViaCopy=true | false – 当一个元素被拷贝到其他的cache中时是否进行复制(指定为true时为复制),默认是true。 -->
    
         <cache name="webCache_LT"
               maxElementsInMemory="10000"
               eternal="false"
               overflowToDisk="false"
               timeToIdleSeconds="3600"
               timeToLiveSeconds="3600"
               memoryStoreEvictionPolicy="LRU">
            <cacheEventListenerFactory
                    class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
                    properties="replicateRemovals=true"/>
             <bootstrapCacheLoaderFactory
                     class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/> 
        </cache>
    
        <cache name="webCache_ST"
               maxElementsInMemory="1000"
               eternal="false"
               overflowToDisk="false"
               timeToIdleSeconds="300"
               timeToLiveSeconds="300"
               memoryStoreEvictionPolicy="LRU">
            <cacheEventListenerFactory
                    class="net.sf.ehcache.distribution.RMICacheReplicatorFactory"
                    properties="replicateRemovals=true"/>
            <bootstrapCacheLoaderFactory
                    class="net.sf.ehcache.distribution.RMIBootstrapCacheLoaderFactory"/>
        </cache>
    
    </ehcache>
    
    

    spring注入jedisPool、redisConnFactory、redisTemplate对象

    
    <!-- 加载redis.propertis -->
        <bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"> 
            <property name="locations" value="classpath:redis.properties"/>
        </bean>
    
        <!-- Redis 连接池 -->
        <bean id="jedisPool" class="redis.clients.jedis.JedisPoolConfig">
            <property name="maxTotal" value="${redis.pool.maxActive}" />
            <property name="maxIdle" value="${redis.pool.maxIdle}" />
            <property name="testOnBorrow" value="${redis.pool.testOnBorrow}" />
            <property name="testOnReturn" value="${redis.pool.testOnReturn}" />
            <property name="maxWaitMillis" value="${redis.pool.maxWait}" />
        </bean>
    
        <!-- Redis 连接工厂 -->
        <bean id="redisConnFactory"
            class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
            <property name="hostName" value="${redis.ip}" />
            <property name="port" value="${redis.port}" />
            <!-- property name="password" value="${redis.password}" -->
            <property name="timeout" value="${redis.timeout}" />
            <property name="poolConfig" ref="jedisPool" />
        </bean>
    
        <!-- redis 操作对象 -->
        <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
            <property name="connectionFactory" ref="redisConnFactory" />
        </bean>
    
    

    3、 spring集成ehcache和redis

    通过上面两步注入的ehcache和redisTemplate我们就能自定义一个方法将两者整合起来。详见EhRedisCache类。

    EhRedisCache.java

    
    /**
     * 两级缓存,一级:ehcache,二级为redisCache
     * @author yulin
     *
     */
    public class EhRedisCache implements Cache{
    
    
        private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
    
        private String name;
    
        private net.sf.ehcache.Cache ehCache;
    
        private RedisTemplate<String, Object> redisTemplate;
    
         private long liveTime = 1*60*60; //默认1h=1*60*60
    
        @Override
        public String getName() {
            return this.name;
        }
    
        @Override
        public Object getNativeCache() {
            return this;
        }
    
        @Override
        public ValueWrapper get(Object key) {
             Element value = ehCache.get(key);
             LOG.info("Cache L1 (ehcache) :{}={}",key,value);
             if (value!=null) {
                 return (value != null ? new SimpleValueWrapper(value.getObjectValue()) : null);
             } 
             //TODO 这样会不会更好?访问10次EhCache 强制访问一次redis 使得数据不失效
             final String keyStr = key.toString();  
             Object objectValue = redisTemplate.execute(new RedisCallback<Object>() {  
                public Object doInRedis(RedisConnection connection)  
                        throws DataAccessException {  
                    byte[] key = keyStr.getBytes();  
                    byte[] value = connection.get(key);  
                    if (value == null) {  
                        return null;  
                    }  
                    //每次获得,重置缓存过期时间
                    if (liveTime > 0) {  
                        connection.expire(key, liveTime);  
                    }  
                    return toObject(value);  
                }  
            },true);  
             ehCache.put(new Element(key, objectValue));//取出来之后缓存到本地
             LOG.info("Cache L2 (redis) :{}={}",key,objectValue);
             return  (objectValue != null ? new SimpleValueWrapper(objectValue) : null);
    
        }
    
        @Override
        public void put(Object key, Object value) {
            ehCache.put(new Element(key, value));
            final String keyStr =  key.toString(); 
            final Object valueStr = value;  
            redisTemplate.execute(new RedisCallback<Long>() {  
                public Long doInRedis(RedisConnection connection)  
                        throws DataAccessException {  
                    byte[] keyb = keyStr.getBytes();  
                    byte[] valueb = toByteArray(valueStr);  
                    connection.set(keyb, valueb);  
                    if (liveTime > 0) {  
                        connection.expire(keyb, liveTime);  
                    }  
                    return 1L;  
                }  
            },true);  
    
        }
    
        @Override
        public void evict(Object key) {
            ehCache.remove(key);
            final String keyStr =  key.toString();  
            redisTemplate.execute(new RedisCallback<Long>() {  
                public Long doInRedis(RedisConnection connection)  
                        throws DataAccessException {  
                    return connection.del(keyStr.getBytes());  
                }  
            },true); 
        }
    
        @Override
        public void clear() {
            ehCache.removeAll();
            redisTemplate.execute(new RedisCallback<String>() {  
                public String doInRedis(RedisConnection connection)  
                        throws DataAccessException {  
                    connection.flushDb();  
                    return "clear done.";  
                }  
            },true);
        }
    
        public net.sf.ehcache.Cache getEhCache() {
            return ehCache;
        }
    
        public void setEhCache(net.sf.ehcache.Cache ehCache) {
            this.ehCache = ehCache;
        }
    
        public RedisTemplate<String, Object> getRedisTemplate() {
            return redisTemplate;
        }
    
        public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
            this.redisTemplate = redisTemplate;
        }
    
        public long getLiveTime() {
            return liveTime;
        }
    
        public void setLiveTime(long liveTime) {
            this.liveTime = liveTime;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        /** 
         * 描述 : Object转byte[]. <br> 
         * @param obj 
         * @return 
         */  
        private byte[] toByteArray(Object obj) {  
            byte[] bytes = null;  
            ByteArrayOutputStream bos = new ByteArrayOutputStream();  
            try {  
                ObjectOutputStream oos = new ObjectOutputStream(bos);  
                oos.writeObject(obj);  
                oos.flush();  
                bytes = bos.toByteArray();  
                oos.close();  
                bos.close();  
            } catch (IOException ex) {  
                ex.printStackTrace();  
            }  
            return bytes;  
        }  
    
        /** 
         * 描述 :  byte[]转Object . <br> 
         * @param bytes 
         * @return 
         */  
        private Object toObject(byte[] bytes) {  
            Object obj = null;  
            try {  
                ByteArrayInputStream bis = new ByteArrayInputStream(bytes);  
                ObjectInputStream ois = new ObjectInputStream(bis);  
                obj = ois.readObject();  
                ois.close();  
                bis.close();  
            } catch (IOException ex) {  
                ex.printStackTrace();  
            } catch (ClassNotFoundException ex) {  
                ex.printStackTrace();  
            }  
            return obj;  
        }  
    }
    
    

    spring注入自定义缓存

    
     <!-- 自定义ehcache+redis-->
       <bean id="ehRedisCacheManager" class="org.springframework.cache.support.SimpleCacheManager">  
            <property name="caches">  
                <set>  
                   <bean  id="ehRedisCache" class="org.musicmaster.yulin.ercache.EhRedisCache">  
                         <property name="redisTemplate" ref="redisTemplate" />  
                         <property name="ehCache" ref="ehCache"/> 
                         <property name="name" value="userCache"/> 
                    <!-- <property name="liveTime" value="3600"/>  --> 
                    </bean>
                </set>  
            </property>  
        </bean>  
    
        <!-- 注解声明 -->
        <cache:annotation-driven cache-manager="ehRedisCacheManager" 
                proxy-target-class="true"  /> 
    
    

    4、 模拟问题中提到的接口

    此处假设该接口满足上述条件。

    UserService.java

    
    public interface UserService {
    
        User findById(long id);
    
        List<User> findByPage(int startIndex, int limit);
    
        List<User> findBySex(Sex sex);
    
        List<User> findByAge(int lessAge);
    
        List<User> findByUsers(List<User> users);
    
        boolean update(User user);
    
        boolean deleteById(long id);
    }
    
    

    UserServiceImpl.java

    
    @Service
    public class UserServiceImpl implements UserService{
    
        private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class);
    
        @Cacheable("userCache")
        @Override
        public User findById(long id) {
            LOG.info("visit business service findById,id:{}",id);
            User user = new User();
            user.setId(id);
            user.setUserName("tony");
            user.setPassWord("******");
            user.setSex(Sex.M);
            user.setAge(32);
            //耗时操作
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return user;
        }
    
    
        @Override
        public List<User> findByPage(int startIndex, int limit) {
            return null;
        }
    
        @Cacheable("userCache")
        @Override
        public List<User> findBySex(Sex sex) {
            LOG.info("visit business service findBySex,sex:{}",sex);
            List<User> users = new ArrayList<User>();
            for (int i = 0; i < 5; i++) {
                User user = new User();
                user.setId(i);
                user.setUserName("tony"+i);
                user.setPassWord("******");
                user.setSex(sex);
                user.setAge(32+i);
                users.add(user);
            }
            return users;
        }
    
        @Override
        public List<User> findByAge(int lessAge) {
            // TODO Auto-generated method stub
            return null;
        }
    
        //FIXME 此处将list参数的地址作为key存储,是否有问题?
        @Cacheable("userCache")
        @Override
        public List<User> findByUsers(List<User> users) {
            LOG.info("visit business service findByUsers,users:{}",users);
            return users;
        }
    
    
        @CacheEvict("userCache")
        @Override
        public boolean update(User user) {
            return true;
        }
    
        @CacheEvict("userCache")
        @Override
        public boolean deleteById(long id) {
            return false;
        }
    
    }
    
    
    

    User.java

    public class User implements Serializable {
    
        private static final long serialVersionUID = 1L;
        public enum Sex{
            M,FM
        }
        private long id;
        private String userName;
        private String passWord;
        private int age;
        private Sex sex;
    
        public long getId() {
            return id;
        }
        public void setId(long id) {
            this.id = id;
        }
        public String getUserName() {
            return userName;
        }
        public void setUserName(String userName) {
            this.userName = userName;
        }
        public String getPassWord() {
            return passWord;
        }
        public void setPassWord(String passWord) {
            this.passWord = passWord;
        }
        public int getAge() {
            return age;
        }
        public void setAge(int age) {
            this.age = age;
        }
        public Sex getSex() {
            return sex;
        }
        public void setSex(Sex sex) {
            this.sex = sex;
        }
        @Override
        public String toString() {
            return "User [id=" + id + ", userName=" + userName + ", passWord="
                    + passWord + ", age=" + age + ", sex=" + sex + "]";
        }
    
    }
    

    实施结果

    我们写个测试类来模拟下

    TestEhRedisCache.java

    
    public class TestEhRedisCache{
    
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("spring-ehRedisCache.xml");
            UserService userService= (UserService) context.getBean("userServiceImpl");
            System.out.println(userService.findById(5l));
            System.out.println(userService.findById(5l));
            System.out.println(userService.findById(5l));
            System.out.println(userService.findById(5l));
            System.out.println(userService.findById(5l));
        }
    }
    

    TEST1 输出结果:

    
    Cache L1 (ehcache) :UserServiceImpl/findById/5=null
    Cache L2 (redis) :UserServiceImpl/findById/5=null
    visit business service findById,id:5
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    

    上面第一次访问,一级缓存ehcache和二级缓存redis都没有数据,访问接口耗时操作,打印日志:

    visit business service findById,id:5

    第二次之后的访问,都会访问一级缓存ehcache,此时响应速度很快。

    TEST2 在TEST1结束后,我们在liveTime的时间内,也就是redis缓存还未过期再次执行,会出现以下结果

    
    Cache L1 (ehcache) :UserServiceImpl/findById/5=null
    Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]
    Cache L1 (ehcache) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=******, age=32, sex=M]
    User [id=5, userName=tony, passWord=******, age=32, sex=M]

    由于TEST1执行完结束后,ehcache为进程间的缓存,自然随着运行结束而释放,所以TEST2出现:

    Cache L1 (ehcache) :UserServiceImpl/findById/5=null

    然而在第二次访问二级缓存redis,还未到缓存过期时间,所以在redis中找到数据(同时数据入一级缓存ehcache):

    Cache L2 (redis) :UserServiceImpl/findById/5=User [id=5, userName=tony, passWord=**, age=32, sex=M]

    此处不会visit….没有经过接口的耗时操作,接下来数据都可以在本地缓存ehcache中获取。

  • 相关阅读:
    easyUI 后台经典框架DEMO下载
    一个通过JSONP跨域调用WCF REST服务的例子(以jQuery为例)
    WCF的三个名称/命名空间,你是否傻傻分不清楚?
    未找到与约束ContractName Microsoft.VisualStudio.Text.ITextDocumentFactoryService... 匹配的导出 VS2012报错
    jquery easyui tabs单击刷新右键刷新
    在64位Windows7上安装64位Oracle11g
    Linux入门
    服务器硬件知识
    IP后面带/30 /29 /27等是什么意思?
    vitualbox安装centos7卡死
  • 原文地址:https://www.cnblogs.com/XiOrang/p/9337996.html
Copyright © 2011-2022 走看看