额外参考资料:

http://www.ehcache.org/documentation/3.2/expiry.html

F. Cache Aside 模式的问题:缓存过期

有时我们会在上线前给缓存系统来个预热,提前读取一部分用户信息到缓存中。默认情况下,这些缓存项拥有相同的 ttl 设置,会在一个很短的时间段内大批量的过期,导致这段时间后端 SoR 压力过大,可能会导致整个系统崩溃。

如果我们给每个缓存项设计一个随机的过期时间,就可以避免缓存过期的集中爆发。

G. Cache Aside 模式的问题:缓存穿透

当查询无结果时,一般情况下我们不会缓存这次查询(Ehcache3 也不允许缓存 null 值)。但是,有时由于程序缺陷或者恶意攻击,短时间内会有大量异常查询请求到达系统,这些请求全都会透过缓存层到达 SoR,可能导致后端 SoR 崩溃。这就是缓存穿透。

因此,对于查询结果为 null 的请求,我们也需要缓存起来。只不过缓存的过期时间必须很短,防止恶意攻击程序制造出太多无用的缓存项,把整个缓存无效化。

缓存穿透的另一种解决办法是使用布隆过滤器将不存在的 id 提前拦截掉,降低 SoR 层的压力。布隆过滤器不在本次交流范围内,具体细节可以参考以下链接:

https://mp.weixin.qq.com/s/TBCEwLVAXdsTszRVpXhVug

http://blog.csdn.net/dadoneo/article/details/6847481

https://en.wikipedia.org/wiki/Bloom_filter

H. Ehcache3 自定义过期策略

通过实现 Expiry 接口即可以自定义过期策略。

public interface Expiry<K, V> {

  /**
* 当缓存项第一次写入缓存中时,为该缓存项设置过期时间(从当前时间开始,超过指定的 Duration 时间即为过期)。
* 返回值不可以为 null。
* 当该方法抛出异常时,异常会被调用方静默处理,相当于返回了 Duration.ZERO,即立即过期。
*/
Duration getExpiryForCreation(K key, V value); /**
* 缓存项被命中时,用返回值重置该缓存项的过期时间。
* 返回值为 null 表示不重置过期时间
* 当该方法抛出异常时,异常会被调用方静默处理,相当于返回了 Duration.ZERO,即立即过期。
*/
Duration getExpiryForAccess(K key, ValueSupplier<? extends V> value); /**
* 缓存项被更新时,用返回值重置该缓存项的过期时间。
* 返回值为 null 表示不重置过期时间
* 当该方法抛出异常时,异常会被调用方静默处理,相当于返回了 Duration.ZERO,即立即过期。
*/
Duration getExpiryForUpdate(K key, ValueSupplier<? extends V> oldValue, V newValue);

演示代码如下:

gordon.study.cache.ehcache3.pattern.CustomExpiryCacheAsideUserService.java

    private static final UserModel NULL_USER = new NullUser();

    private Ehcache<String, UserModel> cache;

    public CustomExpiryCacheAsideUserService() {
cache = (Ehcache<String, UserModel>) UserManagedCacheBuilder.newUserManagedCacheBuilder(String.class, UserModel.class)
.withExpiry(new CustomUserExpiry(new Duration(100, TimeUnit.SECONDS))).build(true);
} public UserModel findUser(String id) {
UserModel cached = cache.get(id);
if (cached != null) {
System.out.println("get user from cache: " + id);
return cached;
}
UserModel user = null;
if (!id.equals("0")) {
user = new UserModel(id, "info ..."); // find user
}
System.out.println("get user from db: " + id);
if (user == null) {
user = NULL_USER;
}
cache.put(id, user);
return user;
} private static class CustomUserExpiry implements Expiry<String, UserModel> { private final Duration ttl; public CustomUserExpiry(Duration ttl) {
this.ttl = ttl;
} @Override
public Duration getExpiryForCreation(String key, UserModel value) {
if (value.isNull()) {
System.out.println("user is null: " + key);
return new Duration(10, TimeUnit.SECONDS);
}
long length = ttl.getLength();
if (length > 10) {
long max = length / 5;
long random = ThreadLocalRandom.current().nextLong(-max, max);
return new Duration(ttl.getLength() + random, ttl.getTimeUnit());
}
return ttl;
} @Override
public Duration getExpiryForAccess(String key, ValueSupplier<? extends UserModel> value) {
return null;
} @Override
public Duration getExpiryForUpdate(String key, ValueSupplier<? extends UserModel> oldValue, UserModel newValue) {
return ttl;
}
} public static void main(String[] args) throws Exception {
final CustomExpiryCacheAsideUserService service = new CustomExpiryCacheAsideUserService();
for (int i = 0; i < 5; i++) {
service.findUser("" + i);
}
}

CustomUserExpiry 类似于 TimeToLiveExpiry,唯一区别是当缓存项被创建时,返回的 Duration 会在原来的基础上随机浮动 20%。考虑到并发性,随机数生成器用的是 ThreadLocalRandom。CustomUserExpiry 类通过代码第7行 withExpiry 方法设置。这用来解决缓存过期问题。

至于缓存穿透问题,首先用 Null Object 模式修改 UserModel 类,增加 isNull 方法用于判断是否为 null object,简单起见,该方法直接返回 false。再定义 NullUser 类,继承自 userModel,其 isNull 方法返回 true。

public class UserModel {

    public boolean isNull() {
return false;
} public static class NullUser extends UserModel { public NullUser() {
super(null, null);
} public boolean isNull() {
return true;
}
}
}

代码第21行,当 SoR 返回的查询结果为 null 时,使用第1行预先定义好的 NullUser 实例作为返回值,同时将本次查询结果放入缓存。

代码第38行,如果即将创建的缓存是 null object,则只缓存10秒钟。

I. 过期算法略读

Ehcache3 的回收判定发生在 put 操作时,而过期判定则发生在 get 操作时。

Ehcache 类的 get 方法调用其 Store store 属性的 get 方法,尝试获得缓存的数据。Store 代表缓存的各种存储方式。

本例中,Store 的具体实现类为 OnHeapStore,表示使用堆空间存储缓存项。它的 get 方法调用其 Backend map 属性的 get 方法尝试获得缓存的数据。Backend 通过泛型屏蔽底层 map 的键类型。

本例中,Backend 的具体实现类为 SimpleBackend,它拥有 org.ehcache.impl.internal.concurrent.ConcurrentHashMap<K, OnHeapValueHolder> realMap 存储缓存项,该 ConcurrentHashMap 基本 copy 自 JDK 的 ConcurrentHashMap,只是增加了几个方法。SimpleBackend 的 get 方法就是调用该 ConcurrentHashMap 的 get 方法尝试获得缓存的数据。

OnHeapStore 从最底层的 ConcurrentHashMap 获取到缓存项后,调用 OnHeapValueHolder 的 isExpired 方法判断缓存项是否过期:

  @Override
public boolean isExpired(long expirationTime, TimeUnit unit) {
final long expire = this.expirationTime;
if (expire == NO_EXPIRE) {
return false;
}
return expire <= nativeTimeUnit().convert(expirationTime, unit);
}

OnHeapValueHolder 的 expirationTime 属性用于判断缓存项是否过期。方法参数 long expirationTime 传入的是当前系统时间,如果 OnHeapValueHolder 的 expirationTime 小于当前系统时间,则该缓存项已经过期。对于过期的缓存项,会将之从 ConcurrentHashMap 移除,因此 Ehcache 的 get 方法会返回 null。

Ehcahce3 的这种设计方式导致 Null Object 模式优化的缓存穿透预防方案有点奇怪。一般情况下,null object 不会被再次 get,就不会被过期算法直接移除,另一方面,按照默认回收算法只比较 lastAccessTime 值,因此为 null object 设置的很短的失效时间实际上很可能没有作用。

个人觉得,put 操作引发的回收算法中可以增加过期判断,如果发现过期数据,优先回收这些数据,可以缓解明明缓存空间中有过期数据,却回收尚未过期的数据这种情况。

缓存技术内部交流_04_Cache Aside续篇的更多相关文章

  1. 缓存技术内部交流_05_Cache Through

    参考资料: http://www.ehcache.org/documentation/3.2/caching-patterns.html http://www.ehcache.org/document ...

  2. 缓存技术内部交流_01_Ehcache3简介

    参考资料: http://www.ehcache.org/documentation/3.2/getting-started.html http://www.ehcache.org/documenta ...

  3. 缓存技术内部交流_03_Cache Aside

    参考资料: http://www.ehcache.org/documentation/3.2/caching-patterns.html http://www.ehcache.org/document ...

  4. 缓存技术内部交流_02_Ehcache3 XML 配置

    参考资料: http://www.ehcache.org/documentation/3.2/getting-started.html#configuring-with-xml http://www. ...

  5. .Net环境下的缓存技术介绍 (转)

    .Net环境下的缓存技术介绍 (转) 摘要:介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页) 1         概念 ...

  6. [.net 面向对象程序设计进阶] (14) 缓存(Cache) (一) 认识缓存技术

    [.net 面向对象程序设计进阶] (14) 缓存(Cache)(一) 认识缓存技术 本节导读: 缓存(Cache)是一种用空间换时间的技术,在.NET程序设计中合理利用,可以极大的提高程序的运行效率 ...

  7. .Net环境下的缓存技术介绍

    .Net环境下的缓存技术介绍 摘要: 介绍缓存的基本概念和常用的缓存技术,给出了各种技术的实现机制的简单介绍和适用范围说明,以及设计缓存方案应该考虑的问题(共17页) 1         概念 1.1 ...

  8. ASP.NET 缓存技术分析

    缓存功能是大型网站设计一个很重要的部分.由数据库驱动的Web应用程序,如果需要改善其性能,最好的方法是使用缓存功能.可能的情况下尽量使用缓存,从内存中返回数据的速度始终比去数据库查的速度快,因而可以大 ...

  9. 强大的Spring缓存技术(上)

    缓存是实际工作中非常常用的一种提高性能的方法, 我们会在许多场景下来使用缓存. 本文通过一个简单的例子进行展开,通过对比我们原来的自定义缓存和 spring 的基于注释的 cache 配置方法,展现了 ...

随机推荐

  1. 010-Hadoop Hive sql语法详解5-HiveQL与SQL区别

    1.Hive不支持等值连接 •SQL中对两表内联可以写成:•select * from dual a,dual b where a.key = b.key;•Hive中应为•select * from ...

  2. HTML5开源RPG游戏引擎lufylegendRPG 1.0.0发布

    经历了几个月的改进,终于发布1.0.0版了.虽然引擎依然存在漏洞,但是比起上次更新还是要好多了.在这里不得不感谢各位网友的大力支持. 首先为引擎做一个开场白吧,也好让大家了解一下它: lufylege ...

  3. 实时流计算Spark Streaming原理介绍

    1.Spark Streaming简介 1.1 概述 Spark Streaming 是Spark核心API的一个扩展,可以实现高吞吐量的.具备容错机制的实时流数据的处理.支持从多种数据源获取数据,包 ...

  4. Linux修改信息

    修改时间 sudo date -s MM/DD/YY //修改日期 sudo date -s hh:mm:ss //修改时间 在修改时间以后,修改硬件CMOS的时间 sudo hwclock --sy ...

  5. Java中二叉树存储结构实现

    一.二叉树 二叉树指的是每个节点最多只能有两个子树的有序树.通常左边的子树被称为“左子树”(left subtree),右边的子树被称为右子树. 二叉树的每个节点最多只有2棵子树,二叉树的子树次序不能 ...

  6. python16_day25【crm】

    一.CRM模拟admin功能 1.过滤功能 2.显示数据分页 3.动态菜单 项目:https://github.com/willianflasky/growup/tree/master/s16/hom ...

  7. AviMemDc: a C++ class

    AviMemDc: a C++ class        This class is used in the Avi Examples.The header fileAviMemDC.h /*    ...

  8. session和token的区别

    session的使用方式是客户端cookie里存id,服务端session存用户数据,客户端访问服务端的时候,根据id找用户数据 而token一般翻译成令牌,一般是用于验证表明身份的数据或是别的口令数 ...

  9. Java哲学家进餐

    某次操作系统实验存档. 这个哲学家除了吃就是睡.. 哲学家.java: package operating.entity.philosophyeating; import operating.meth ...

  10. Python Date 1–Hello world print

    对比学习Python与C str1 = 'hello python 2'# 字符串i = 3.1415926535 print(str1)print("hello python\n" ...