李锋镝的博客

  • 首页
  • 时间轴
  • 留言
  • 插件
  • 左邻右舍
  • 关于我
    • 关于我
    • 另一个网站
  • 知识库
  • 赞助
Destiny
自是人生长恨水长东
  1. 首页
  2. 原创
  3. 技术
  4. 正文

OHCache使用

2022年9月22日 5902点热度 0人点赞 0条评论

OHCache介绍

缓存框架OHC基于Java语言实现,并以类库的形式供其他Java程序调用,是一种以单机模式运行的堆外缓存。

OHC简介

缓存的分类与实现机制多种多样,包括单机缓存与分布式缓存等等。具体到JVM应用,又可以分为堆内缓存和堆外缓存。

OHC 全称为 off-heap-cache,即堆外缓存,是一款基于Java 的 key-value 堆外缓存框架。

OHC是2015年针对 Apache Cassandra 开发的缓存框架,后来从 Cassandra 项目中独立出来,成为单独的类库,其项目地址为:https://github.com/snazy/ohc

堆内和堆外

  Java程序运行时,由Java虚拟机(JVM)管理的内存区域称为堆(heap)。垃圾收集器会扫描堆内空间,识别应用程序已经不再使用的对象,并释放其空间,这个过程称为GC。

  堆内缓存,顾名思义,是指将数据缓存在堆内的机制,比如 HashMap 就可以用作简单的堆内缓存。由于垃圾收集器需要扫描堆,并且在扫描时需要暂停应用线程(stop-the-world,STW),因此,缓存数据过多会导致GC开销增大,从而影响应用程序性能。

  与堆内空间不同,堆外空间不影响GC,由应用程序自身负责分配与释放内存。因此,当缓存数据量较大(达到G以上级别)时,可以使用堆外缓存来提升性能。
image

OHC 的特性

  相对于持久化数据库,可用的内存空间更少、速度也更快,因此通常将访问频繁的数据放入堆外内存进行缓存,并保证缓存的时效性。OHC主要具有以下特性来满足需求:

  • 数据存储在堆外,不影响GC
  • 支持为每个缓存项设置过期时间
  • 支持配置LRU、W-TinyLFU逐出策略
  • 能够维护大量的缓存条目(百万量级以上)
  • 支持异步加载缓存
  • 读写速度在微秒级别

OHC具有低延迟、容量大、不影响GC的特性,并且支持使用方根据自身业务需求进行灵活配置。

使用

在java项目中使用OHC,示例如下:

pom文件:

<dependency>
    <groupId>org.caffinitas.ohc</groupId>
    <artifactId>ohc-core</artifactId>
    <version>0.7.4</version>
</dependency>

<dependency>
    <groupId>com.esotericsoftware</groupId>
    <artifactId>kryo</artifactId>
    <version>5.3.0</version>
</dependency>

字符串序列化,这里序列化使用的是Kryo,是一款性能非常优秀的序列化工具:


import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.caffinitas.ohc.CacheSerializer;
import org.objenesis.strategy.StdInstantiatorStrategy;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.nio.ByteBuffer;

/**
 * @author lifengdi
 * @date 2022/9/19 17:58
 */
public class OhcStringSerializer implements CacheSerializer<String> {
    private final ThreadLocal<Kryo> kryoThreadLocal = ThreadLocal.withInitial(() -> {
        Kryo kryo = new Kryo();
        kryo.setReferences(false);
        // 设置是否注册全限定名,
        kryo.setRegistrationRequired(false);
        // 设置初始化策略,如果没有默认无参构造器,那么就需要设置此项,使用此策略构造一个无参构造器
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        return kryo;
    });

    @Override
    public void serialize(String value, ByteBuffer buf) {
        Kryo kryo = kryoThreadLocal.get();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.writeObjectOrNull(output, value, String.class);
        output.close();
        byte[] bytes = bos.toByteArray();
        buf.put(bytes);
    }

    @Override
    public String deserialize(ByteBuffer buf) {
        Kryo kryo = kryoThreadLocal.get();
        byte[] bytes = new byte[buf.remaining()];
        buf.get(bytes);
        ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        Input input = new Input(bis);
        input.close();
        return kryo.readObjectOrNull(input, String.class);
    }

    @Override
    public int serializedSize(String value) {
        Kryo kryo = kryoThreadLocal.get();
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Output output = new Output(bos);
        kryo.writeObject(output, value);
        output.close();
        return bos.size();
    }
}

工具类封装:


import org.caffinitas.ohc.*;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;

/**
 * @author lifengdi
 * @date 2022/9/19 18:07
 */
public class OffHeapCache {

    OHCache<String, String> ohCache;

    /**
     * 过期时间
     */
    private static final long EXPIRE_TIME = 24;

    /**
     * 时间单位
     */
    private static final TimeUnit TIME_UNIT = TimeUnit.HOURS;

    private OffHeapCache() {
        ohCache = OHCacheBuilder.<String, String>newBuilder()
            .keySerializer(new OhcStringSerializer())
            .valueSerializer(new OhcStringSerializer())
            .eviction(Eviction.LRU)
            .capacity(1024 * 1024 * 1024)
            .hashTableSize(16)
            .timeouts(true)
            .defaultTTLmillis(TIME_UNIT.toMillis(EXPIRE_TIME))
            .build();
    }

    private static class SingletonHolder {
        static final OffHeapCache instance = new OffHeapCache();
    }

    public static OffHeapCache getInstance() {
        return OffHeapCache.SingletonHolder.instance;
    }

    public void put(String key, String value) {
        ohCache.put(key, value);
    }

    public String get(String key) {
        return ohCache.get(key);
    }

    /**
     * 设置缓存
     * @param key key
     * @param value value
     * @param expireTime 过期时间(毫秒)
     */
    public void put(String key, String value, long expireTime) {
        long expireAt = System.currentTimeMillis() + expireTime;
        ohCache.put(key, value, expireAt);
    }

    /**
     * 统计
     * @return OHCacheStats
     */
    public OHCacheStats stats() {
        return ohCache.stats();
    }

    /**
     * 获取hotKey
     * @param n 数量
     * @return list
     */
    public List<String> hotKeyIterator(int n) {
        CloseableIterator<String> hotKeyIterator = ohCache.hotKeyIterator(n);
        List<String> hotKey = new ArrayList<>();
        while (hotKeyIterator.hasNext()) {
            hotKey.add(hotKeyIterator.next());
        }
        try {
            hotKeyIterator.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return hotKey;
    }

}

用法示例:

import org.caffinitas.ohc.Eviction;
import org.caffinitas.ohc.OHCache;
import org.caffinitas.ohc.OHCacheBuilder;

public class OffHeapCacheExample {

    public static void main(String[] args) {
        OffHeapCache ohCache = OffHeapCache.getInstance();

        ohCache.put("hello", "world");
        System.out.println(ohCache.get("hello")); // world
    }
}

OHC 的底层原理

整体架构

  OHC 以 API 的方式供其他 Java 程序调用,其 org.caffinitas.ohc.OHCache 接口定义了可调用的方法。对于缓存来说,最常用的是 get 和 put 方法。针对不同的使用场景,OHC提供了两种OHCache的实现:

org.caffinitas.ohc.chunked.OHCacheChunkedImpl

org.caffinitas.ohc.linked.OHCacheLinkedImpl

  以上两种实现均把所有条目缓存在堆外,堆内通过指向堆外的地址指针对缓存条目进行管理。

  其中,linked 实现为每个键值对分别分配堆外内存,适合中大型键值对。chunked 实现为每个段分配堆外内存,适用于存储小型键值对。由于 chunked 实现仍然处于实验阶段,所以我们选择 linked 实现在线上使用,后续介绍也以linked 实现为例,其整体架构及内存分布如下图所示,下文将分别介绍其功能。
image
  

OHCacheLinkedImpl

OHCacheLinkedImpl是堆外缓存的具体实现类,其主要成员包括:

段数组:OffHeapLinkedMap[]
序列化器与反序列化器:CacheSerializer

OHCacheLinkedImpl 中包含多个段,每个段用 OffHeapLinkedMap 来表示。同时,OHCacheLinkedImpl 将Java对象序列化成字节数组存储在堆外,在该过程中需要使用用户自定义的 CacheSerializer。OHCacheLinkedImpl 的主要工作流程如下:

  1. 计算 key 的 hash值,根据 hash值 计算段号,确定其所处的 OffHeapLinkedMap

  2. 从 OffHeapLinkedMap 中获取该键值对的堆外内存指针

  3. 对于 get 操作,从指针所指向的堆外内存读取 byte[],把 byte[] 反序列化成对象

  4. 对于 put 操作,把对象序列化成 byte[],并写入指针所指向的堆外内存

段的实现:OffHeapLinkedMap

在OHC中,每个段用 OffHeapLinkedMap 来表示,段中包含多个分桶,每个桶是一个链表,链表中的元素即是缓存条目的堆外地址指针。OffHeapLinkedMap 的主要作用是根据 hash值 找到 键值对 的 堆外地址指针。在查找指针时,OffHeapLinkedMap 先根据 hash值 计算出 桶号,然后找到该桶的第一个元素,然后沿着第一个元素按顺序线性查找。

空间分配

  OHC 的 linked 实现为每个键值对分别分配堆外内存,因此键值对实际是零散地分布在堆外。

  OHC提供了JNANativeAllocator 和 UnsafeAllocator 这两个分配器,分别使用 Native.malloc(size) 和 Unsafe.allocateMemory(size) 分配堆外内存,用户可以通过配置来使用其中一种。

  OHC 会把 key 和 value 序列化成 byte[] 存储到堆外,用户需要通过实现 CacheSerializer 来自定义类完成 序列化 和 反序列化。因此,占用的空间实际取决于用户自定义的序列化方法。

  除了 key 和 value 本身占用的空间,OHC 还会对 key 进行 8位 对齐。比如用户计算出 key 占用 3个字节,OHC会将其对齐到8个字节。另外,对于每个键值对,OHC需要额外的64个字节来维护偏移量等元数据。因此,对于每个键值对占用的堆外空间为:

每个条目占用堆外内存 = key占用内存(8位对齐) + value占用内存 + 64字节
除非注明,否则均为李锋镝的博客原创文章,转载必须以链接形式标明本文链接

本文链接:https://www.lifengdi.com/archives/article/tech/3992

相关文章

  • TransmittableThreadLocal介绍与使用
  • ReentrantLock深度解析
  • SpringBoot常用注解
  • CompletableFuture使用详解
  • 金融级JVM深度调优实战的经验和技巧
本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可
标签: JAVA OHCache 缓存
最后更新:2022年9月22日

李锋镝

既然选择了远方,便只顾风雨兼程。

打赏 点赞
< 上一篇
下一篇 >

文章评论

1 2 3 4 5 6 7 8 9 11 12 13 14 15 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 46 47 48 49 50 51 52 53 54 55 57 58 60 61 62 63 64 65 66 67 69 72 74 76 77 78 79 80 81 82 85 86 87 90 92 93 94 95 96 97 98 99
取消回复

了却君王天下事,赢得生前身后名。

最新 热点 随机
最新 热点 随机
SpringBoot框架自动配置之spring.factories和AutoConfiguration.imports 应用型负载均衡(ALB)和网络型负载均衡(NLB)区别 什么是Helm? TransmittableThreadLocal介绍与使用 ReentrantLock深度解析 RedisTemplate和Redisson的区别
玩博客的人是不是越来越少了?准备入手个亚太的ECS,友友们有什么建议吗?什么是Helm?2024年11月1号 农历十月初一别再背线程池的七大参数了,现在面试官都这么问URL地址末尾加不加“/”有什么区别
ConcurrentHashMap常用方法源码解析(jdk1.8) 出院了~~~ 博客有logo啦 分布式锁-Java常用技术方案 JAVA简单快速的读写Excel之EasyExcel SpringBoot整合MongoDB
标签聚合
面试 docker JVM Redis K8s SQL IDEA ElasticSearch JAVA 多线程 分布式 架构 SpringBoot 日常 设计模式 数据库 MySQL 教程 文学 Spring
友情链接
  • i架构
  • 临窗旋墨
  • 博友圈
  • 博客录
  • 博客星球
  • 哥斯拉
  • 志文工作室
  • 搬砖日记
  • 旋律的博客
  • 旧时繁华
  • 林羽凡
  • 知向前端
  • 蜗牛工作室
  • 集博栈
  • 韩小韩博客
  • 風の声音

COPYRIGHT © 2025 lifengdi.com. ALL RIGHTS RESERVED.

Theme Kratos Made By Dylan

津ICP备2024022503号-3