额外参考资料:

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. 图文讲解zabbix安装全过程(5)

    在了解<zabbix硬件.软件需求>之后,在你心里应该有备选的机器.今天开始安装zabbix.zabbix需要LNMP或者LAMP环境.环境的搭建不在本章范围内. LNMP环境配置Linu ...

  2. spring MVC中的异常统一处理

    1.spring MVC中定义了一个标准的异常处理类SimpleMappingExceptionResolver 该类实现了接口HandlerExceptionResolver 2.看下SimpleM ...

  3. Spark2.0机器学习系列之8:多类分类问题(方法归总和分类结果评估)

    一对多(One-vs-Rest classifier) 将只能用于二分问题的分类(如Logistic回归.SVM)方法扩展到多类. 参考:http://www.cnblogs.com/CheeseZH ...

  4. debian flam3 依赖文件

    https://packages.debian.org/stable/graphics/flam3     package names   descriptions   source package ...

  5. delphi pchar 指针错误

    2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var P: Pchar; //P 是指针 CSize: Cardinal; ...

  6. JQuery变量数字相加的研究

    在 jquery中,一个变量和一个数字相加,期望得到1+2=3但是: eg: <input type="text" class="input_Num" i ...

  7. XDU 1140 寻找万神(字符串匹配)

    学会strstr的使用 strstr(str1,str2)函数用于判断字符串str2是否是str1的子串.如果是,则该函数返回str2在str1中首次出现的地址:否则,返回NULL. #include ...

  8. Java设计原则—迪米特法则(转)

    定义: 迪米特法则(Law of Demeter,LoD)也称为最少知识原则(Least Knowledge Principle,LKP). 一个对象应该对其他对象有最少的了解.通俗地讲,一个类应该对 ...

  9. MVC[xml]页面传值

    MVC 各种传值方式 ViewData传值.HomeController.cs Co de: public ActionResult Index(){      ViewData["Titl ...

  10. hive报错汇总

    1.需要注意的是,要在namenode(超级用户)上操作,貌似是 hive> insert into table record_partition partition(trancation_da ...