Ehcache的多级缓存机制

1. 简介

Ehcache是一种广泛使用的开源Java分布式缓存。主要面向通用缓存、Java EE和轻量级容器。它具有堆内内存、堆外内存、磁盘存储,缓存加载器,缓存扩展,缓存异常处理程序等特点。

本文将介绍堆内内存、堆外内存、磁盘存储多种存储介质组合的缓存模式及原理分析。

2. 分级缓存设计

作为本地缓存框架, Ehcache支持多层缓存模式,常用的有三种数据存储介质:

  • 堆内
    直接在JVM堆中存储JAVA对象,优点是速度快;缺点是会增加GC的频次和GC时间。
  • 堆外
    在堆外内存中存储序列化的JAVA对象,优点是不会增加GC;缺点是存取速度较慢,需要额外的时间处理序列化和反序列化。
  • 磁盘
    缓存数据到磁盘,优点是掉电不会丢失数据,可用空间更大;缺点是存取速度比堆外慢很多。

对于这三种缓存存储介质,Ehcache支持三种组合模式:

  • 堆内+堆外
  • 堆内+磁盘
  • 堆内+堆外+磁盘

典型的堆内+堆外+磁盘的结构图如下:
图片.png

2.1 时序图

在Ehcache的多层缓存结构中,最底层被称为Authoritative Tier,其余的缓存层被称为Caching Tier。Authoritative Tier层数据是最全的,其余层的数据都是该层的数据子集,只是临时存储数据。

比如,堆内+堆外模式中,堆外为Authoritative Tier。堆内+堆外+磁盘模式中,磁盘为Authoritative Tier。

2.1.1 put

图片.png

2.1.2 get

图片.png

3. 代码分析

3.1 Cache的使用

        CacheManagerConfiguration<PersistentCacheManager> persistentManagerConfig = CacheManagerBuilder
            .persistence(new File("/tmp", "ehcache-junit"));

        PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder()
            .with(persistentManagerConfig).build();
        persistentCacheManager.init();

        ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder()
            .heap(4, MemoryUnit.MB)
            .offheap(16, MemoryUnit.MB)
            .disk(256, MemoryUnit.MB, true);

        CacheConfiguration<Long, String> config = CacheConfigurationBuilder
            .newCacheConfigurationBuilder(Long.class, String.class, resource).build();
        Cache<Long, String> cache = persistentCacheManager.createCache("test",
            CacheConfigurationBuilder.newCacheConfigurationBuilder(config));

        cache.put(100L, "abc");
        System.out.println(cache.get(100L));
        System.out.println(cache.get(101L));

空间大小必须heap小于offhead小于disk,否则抛出IllegalArgumentException
java.lang.IllegalArgumentException: Tiering Inversion: ‘Pool {4 MB heap}’ is not smaller than ‘Pool {4 MB disk(persistent)}’

程序运行后在/tmp/ehcache-junit/file目录下创建了test_a94a8fe5ccb19ba61c4c0873d391e987982fbbd3/offheap-disk-store目录,该目录下存在两个文件ehcache-disk-store.meta和ehcache-disk-store.data,记录缓存的元数据和缓存数据。

ehcache-disk-store.meta内容如下,明文记录了key和value的类型信息

#Key and value types
#Wed Jul 10 12:52:06 CST 2019
keyType=java.lang.Long
valueType=java.lang.String

ehcache-disk-store.data则记录了缓存的序列化数据。

3.2 序列化

堆外和磁盘存储时,必须先将对象序列化为java.nio.ByteBuffer,Ehcache允许用户按下面的代码传入自定义的序列化类。

CacheManagerBuilder.newCacheManagerBuilder().withSerializer(Employee.class,
  EmployeeSerializer.class).withSerializer(Person.class, PersonSerializer.class)

Ehcache自带的序列化器支持如下的类型:

  • java.io.Serializable
  • java.lang.Long
  • java.lang.Integer
  • java.lang.Float
  • java.lang.Double
  • java.lang.Character
  • java.lang.String
  • byte[]

LongSerializer逻辑如下:

  @Override
  public ByteBuffer serialize(Long object) {
    ByteBuffer byteBuffer = ByteBuffer.allocate(8);
    byteBuffer.putLong(object).flip();
    return byteBuffer;
  }

  @Override
  public Long read(ByteBuffer binary) throws ClassNotFoundException {
    return binary.getLong();
  }

StringSerializer逻辑如下:

public ByteBuffer serialize(String object) {
    int length = object.length();

    try(ByteArrayOutputStream bout = new ByteArrayOutputStream(length)) {
      int i = 0;

      for (; i < length; i++) {
        char c = object.charAt(i);
        if (c == 0x0000 || c > 0x007f) {
          break;
        }
        bout.write(c);
      }

      for (; i < length; i++) {
        char c = object.charAt(i);
        if (c == 0x0000) {
          bout.write(0xc0);
          bout.write(0x80);
        } else if (c < 0x0080) {
          bout.write(c);
        } else if (c < 0x800) {
          bout.write(0xc0 | ((c >>> 6) & 0x1f));
          bout.write(0x80 | (c & 0x3f));
        } else {
          bout.write(0xe0 | ((c >>> 12) & 0x1f));
          bout.write(0x80 | ((c >>> 6) & 0x3f));
          bout.write(0x80 | (c & 0x3f));
        }
      }

      return ByteBuffer.wrap(bout.toByteArray());

    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public String read(ByteBuffer binary) throws ClassNotFoundException {
    StringBuilder sb = new StringBuilder(binary.remaining());
    int i = binary.position();
    int end = binary.limit();
    for (; i < end; i++) {
      byte a = binary.get(i);
      if (((a & 0x80) != 0)) break;
      sb.append((char) a);
    }

    for (; i < end; i++) {
      byte a = binary.get(i);
      if ((a & 0x80) == 0) {
        sb.append((char) a);
      } else if ((a & 0xe0) == 0xc0) {
        sb.append((char) (((a & 0x1f) << 6) | ((binary.get(++i) & 0x3f))));
      } else if ((a & 0xf0) == 0xe0) {
        sb.append((char) (((a & 0x0f) << 12) | ((binary.get(++i) & 0x3f) << 6) | (binary.get(++i) & 0x3f)));
      } else {
        //these remaining stanzas are for compatibility with the previous regular UTF-8 codec
        int codepoint;
        if ((a & 0xf8) == 0xf0) {
          codepoint = ((a & 0x7) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
        } else if ((a & 0xfc) == 0xf8) {
          codepoint = ((a & 0x3) << 24) | ((binary.get(++i) & 0x3f) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
        } else if ((a & 0xfe) == 0xfc) {
          codepoint = ((a & 0x1) << 30) | ((binary.get(++i) & 0x3f) << 24) | ((binary.get(++i) & 0x3f) << 18) | ((binary.get(++i) & 0x3f) << 12) | ((binary.get(++i) & 0x3f) << 6) | ((binary.get(++i) & 0x3f));
        } else {
          throw new SerializerException("Unexpected encoding");
        }
        sb.appendCodePoint(codepoint);
      }
    }

    return sb.toString();
  }

3.3 offheap的get和put

3.3.1 get

堆外缓存的读写逻辑主要在org.ehcache.impl.internal.store.offheap.AbstractOffHeapStore中

  private Store.ValueHolder<V> internalGet(K key, final boolean updateAccess, final boolean touchValue) throws StoreAccessException {

    final StoreEventSink<K, V> eventSink = eventDispatcher.eventSink();
    final AtomicReference<OffHeapValueHolder<V>> heldValue = new AtomicReference<>();
    try {
      OffHeapValueHolder<V> result = backingMap().computeIfPresent(key, (mappedKey, mappedValue) -> {
        long now = timeSource.getTimeMillis();

        if (mappedValue.isExpired(now)) {
          onExpiration(mappedKey, mappedValue, eventSink);
          return null;
        }

        // 更新访问时间
        if (updateAccess) {
          mappedValue.forceDeserialization();
          OffHeapValueHolder<V> valueHolder = setAccessTimeAndExpiryThenReturnMapping(mappedKey, mappedValue, now, eventSink);
          if (valueHolder == null) {
            heldValue.set(mappedValue);
          }
          return valueHolder;
        } else if (touchValue) {
          mappedValue.forceDeserialization();
        }
        return mappedValue;
      });
      if (result == null && heldValue.get() != null) {
        result = heldValue.get();
      }
      eventDispatcher.releaseEventSink(eventSink);
      return result;
    } catch (RuntimeException re) {
      eventDispatcher.releaseEventSinkAfterFailure(eventSink, re);
      throw handleException(re);
    }
  }

4. 总结

本地缓存是微服务开发中经常使用的功能,开发者通常使用原生的ConcurrentHashmap、Guava、Ehcache等,经常上面的源码分析,我们可以理解Ehcache的分级缓存机制和使用场景。

5. 引用

[https://www.ehcache.org/documentation/2.8/get-started/storage-options.html]
[https://www.ehcache.org/documentation/3.4/tiering.html]

猜你喜欢

转载自blog.csdn.net/a860MHz/article/details/95328906