缓存的一些事

#作者:彭秦进;

@前几天也给大家写过一篇ClassLoader之类的文章,感觉也没那么多的耗时;个人本身实践不多,所以也只能把自己的实践,慢慢的记录下来累计一起,然后写成篇幅给大家分享;

开始说今天的故事吧;缓存,相信大家见过也蛮多,比如Static 变量,List,HashMap,Ehcahe,等等;当然还有NB,前沿的如CDN等等;

缓存

一般而言,现在互联网模式(一个网站或一个应用),整体流程可以概括描述为 浏览器→应用服务器→数据库或文件(存储)→应用服务器→浏览器,这是一个标准流程,通过浏览器(或App界面)发起请求,经过服务器、数据库计算整合后反馈浏览器呈现内容。随着互联网的普及,内容信息越来越复杂,使用者和访问量越来越大,我们的应用需要支撑更多的并发量,同时我们的应用服务器和数据库服务器所做的计算也越来越多。但是往往我们的应用服务器资源是有限的,且技术变革是缓慢的,数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的),如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是减少计算量,缩短请求流程——这就是缓存。缓存的出现就是打破上述的标准流程,其中的任何一个环节都可以被截断,请求可以从缓存中直接获取目标数据并返回。通过这种打破常规的方式,有效减少计算量,缩短请求流程,有效提升响应速度,节省硬件资源,让有限的资源服务更多的用户。

 

@上面这段来自网络上的一些解释;

被坑记;

应用结构

因为我的一个项目 ILogCMS 也就是本站,最近也在做些优化,尝试采用分布式来解决问题;先介绍下原结构;

基于JFinal(MVC) + Ehcache(缓存) + Druid(Mysql 连接池) + Lucence(搜索) + Jetty (J2ee容器),其实看到这些组件大家也很清楚是哪些东东;如

  • Jfinal:WEB + ORM 更多的解释:http://www.jfinal.com/
  • Ehcache:后面解释;这篇文章也是主要说的;
  • Druid:数据链接池的控件;类似的还是dpcp pool,c3p0等
  • Lucence:这个就木必要解释了,因为我用mysql 是5.1版本的,所以自身的索引用不了了,所以使用了这个,更重要的原因是减少数据库的压力,尽量专业的事情,专业的人来做
  • Jetty:功能类似Tomcat,也不难解释,我用这个的原因其实大家可以参见前面的一片文章 ClassLoader
  • Jpress:关键的东东,我也是一个不想重复造轮子的人,所以使用了开源的blog框架Jpress,代码写还是不错,当然有部分我也慢慢的在改里面的实现了,也请原作者不要介意,嘿嘿;附上原链接

部署结构在这里就不聊了,后续找个机会也给大家介绍介绍;

“分布式”

最近无聊,新购了一台虚机,所以采用了一些方式弄了简单的“分布式”应用,于是有多台主机、应用,所以问题来了,从上面那栏目看到很多东东都是本地缓存,如Ehcache ,所以问题来了,大家公用,就无法协同了,以及各个应用的信息不一致;如遇到问题有:

  • 登陆的信息大家不同步,比如在A应用登陆的,B应用就没有登陆
  • A应用的缓存的界面,和B应用的界面不一致等等,
  • A应用发布了文章,或更新了文章,B应用的版本还是老的
  • ...等等问题

说说结构大家就清晰了;

        浏览器
        |
        Nginx
        |
A应用   B应用   C应用
  |       |       |
 ehcache  ..     ..

简单的图,大家意会一下;如是就要想办法协同了,怎么办呢? 就是要想办法 ehcache 的集中的问题,也讨论了两个策略:

  • 使用Ehcache的分布式缓存:
    • RMI,JGroups,JMS等等协议方式通知,当然配置简单;配置个PeerProvider通过通知应用组中其他应用来更新缓存信息;
      如:
      <cacheManagerPeerListenerFactory  
                  class="net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory"  
                  properties="hostName=localhost, port=40001, socketTimeoutMillis=2000"   
          />  
      机制也说大家也比较明确
    • 优点:简化了缓存结构,大家各自缓存,变化互相通知
    • 缺点:每个应用都要自己配置文件,动态扩展比较麻烦
  • 使用流行的redis 解决,说白了,就是部署一个redis进程,供各个应用的来调用
    • 优点:解决了公共缓存的问题
    • 缺点:就算是redis效率再高,如果大家使用缓存的时候还会出现网络消耗,这个也是一个不小的开销。

综合以上两点,慢慢的在考虑方案了;也搜索大量的资料,所以使用做了一些方案,最后考虑的方案如下;

            浏览器
            |
            Nginx
            |
    A应用   B应用   C应用
      |       |       |
     ehcache  ..     ..
            |
            (redis)集群

当然这个里面有几个关键点:

  • 缓存策略:L1:Ehcache,L2:Redis;首先到L1 Ehcache 的查询,查询不到,则到Redis查询,在查询不到会到通过dataLoader 查询,也就是MySQL查询
  • 通知:毕竟是是分布式,所以也有互相通知的问题,可以考虑通知的问题;简易的方案就是通过redis 的订阅的功能实现 基于各种各样的综合考虑,所以使用了 @红薯 的J2Cache.

J2Cache

在 OSChina.net 社区里面关注度最高,长期占据开源中国开源项目排行榜。趴趴J2Cache的源代码给大家解释解释。

j2cache.properties

首先看下配置文件:j2cache.properties;

#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism
#########################################

cache.broadcast=redis

#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache as level 1 cache
# redis -> use redis as level 2 cache
# [classname] -> use custom provider
#########################################

cache.L1.provider_class=ehcache
cache.L2.provider_class=redis

从上面看

1级缓存,Ehcache;

2级缓存,Redis;

CacheProvider

              CacheProvider
                    |
EhCacheProvider         RedisCacheProvider

上面两个类,看的比较清晰,看到一级缓存使用的是Ehcache,二级缓存是Redis,分别类为上面的Provider.细看也比较简单;

大家可以看看;


public interface CacheProvider {

    /**
     * 缓存的标识名称
     * @return return cache provider name
     */
    public String name();

    /**
     * Configure the cache
     *
     * @param regionName the name of the cache region
     * @param autoCreate autoCreate settings
     * @param listener listener for expired elements
     * @return return cache instance
     * @throws CacheException cache exception
     */
    public Cache buildCache(String regionName, boolean autoCreate, CacheExpiredListener listener) throws CacheException;

    /**
     * Callback to perform any necessary initialization of the underlying cache implementation
     * during SessionFactory construction.
     *
     * @param props current configuration settings.
     */
    public void start(Properties props) throws CacheException;

    /**
     * Callback to perform any necessary cleanup of the underlying cache implementation
     * during SessionFactory.close().
     */
    public void stop();

}

看看获取的Build Cache的比较简单获取Cache

Cache

    Cache
      |
EhCache RedisCache

这个里面就就比较简单,无非就是调用Ehcache,Redis 的增删改查接口,调用个实现,大家溜溜就不解释;

//$Id: EhCache.java 10716 2006-11-03 19:05:11Z max.andersen@jboss.com $
/**
 *  Copyright 2003-2006 Greg Luck, Jboss Inc
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package net.oschina.j2cache.ehcache;

import java.util.List;

import net.oschina.j2cache.Cache;
import net.oschina.j2cache.CacheException;
import net.oschina.j2cache.CacheExpiredListener;
import net.sf.ehcache.CacheManager;
import net.sf.ehcache.Ehcache;
import net.sf.ehcache.Element;
import net.sf.ehcache.event.CacheEventListener;

/**
 * EHCache
 */
public class EhCache implements Cache, CacheEventListener {

    private net.sf.ehcache.Cache cache;
    private CacheExpiredListener listener;

    /**
     * Creates a new Hibernate pluggable cache based on a cache name.
     *
     * @param cache The underlying EhCache instance to use.
     * @param listener cache listener
     */
    public EhCache(net.sf.ehcache.Cache cache, CacheExpiredListener listener) {
        this.cache = cache;
        this.cache.getCacheEventNotificationService().registerListener(this);
        this.listener = listener;
    }

    @SuppressWarnings("rawtypes")
    public List keys() throws CacheException {
        return this.cache.getKeys();
    }

    /**
     * Gets a value of an element which matches the given key.
     *
     * @param key the key of the element to return.
     * @return The value placed into the cache with an earlier put, or null if not found or expired
     * @throws CacheException cache exception
     */
    public Object get(Object key) throws CacheException {
        try {
            if ( key == null ) 
                return null;
            else {
                Element element = cache.get( key );
                if ( element != null )
                    return element.getObjectValue();                
            }
            return null;
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }
    }

    /**
     * Puts an object into the cache.
     *
     * @param key   a key
     * @param value a value
     * @throws CacheException if the {@link CacheManager}
     *                        is shutdown or another {@link Exception} occurs.
     */
    public void update(Object key, Object value) throws CacheException {
        put( key, value );
    }

    /**
     * Puts an object into the cache.
     *
     * @param key   a key
     * @param value a value
     * @throws CacheException if the {@link CacheManager}
     *                        is shutdown or another {@link Exception} occurs.
     */
    public void put(Object key, Object value) throws CacheException {
        try {
            Element element = new Element( key, value );
            cache.put( element );
        }
        catch (IllegalArgumentException e) {
            throw new CacheException( e );
        }
        catch (IllegalStateException e) {
            throw new CacheException( e );
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }

    }

    /**
     * Removes the element which matches the key
     * If no element matches, nothing is removed and no Exception is thrown.
     *
     * @param key the key of the element to remove
     * @throws CacheException cache exception
     */
    @Override
    public void evict(Object key) throws CacheException {
        try {
            cache.remove( key );
        }
        catch (IllegalStateException e) {
            throw new CacheException( e );
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }
    }

    /* (non-Javadoc)
     * @see net.oschina.j2cache.Cache#batchRemove(java.util.List)
     */
    @Override
    @SuppressWarnings("rawtypes")
    public void evict(List keys) throws CacheException {
        cache.removeAll(keys);
    }

    /**
     * Remove all elements in the cache, but leave the cache
     * in a useable state.
     *
     * @throws CacheException cache exception
     */
    public void clear() throws CacheException {
        try {
            cache.removeAll();
        }
        catch (IllegalStateException e) {
            throw new CacheException( e );
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }
    }

    /**
     * Remove the cache and make it unuseable.
     *
     * @throws CacheException  cache exception
     */
    public void destroy() throws CacheException {
        try {
            cache.getCacheManager().removeCache( cache.getName() );
        }
        catch (IllegalStateException e) {
            throw new CacheException( e );
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }
    }

    public Object clone() throws CloneNotSupportedException { 
        throw new CloneNotSupportedException(); 
    }

    @Override
    public void dispose() {}

    @Override
    public void notifyElementEvicted(Ehcache arg0, Element arg1) {}

    @Override
    public void notifyElementExpired(Ehcache cache, Element elem) {
        if(listener != null){
            listener.notifyElementExpired(cache.getName(), elem.getObjectKey());
        }
    }

    @Override
    public void notifyElementPut(Ehcache arg0, Element arg1) throws net.sf.ehcache.CacheException {}

    @Override
    public void notifyElementRemoved(Ehcache arg0, Element arg1) throws net.sf.ehcache.CacheException {}

    @Override
    public void notifyElementUpdated(Ehcache arg0, Element arg1) throws net.sf.ehcache.CacheException {}

    @Override
    public void notifyRemoveAll(Ehcache arg0) {}

    @Override
    public void put(Object key, Object value, Integer expireInSec) throws CacheException {
        try {
            Element element = new Element( key, value );
            element.setTimeToLive(expireInSec);
            cache.put( element );
        }
        catch (IllegalArgumentException e) {
            throw new CacheException( e );
        }
        catch (IllegalStateException e) {
            throw new CacheException( e );
        }
        catch (net.sf.ehcache.CacheException e) {
            throw new CacheException( e );
        }
    }

    @Override
    public void update(Object key, Object value, Integer expireInSec) throws CacheException {
        put(key, value, expireInSec);
    }

}

值得注意的是:在Redis实现过程中使用了hash存储,提升效率,以及在client 的keys 可能查询不出来。

获取策略


     /**
         * 获取缓存中的数据
         *
         * @param region : Cache Region name
         * @param key    : Cache key
         * @return cache object
         */
        public CacheObject get(String region, Object key) {
            CacheObject obj = new CacheObject();
            obj.setRegion(region);
            obj.setKey(key);
            if (region != null && key != null) {
                obj.setValue(CacheManager.get(LEVEL_1, region, key));
                if (obj.getValue() == null) {
                    obj.setValue(CacheManager.get(LEVEL_2, region, key));
                    if (obj.getValue() != null) {
                        obj.setLevel(LEVEL_2);
                        CacheManager.set(LEVEL_1, region, key, obj.getValue());
                    }
                } else
                    obj.setLevel(LEVEL_1);
            }
            return obj;
        }

首先看代码,写的比较简单,无非就是获取各种各样的信息从缓存中取,分别从一级、二级获取。就是有些策略是2级获取同时赛至1级,保证第二次获取获取直接内存获取。

设置策略

/**
     * 写入缓存
     *
     * @param region : Cache Region name
     * @param key    : Cache key
     * @param value  : Cache value
     */
    public void set(String region, Object key, Object value) {
        if (region != null && key != null) {
            if (value == null)
                evict(region, key);
            else {
                // 分几种情况
                // Object obj1 = CacheManager.get(LEVEL_1, region, key);
                // Object obj2 = CacheManager.get(LEVEL_2, region, key);
                // 1. L1 和 L2 都没有
                // 2. L1 有 L2 没有(这种情况不存在,除非是写 L2 的时候失败
                // 3. L1 没有,L2 有
                // 4. L1 和 L2 都有
                // _sendEvictCmd(region, key);// 清除原有的一级缓存的内容
                CacheManager.set(LEVEL_1, region, key, value);
                CacheManager.set(LEVEL_2, region, key, value);
                _sendEvictCmd(region, key);// 清除原有的一级缓存的内容
            }
        }
        // log.info("write data to cache region="+region+",key="+key+",value="+value);
    }

设置基本上也是通过设置1,2级接口数据信息;如:CacheManager.setXXX

_sendEvictCmd 暂时忽略,最后给大家解释解释

订阅被订阅

纠结了半天,为啥使用这个名字,最后想想还是 被动和主动的关系 ,不纠结了,直接说原理吧;
在应用启动时,大家可以看到有个 Thread ,不知道大家有没有看到;thread_subscribe

    private RedisCacheChannel(String name) throws CacheException {
        this.name = name;
        try {
            long ct = System.currentTimeMillis();
            CacheManager.initCacheProvider(this);
            redisCacheProxy = new RedisCacheProvider().getResource();

            //不深就在这,主要是处理Redis 订阅信息,注意实现类:RedisCacheChannel,细的自己看哦,主要是onMessage;

            thread_subscribe = new Thread(new Runnable() {
                @Override
                public void run() {
                    redisCacheProxy.subscribe(RedisCacheChannel.this, SafeEncoder.encode(channel));
                }
            });

            thread_subscribe.start();

            // 启动结束了

            log.info("Connected to channel:" + this.name + ", time " + (System.currentTimeMillis() - ct) + " ms.");

        } catch (Exception e) {
            throw new CacheException(e);
        }
    }

看到这就能看到一个信息就是通过订阅Redis的信息,然后处理Ehcache的数据;

回过头看看:_sendEvictCmd 这个玩意,其实就是发送订阅消息至Redis ,然后通过 Redis onmessage 获取信息将指令反序列化,处理EHCache信息。

HOHO ,差不多了,

后面有补充的Command等等慢慢解释,或者自己折腾折腾

忘记补充一点关键:

J2cache maven 上的 1.3.0 版本,有点BUG,在新的版本上修复了,但是没有发布,所以可以使用

https://git.oschina.net/ld/J2Cache.git

版本下下来自己编译一个版本;

更新POM.xml

        <dependency>
            <groupId>net.oschina.j2cache</groupId>
            <artifactId>j2cache-core</artifactId>
            <version>1.4.0</version>
        </dependency>
 

HOHO ,完毕~

除特别注明外,本站所有文章均为duzhi原创,转载请注明出处来自https://www.duzhi.me/article/5824.html

联系我们

******

在线咨询:点击这里给我发消息

邮件:ashang.peng#aliyun.com

QR code