何时对null值做Cache

前几天发现某个系统对某个远程调用接口的调用量大幅上升,涨幅不可思议。根据接口调用上升的时间点和发布记录,查看SVN提交记录,发现是在系统主路径中添加了这个接口的调用,难道这个接口没有做Cache吗?仔细一看,倒是也做了Cache,并且这个RPC对应的DB表的数据量非常小,按理说是能全部被缓存起来的。那么为什么会反复调用,看起来仿佛没有Cache一样呢?

直觉是缓存被不存在的数据击穿了,马上验证。

通过对系统方法的追踪,发现每次调用传入的参数都是0,再去DB里面查,0对应的结果确实为空。

所以这是一个典型的因为空记录导致的缓存被击穿的案例。

解决方法很简单,对不存在的记录做一个null的Cache,下次就不会落到远端了。不过这里结合业务的特定场景,我只是加了一个判断,当值大于0才会去查询,这样连一次查询Cache的开销也省掉了。

这个简单的问题可以衍生出一些思考。

一、何时做put

通常的缓存put策略有两种:

1、查询时put:先查Cache,若不命中,则查存储(例如DB),查到后put进Cache。

2、写入时put:当数据被插入或修改时,主动put一份到Cache。

实践中其实第一种用法更普遍,只有当数据被用到了才会进入Cache。

二、如果DB没查到,是否要put null

这个就跟具体的业务场景相关了。

如果你的数据变化不频繁,那么put一个null,就可以有效起到用Cache减轻后端查询压力的作用。

但如果你的数据变化很频繁,那么put null的结果很可能导致业务上的不一致性,此时就不该做null的Cache。

即便是数据变化不频繁的情况下,如果在null的Cache失效之前,DB中又写入了新的值造成了非null的情况,这时的不一致也是不能接受的。所以在做了null的Cache后,写入的时候应该做到主动失效。

三、异常情况的处理

如果在查询DB的时候抛出了异常,例如连接拿不到、超时等等异常的时候,不应该做null的Cache。

因为此时你并不知道DB中究竟是否存在你要查的数据,如果放了一个null,当DB恢复后,就造成了数据不一致。

实践中这个问题更常见的场景在于,有时候我们的DAO没有把该抛的Exception抛出来,而是直接return null。这时外界的调用方就无法区分,你到底是没查到还是查的过程中出了异常。所以说,该抛的异常应该抛出去,不要什么情况都自己吃掉了。

四、How to put null

存放null的方法有很多,鉴于很多Cache不允许value为null,可以直接放一个跟正常对象不一致的对象,例如boolean, int这种基本类型,然后用instanceof去判断。

不过这种方式总显得不那么优雅,我觉得这里用存放Option的方式就不错。

Option本身可以认为是一个存放对象的小容器,它有两种状态:

1、存放了一个非null的对象

2、null

获取Option的时候判断是否为null就很方便了。

以Guava的库为例子,实现如下:


public Object get(String key) {
        Optional<Object> result = getFromCache(key);
        if (result.isPresent()) { // 如果不为null
                return result.get();
        } else {
                Object obj = queryFromDB(key);
                putToCache(key, Optional.fromNullable(obj)); // Option可能有值可能为null
                return obj;
        }
}

public Optional<Object> getFromCache(String key) {
        return cache.get(key);
}

public void putToCache(String key, Optional<Object> value) {
        cache.put(key, value);
}

public abstract Object queryFromDB(String key);


上一篇:Java应用线上问题排查的常用工具和方法


下一篇:阿里云容器服务 - 提速云端应用部署与运维