缓存

缓存

一般而言, 现在互联网应用(网站或App)的整体流程, 可以概括下图所示, 用户请求从界面(浏览器或App界面)到网络转发、应用服务器再到存储)数据库或文件系统, 然后返回到界面呈现内容.

随着互联网的普及, 内容信息越来越复杂, 用户数和访问量越来越大, 应用需要支持更多的并发量, 同时我们应用服务器和数据库服务器所做的计算也越来越多. 但是往往我们的应用服务器资源是有限的, 且技术变革是缓慢的, 数据库每秒能接受的请求次数也是有限的(或者文件的读写也是有限的), 如何能够有效利用有限的资源来提供尽可能大的吞吐量?一个有效的办法就是引入缓存, 打破标准流程, 每个环节中请求可以从缓存中直接获取目标数据并返回, 从而减少计算量, 有效提升响应速度, 让有限的资源服务更多的用户.

下图所示, 缓存的使用可以出现1~4的各个环节中, 每个环节的缓存方案与使用各有特点.
互联网应用

缓存特征

缓存也是一个数据模型对象, 那么必然有它的一些特征:

命中率

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

最大元素(最大空间)

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

清空策略

如上所述, 缓存的存储空间有限制, 当缓存空间被用满时, 如何保证在稳定服务的同时有效提升命中率?这就由缓存清空策略来处理, 设计适合自身数据特征的清空策略能有效提升命中率. 常见的一般策略有:

  • FIFO(first in first out)
    先进先出策略, 最先进入缓存的数据在缓存空间不够的情况下(超出最大元素限制)会被优先清除掉, 以腾出新的空间接受新的数据. 策略算法主要比较缓存元素的创建时间. 在数据时效性要求场景下可选择该策略, 优先保障最新数据可用.
  • LFU(less frequently used)
    最少使用策略, 无论是否过期, 根据元素最后一次被使用的时间戳, 清除最远使用时间戳的元素释放空间. 策略算法主要比较元素最近一次被get使用时间. 在热点数据场景下较使用, 优先保证热点数据的时效性.
    除此之外, 还有一些简单策略比如:
  • 根据过期时间判断, 清理过期时间最长的元素;
  • 根据过期时间判断, 清理最近要过期的元素;
  • 随机清理
  • 根据关键字(或元素内容)长短清理等;

缓存介质

虽然从硬件介质上来看, 无非就是内存和硬盘两种, 但从技术上, 可以分成内存、硬盘文件、数据库.

  • 内存: 将缓存存储于内存中是最快的选择, 无需额外的I/O开销, 但内存的缺点是没有持久化落地物理硬盘, 一旦应用异常break down而重新启动, 数据很难或者无法复原.
  • 硬盘: 一般来说, 很多缓存框架会结合使用内存和硬盘, 在内存分配空间满了或是在异常的情况下, 可以被动或主动的将内存空间数据持久化到硬盘中, 达到释放空间或备份数据的目的.
  • 数据库: 前面有提到, 增加缓存的策略的目的之一就是为了减少数据库的I/O压力. 现在使用数据库做缓存介质是不是又回到了老问题上?其实, 数据库也有很多种类型, 像那些不支持SQL, 只是简单的key-value存储结构的特殊数据库(比如BerkeleyDB和Redis), 响应速度和吞吐量都远远高于我们常用的关系型数据库等.

缓存分类和应用场景

缓存有各种特征, 而且有不同介质的区别, 那么实际工程中我们怎么去对缓存分类呢?在目前的应用服务框架中, 比较常见的, 是根据缓存与应用的耦合度, 分为local cache(本地缓存)和remote cache(分布式缓存):

  • 本地缓存: 指的是在应用中的缓存组件, 其最大的优点是应用和cache是在同一个进程内部, 请求缓存非常快速, 没有过多的网络开销等, 在单应用不需要集群支持或集群情况下各节点无需互相通知的场景下使用本地缓存较合适; 同时, 它的缺点也是因为缓存和应用程序耦合, 多个应用程序无法直接的共享缓存, 各应用或集群的各节点都需要维护自己的单独缓存, 对内存是一种浪费.
  • 分布式缓存: 指的是与应用分离的缓存组件或服务, 其最大的优点是自身就是一个独立的应用, 与本地应用隔离, 多个应用可直接的共享缓存.
    目前各种类型的缓存都活跃在成千上万的应用服务中, 还没有一种缓存方案可以解决一切的业务场景或数据类型, 我们需要根据自身的特殊场景和背景, 选择最适合的缓存方案. 缓存的使用是程序员、架构师的必备技能, 好的程序员能根据数据类型、业务场景来准确判断使用何种类型的缓存, 如何使用这种缓存, 以最小的成本最快的效率达到最优的目的.
本地缓存

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

成员变量或局部变量实现
代码如下:

	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{// 缓存未命中 IO获取数据, 结果存入缓存
			Object valueObject = this.getInfoFromDB();
			localCacheStoreMap.put(valueObject.toString(),valueObject);
		}
	}
	}
//实例
private List<Object> getInfoList(){
	return new ArrayList<Object>();
}
//示例数据库IO获取
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.generateAuthDateHeader(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的统一管理, 做到自动动态更新缓存, 如图所示
在这里插入图片描述
这类缓存实现, 优点是能直接在head区内读写, 最快也最方便; 缺点同样是受head区域影响, 缓存的数据量非常有限, 同时缓存时间受GC影响. 主要满足单机场景下的小数据量缓存需求, 同时对缓存数据的变更无需太敏感感知, 如上一般配置管理、基础静态数据等场景.

Ehcache

Ehcache是现在最流行的纯Java开源缓存框架, 配置简单、结构清晰、功能强大, 是一个非常轻量级的缓存实现, 我们常用的Hibernate里面就集成了相关缓存功能.
在这里插入图片描述
参考上图, Ehcache的核心定义主要包括:

  • cache manager: 缓存管理器, 以前是只允许单例的, 现在也支持多实例了.
  • cache: 缓存管理器内可以放置若干cache, 存放数据的实质, 所有cache都实现了Ehcache接口, 这是一个真正使用的缓存实例; 通过缓存管理器的模式, 可以在单个应用中轻松隔离多个缓存实例, 独立服务于不同业务场景需求, 缓存数据物理隔离, 同时需要时又可共享使用.
  • element: 单条缓存数据的组成单位.
  • system of record(SOR): 可以取到真实数据的组件, 可以是真正的业务逻辑、外部接口调用、存放真实数据的数据库等, 缓存就是从SOR中读取或者写入到SOR中去的.

在上层可以看到, 整个Ehcache提供了对JSR、JMX等的标准支持, 能够较好的兼容和移植, 同时对各类对象又较完整的监控管理机制. 它的缓存介质涵盖堆内存(head)、堆外内存(BigMemory商用版本支持)和磁盘, 各介质可独立设置属性和策略. Ehcache最初是独立的本地缓存框架组件, 在后期的发展中, 结合Terracotta服务阵列模型, 可以支持分布式缓存集群, 主要有RMI、JGroups、JMS和Cache Server等传播方式进行节点间通信, 参考上图左侧部分描述.

整体数据流转包括这样几类行为:

  • Flush: 缓存条目向低层次移动.
  • Fault: 从低层拷贝一个对象到高层. 在获取缓存的过程中, 某一层发现自己的该缓存条目已经失效, 就触发了Fault行为.
  • Eviction: 把缓存条目除去.
  • Expiration: 失效状态.
  • Pinning: 强制缓存条目保持在某一层.

下图反映了数据在各层之间的流转, 同时也体现了各层数据的一个生命周期.
在这里插入图片描述
Ehcache的配置使用如下:

<ehcache>
<!-- 指定一个文件目录,当Ehcache把数据写到硬盘上时,将把数据写到这个文件目录下 -->
<diskStore path="java.io.tmpdir"/>

<!-- 设定缓存的默认数据过期策略 -->
<defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        overflowToDisk="true"
        timeToIdleSeconds="0"
        timeToLiveSeconds="0"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"/>

<!--  
    设定具体的命名缓存的数据过期策略

    cache元素的属性:
        name:缓存名称

        maxElementsInMemory:内存中最大缓存对象数

        maxElementsOnDisk:硬盘中最大缓存对象数,若是0表示无穷大

        eternal:true表示对象永不过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性,默认为false

        overflowToDisk:true表示当内存缓存的对象数目达到了maxElementsInMemory界限后,会把溢出的对象写到硬盘缓存中。注意:如果缓存的对象要写入到硬盘中的话,则该对象必须实现了Serializable接口才行。

        diskSpoolBufferSizeMB:磁盘缓存区大小,默认为30MB。每个Cache都应该有自己的一个缓存区。

        diskPersistent:是否缓存虚拟机重启期数据

        diskExpiryThreadIntervalSeconds:磁盘失效线程运行时间间隔,默认为120秒

        timeToIdleSeconds: 设定允许对象处于空闲状态的最长时间,以秒为单位。当对象自从最近一次被访问后,如果处于空闲状态的时间超过了timeToIdleSeconds属性值,这个对象就会过期,EHCache将把它从缓存中清空。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地处于空闲状态

        timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位。当对象自从被存放到缓存中后,如果处于缓存中的时间超过了 timeToLiveSeconds属性值,这个对象就会过期,Ehcache将把它从缓存中清除。只有当eternal属性为false,该属性才有效。如果该属性值为0,则表示对象可以无限期地存在于缓存中。timeToLiveSeconds必须大于timeToIdleSeconds属性,才有意义

        memoryStoreEvictionPolicy:当达到maxElementsInMemory限制时,Ehcache将会根据指定的策略去清理内存。可选策略有:LRU(最近最少使用,默认策略)、FIFO(先进先出)、LFU(最少访问次数)。
-->
<cache name="CACHE1"
       maxElementsInMemory="1000"
       eternal="true"
       overflowToDisk="true"/>  

<cache name="CACHE2"
    maxElementsInMemory="1000"
    eternal="false"
    timeToIdleSeconds="200"
    timeToLiveSeconds="4000"
    overflowToDisk="true"/>
</ehcache>

整体上看, Ehcache的使用还是相对简单便捷的, 提供了完整的各类API的接口. 需要注意的是, 虽然Ehcache支持磁盘的持久化, 但是由于存在两级缓存介质, 在一级内存中的缓存, 如果没有主动的刷入磁盘持久化的话, 在应用异常down机等情形下, 依然会出现缓存数据丢失, 为此可以根据需要将缓存刷到磁盘, 将缓存条目刷到磁盘的操作可以通过cache.flush()方法来执行, 需要注意的是, 对于对象的磁盘写入, 前提是要将对象进行序列化.

主要特性:

  • 快速,针对大型高并发系统场景,Ehcache的多线程机制有相应的优化改善.
  • 简单,很小的jar包,简单配置就可直接使用,单机场景下无需过多的其他服务依赖.
  • 支持多种的缓存策略,灵活.
  • 缓存数据有两级:内存和磁盘,与一般的本地内存缓存相比,有了磁盘的存储空间,将可以支持更大量的数据缓存需求.
  • 具有缓存和缓存管理器的侦听接口,能更简单方便的进行缓存实例的监控管理.
  • 支持多缓存管理器实例,以及一个实例的多个缓存区域.

注意: Ehcache的超时设置主要针对整个cache实例设置整体的超市策略, 而没有较好的处理针对单独的key的个性的超时设置(有策略设置, 但是比较复杂), 因此, 在使用中药注意过期失效的缓存元素无法被GC回收, 时间越长缓存越多, 内存占用也就越大, 内存泄漏的概率也大.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Mybatis-Plus是一个Mybatis框架的增强插件,它提供了简单的配置和快速进行CRUD操作的功能。在Mybatis-Plus中,缓存级别分为一级缓存和二级缓存。 一级缓存是Mybatis框架默认开启的,它是指在同一个SqlSession中,如果执行相同的查询语句,那么Mybatis会将查询结果缓存起来,下次执行相同的查询语句时,直接从缓存获取结果,而不会再次去数据库查询。一级缓存的作用范围是在同一个SqlSession中,当SqlSession被关闭后,缓存也会被清空。 二级缓存是全局缓存,也称为mapper级别的缓存,它可以在多个SqlSession之间共享缓存结果。二级缓存的默认机制是采用PerpetualCache和HashMap存储缓存结果,所以默认是本地缓存。不同之处在于二级缓存的存储作用域是Mapper(Namespace),可以供多个SqlSession共享。可以通过配置来启用二级缓存,并且可以自定义存储源,如Ehcache、Redis等。 总结来说,Mybatis-Plus提供了一级缓存和二级缓存两种缓存级别。一级缓存在同一个SqlSession中有效,而二级缓存在多个SqlSession之间共享。开发人员可以根据具体的需求选择是否启用缓存,并可以自定义缓存的存储源。 : MyBatis包含一个非常强大的查询缓存特性,它可以非常方便地定制和配置缓存缓存可以极大地提升查询效率。 : Mybatis-Plus是一个Mybatis框架的增强插件,根据官方描述,MP只做增强不做改变。它提供了简单的配置和快速进行CRUD操作的功能,包括代码生成、分页、性能分析等。 : 二级缓存也称为全局缓存,是mapper级别的缓存。它可以在多个SqlSession之间共享缓存结果,存储作用域为Mapper(Namespace)。默认使用PerpetualCache和HashMap存储,可以自定义存储源。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值