1、背景
在当前微服务体系架构中,有很多服务例如,在 特征组装 与 排序等场景都需要有大量的数据支撑,快速读取这些数据对提升整个服务于的性能起着至关重要的作用。
缓存在各大系统中应用非常广泛。尤其是业务程序所依赖的数据可能在各种类型的数据库上(mysql、hive 等),那么如果想要获取到这些数据需要通过网络来访问。再加上往往数据量又很庞大,网络传输的耗时,自然会增加系统的相应时间。为了降低相应时间,业务程序可以将从数据库中读取到的部分数据,缓存在本地服务器可以很方便快速的直接使用。
缓存框架OHC基于Java语言实现,并以类库的形式供其他Java程序调用,是一种以单机模式运行的堆外缓存。
2、OHC 简介
缓存的分类与实现机制多种多样,包括单机缓存与分布式缓存等等。具体到JVM应用,又可以分为堆内缓存和堆外缓存。
OHC 全称为 off-heap-cache,即堆外缓存,是一款基于Java 的 key-value 堆外缓存框架。
OHC是2015年针对 Apache Cassandra 开发的缓存框架,后来从 Cassandra 项目中独立出来,成为单独的类库,其项目地址为:https://github.com/snazy/ohc
2.1、堆内和堆外
Java程序运行时,由Java虚拟机(JVM)管理的内存区域称为堆(heap)。垃圾收集器会扫描堆内空间,识别应用程序已经不再使用的对象,并释放其空间,这个过程称为GC。
堆内缓存,顾名思义,是指将数据缓存在堆内的机制,比如 HashMap 就可以用作简单的堆内缓存。由于垃圾收集器需要扫描堆,并且在扫描时需要暂停应用线程(stop-the-world,STW),因此,缓存数据过多会导致GC开销增大,从而影响应用程序性能。
与堆内空间不同,堆外空间不影响GC,由应用程序自身负责分配与释放内存。因此,当缓存数据量较大(达到G以上级别)时,可以使用堆外缓存来提升性能。
2.2、OHC 的特性
相对于持久化数据库,可用的内存空间更少、速度也更快,因此通常将访问频繁的数据放入堆外内存进行缓存,并保证缓存的时效性。OHC主要具有以下特性来满足需求:
- 数据存储在堆外,不影响GC
- 支持为每个缓存项设置过期时间
- 支持配置LRU、W-TinyLFU逐出策略
- 能够维护大量的缓存条目(百万量级以上)
- 支持异步加载缓存
- 读写速度在微秒级别
OHC具有低延迟、容量大、不影响GC的特性,并且支持使用方根据自身业务需求进行灵活配置。
2.3、使用示例
在Java项目中使用OHC,主要包括以下步骤:
1. 在项目中引入OHC。如果使用Maven来管理依赖,可以将OHC的坐标添加到到项目的POM文件中。
<dependency>
<groupId>org.caffinitas.ohc</groupId>
<artifactId>ohc-core</artifactId>
<version>0.7.0</version>
</dependency>
2. OHC是将Java对象序列化后存储在堆外,因此用户需要实现 org.caffinitas.ohc.CacheSerializer 类,OHC会运用其实现类来序列化和反序列化对象。例如,以下例子是对 string 进行的序列化实现:
public class StringSerializer implements CacheSerializer<String> { /** * 计算字符串序列化后占用的空间 * * @param value 需要序列化存储的字符串 * @return 序列化后的字节数 */ @Override public int serializedSize(String value) { byte[] bytes = value.getBytes(Charsets.UTF_8); // 设置字符串长度限制,2^16 = 65536 if (bytes.length > 65536) throw new RuntimeException("encoded string too long: " + bytes.length + " bytes"); // 设置字符串长度限制,2^16 = 65536 return bytes.length + 2; } /** * 将字符串对象序列化到 ByteBuffer 中,ByteBuffer是OHC管理的堆外内存区域的映射。 * * @param value 需要序列化的对象 * @param buf 序列化后的存储空间 */ @Override public void serialize(String value, ByteBuffer buf) { // 得到字符串对象UTF-8编码的字节数组 byte[] bytes = value.getBytes(Charsets.UTF_8); // 用前16位记录数组长度 buf.put((byte) ((bytes.length >>> 8) & 0xFF)); buf.put((byte) ((bytes.length) & 0xFF)); buf.put(bytes); } /** * 对堆外缓存的字符串进行反序列化 * * @param buf 字节数组所在的 ByteBuffer * @return 字符串对象. */ @Override public String deserialize(ByteBuffer buf) { // 判断字节数组的长度 int length = (((buf.get() & 0xff) << 8) + ((buf.get() & 0xff))); byte[] bytes = new byte[length]; // 读取字节数组 buf.get(bytes); // 返回字符串对象 return new String(bytes, Charsets.UTF_8); } }