手记

本地/分布式缓存概论

现在互联网应用(网站/App)的整体流程,可概括如图


图1 互联网应用一般流程


用户请求从界面(浏览器/App)到网络转发、应用服务再到存储(数据库或文件系统),然后返回到界面呈现内容。

如图1所示,缓存的使用可以出现在1~4的各个环节中,每个环节的缓存方案与使用各有特点。

1 缓存特征

缓存是一个数据模型对象,有它的一些特征

1.1 命中率

命中率=返回正确结果数/请求缓存次数
命中率问题是缓存中的一个非常重要的问题,它是衡量缓存有效性的重要指标。命中率越高,表明缓存的使用率越高。

1.2 最大元素(或最大空间)

缓存中可以存放的最大元素的数量,一旦缓存中元素数量超过这个值(或者缓存数据所占空间超过其最大支持空间)
那么将会触发缓存启动清空策略,根据不同的场景合理的设置最大元素值往往可以一定程度上提高缓存的命中率,从而更有效的使用缓存

1.3 清空策略

缓存的存储空间有限制,当缓存空间被用满时,如何保证在稳定服务的同时有效提升命中率?
这就由缓存清空策略来处理
常见的一般策略有:

  • FIFO(first in first out)
    最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先被清除掉,以腾出新的空间接受新的数据
    策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用

  • LFU(less frequently used)
    无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素释放空间
    策略算法主要比较元素的hitCount(命中次数)。在保证高频数据有效性场景下,可选择这类策略

  • LRU(least recently used)
    无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素释放空间
    策略算法主要比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性

除此之外,还有一些简单策略比如:

  • 根据过期时间判断,清理过期时间最长的元素

  • 根据过期时间判断,清理最近要过期的元素

  • 随机清理

  • 根据关键字(或元素内容)长短清理等

2 缓存介质

从硬件介质上来看,内存和硬盘
从技术上,可以分成内存、硬盘文件、数据库

  • 内存:将缓存存储于内存中是最快的选择,无需额外的I/O开销,但是内存的缺点是没有持久化落地物理磁盘,一旦应用异常break down而重新启动,数据很难或者无法复原

  • 硬盘:一般来说,很多缓存框架会结合使用内存和硬盘,在内存分配空间满了或是在异常的情况下,可以被动或主动的将内存空间数据持久化到硬盘中,达到释放空间或备份数据的目的。

  • 数据库:前面有提到,增加缓存的策略的目的之一就是为了减少数据库的I/O压力。现在使用数据库做缓存介质是不是又回到了老问题上了?其实,数据库也有很多种类型,像那些不支持SQL,只是简单的key-value存储结构的特殊数据库(如BerkeleyDB和Redis),响应速度和吞吐量都远远高于我们常用的关系型数据库等。

3 缓存分类和应用场景

根据缓存与应用的藕合度,分为local cache(本地缓存)和remote cache(分布式缓存)

  • 本地缓存:指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。

  • 分布式缓存:指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存

3.1 本地缓存

3.1.1  编程直接实现缓存

个别场景下,我们只需要简单的缓存数据的功能,而无需关注更多存取、清空策略等深入的特性时,直接编程实现缓存则是最便捷和高效的

  • 成员变量或局部变量实现

    public void UseLocalCache(){     //一个本地的缓存变量
     Map<String, Object> localCacheStoreMap = new HashMap<String, Object>();

    List<Object> infosList = this.getInfoList();    for(Object item:infosList){        if(localCacheStoreMap.containsKey(item)){ //缓存命中 使用缓存数据
            // todo
        } else { // 缓存未命中  I/O获取数据,结果存入缓存
            Object valueObject = this.getInfoFromDB();
            localCacheStoreMap.put(valueObject.toString(), valueObject);

        }
    }
}//示例private List<Object> getInfoList(){    return new ArrayList<Object>();
}//示例数据库I/O获取private Object getInfoFromDB(){    return new Object();
}

以局部变量map结构缓存部分业务数据,减少频繁的重复数据库I/O操作。缺点仅限于类的自身作用域内,类间无法共享缓存。

  • 静态变量实现
    最常用的单例实现静态资源缓存

   public class CityUtils {    private static final HttpClient httpClient = ServerHolder.createClientWithPool();    private static Map<Integer, String> cityIdNameMap = new HashMap<Integer, String>();    private static Map<Integer, String> districtIdNameMap = new HashMap<Integer, String>();    static {
        HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/city/all");
        BaseAuthorizationUtils.generateAuthAndDateHeader(get,
                BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
                BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);        try {
            String resultStr = httpClient.execute(get, new BasicResponseHandler());
            JSONObject resultJo = new JSONObject(resultStr);
            JSONArray dataJa = resultJo.getJSONArray("data");            for (int i = 0; i < dataJa.length(); i++) {
                JSONObject itemJo = dataJa.getJSONObject(i);
                cityIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
            }
        } catch (Exception e) {            throw new RuntimeException("Init City List Error!", e);
        }
    }    
    static {
        HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/district/all");
        BaseAuthorizationUtils.generateAuthAndDateHeader(get,
                BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
                BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);        try {
            String resultStr = httpClient.execute(get, new BasicResponseHandler());
            JSONObject resultJo = new JSONObject(resultStr);
            JSONArray dataJa = resultJo.getJSONArray("data");            for (int i = 0; i < dataJa.length(); i++) {
                JSONObject itemJo = dataJa.getJSONObject(i);
                districtIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
            }
        } catch (Exception e) {            throw new RuntimeException("Init District List Error!", e);
        }
    }    public static String getCityName(int cityId) {
        String name = cityIdNameMap.get(cityId);        if (name == null) {
            name = "未知";
        }        return name;
    }    public static String getDistrictName(int districtId) {
        String name = districtIdNameMap.get(districtId);        if (name == null) {
            name = "未知";
        }        return name;
    }
}

O2O业务中常用的城市基础基本信息判断,通过静态变量一次获取缓存内存中,减少频繁的I/O读取
静态变量实现类间可共享,进程内可共享,缓存的实时性稍差

为了解决本地缓存数据的实时性问题,目前大量使用的是结合ZooKeeper的自动发现机制,实时变更本地静态变量缓存:

美团的基础配置组件MtConfig,采用的就是类似原理,使用静态变量缓存,结合ZooKeeper的统一管理,做到自动动态更新缓存




作者:JavaEdge
链接:https://www.jianshu.com/p/eb94467a530a


0人推荐
随时随地看视频
慕课网APP