昨天线上系统报警,发现数据库QPS很高。经过排查后,发现是有一个接口没有走缓存,每次请求都透到了数据库。于是,找到接口代码看了一下。大致如下:(只是个描述逻辑的代码)
CustomerObject obj = redisService.get("myKey");
if (obj == null) {
obj = CustomerObjMapper.findBy(id);
redisService.set(obj);
}
return obj;
乍一看代码没发现啥问题,也用了缓存。那为什么线上的请求都没没走缓存呢?抓取了线上的请求,测了一把。原来这个请求,数据库没有该数据,所以方法CustomerObjMapper.findBy
直接返回了null
。然后他其实就是对这个null
做了缓存,那就等于没有做缓存了。当下次请求进来,redisService.get("myKey")
肯定就是null
。于是请求又到了数据库,最终导致数据库QPS上升。
其实,对于数据库没有数据的情况,我们也要把这个空结果缓存下来。毕竟数据库就是没有数据,你频繁查询数据库也是没有意义的。那对于这种数据库查询结果为空,怎么去做缓存呢?模板代码来了:
CustomerObject obj = redisService.get("myKey");
if (obj == null) {
obj = CustomerObjMapper.findBy(id);
// 创建一个空对象并缓存。
if (obj == null) {
obj = new CustomerObject();
}
redisService.set(obj);
}
// 通过Id区分对象是真正的数据对象,还是一个空对象。
if (obj.getId() == null) {
retun null;
}
return obj;
如果dao返回null
给我们,我们就创建一个空对象缓存下来,下次请求该方法时,就能从缓存中拿到这个空对象。然后判断一下Id是否有值。没有就说明这是一个空对象,我们可以直接reutrn null
。如果不为空,说明拿到的对象就是真正的数据对像。
其实我想了想,这也是null一直被诟病的地方吧。null既可以表示对象没有赋值,也可以表示没有数据的情况。在平时写代码中,如何更好地去分开使用null的这2种情况,是需要注意的。一般来说,对于可能返回null的方法,我们平时有一下几种处理方法:
- 如果该方法返回的是集合类型,那么我们就返回一个空集合就好了。
- 如果想用null来表达没有数据的语义,那么可以直接返回null。但必须在方法的注释中提醒调用者,该方法会返回null,代表没有查询到相关数据。
- 定义一个特殊的空对象。该对象继承了返回的目标类。
- 使用Java8的Optional类。
大家可以阅读java匠人手法-优雅的处理空值,写得很详细。