前言
日常开发为重中常遇到数据库和缓存同步的问题,目前比较常用的流程是:查询时先查询缓存,缓存不存在则从数据库查询后同时维护缓存;修改时候先修改数据库,再删除对应的缓存数据;同时结合缓存本身的淘汰策略进行整个缓存的维护。本文主要讨论查询过程中为了同步更新的写法问题,正常情况下我们可以做以下写法:
public static BizObj getBizObj(Long id) {
BizObj bizObj = cache.get(id);
if (bizObj == null) {
bizObj = getFromDb(id);
if (bizObj != null) {
cache.put(id, bizObj);
}
}
return bizObj;
}
但是某些模块可能涉及比较多的缓存,所以以上的判断缓存是否存在和维护缓存的逻辑会非常多,导致重复代码;通过函数式接口可以解决以上问题,在动手之前我们先介绍一下函数式接口。
一、函数式接口概念
函数式接口实际上对我们并不陌生,常见到的Runnable、Callable、Comparator、Function、BiFunction、Consumer、Predicate、Supplier等都是函数式接口声明。其上都有@FunctionalInterface注解,但是需要说明的FunctionalInterface注解只能作用于接口,该注解主要用于编译级错误检查。使用@FunctionalInterface注解修饰接口后,如果写的接口不符合函数式接口规范,则编译器会报错,并非加了该注解才算是函数式接口。
函数式接口就是指对于一个接口只能有一个抽象方法
,这种类型的接口也称为SAM(Single Abstract Method)接口。如果使用该注解同时定义多个抽象方法,编译阶段则会报错:
二、函数式接口实现缓存查询工具类和demo
通过这个接口定义,我们可以将以上缓存信息判断的逻辑统一到工具方法中去,数据库查询方法通过函数式接口的方式传入工具方法实现,后续有以上逻辑的时候就可以直接调用工具类来实现,demo如下。
1.自定义函数式接口和工具方法实现
代码如下(示例):
public class CollectionUtils<T> {
public static <K, V> V getFromCache(Map<K, V> map, K key, CacheFunction<K, V> function) {
try {
if (key == null) {
return null;
}
V v = map.get(key);
if (v == null) {
V fromDb = function.getFromDb(key);
System.err.println("数据库查询结果" + fromDb);
if (fromDb != null) {
map.put(key, fromDb);
System.err.println("数据库查询结果同步缓存-完成");
return fromDb;
}
}
return v;
} catch (Exception e) {
System.err.println("从缓存查询数据异常,从数据库查询" + key);
return function.getFromDb(key);
}
}
/**
* 可以直接使用接口{@link java.util.function.Function}
* 为了代码排查方便,使用自定义的接口
*/
@FunctionalInterface
public interface CacheFunction<K, V> {
V getFromDb(K k);
}
}
2.模拟缓存调用demo
代码如下(示例):
public class CacheTest {
// 缓存
private static Map<Long, BizObj> cache = new HashMap<>();
// 数据库数据模拟
private static Map<Long, BizObj> dbMock = new HashMap<Long, BizObj>() {
{
put(1L, new BizObj(1L, "router"));
put(2L, new BizObj(2L, "switch"));
put(3L, new BizObj(3L, "voice"));
}
};
// 业务调用模拟
public static void main(String[] args) {
System.err.println("【查询结果】" + getBizObj(1L));
System.err.println("【查询结果】" + getBizObj(2L, "switch"));
}
public static BizObj getBizObj1(Long id) {
BizObj bizObj = cache.get(id);
if (bizObj == null) {
bizObj = getFromDb(id);
if (bizObj != null) {
cache.put(id, bizObj);
}
}
return bizObj;
}
// 对外提供的查询接口
public static BizObj getBizObj(Long id) {
return CollectionUtils.getFromCache(cache, id, CacheTest::getFromDb);
}
// 对外提供的查询接口
public static BizObj getBizObj(Long id, String name) {
return CollectionUtils.getFromCache(cache, id, key -> getFromDb(id, name));
}
// 查询数据库的接口不对外提供
private static BizObj getFromDb(Long id) {
return dbMock.get(id);
}
// 查询数据库的接口不对外提供
private static BizObj getFromDb(Long id, String name) {
BizObj bizObj = dbMock.get(id);
return bizObj != null && bizObj.getName().equals(name) ? bizObj : null;
}
}
业务数据类定义如下(此处省略get/set方法):
/**
* 业务数据结构定义
*/
public class BizObj
{
private Long id;
private String name;
}
3.补充批量查询的工具方法
代码如下(示例):
public static <K, V> Map<K, V> getMultiFromCache(Map<K, V> map, Set<K> keys, CacheMultiFunction<K, V> function) {
try {
HashSet notHitKeys;
if (org.apache.commons.collections4.CollectionUtils.isEmpty(keys)) {
return new HashMap<>();
}
Map cache = map.getAll(keys);
HashSet hashSet = notHitKeys = cache == null ? keys : new HashSet(org.apache.commons.collections4.CollectionUtils.
subtract((Iterable) keys, cache.keySet()));
if (!notHitKeys.isEmpty()) {
Map<K, V> fromDb = function.getFromDb(notHitKeys);
System.err.println("数据库查询结果大小:" + fromDb != null ? Integer.valueOf(fromDb.size()) : null);
if (fromDb != null && !fromDb.isEmpty()) {
map.putAll(fromDb);
System.err.println("更新缓存-putAll-ok;");
if (cache != null) {
cache.putAll(fromDb);
} else {
return fromDb;
}
}
}
return cache == null ? new HashMap() : cache;
} catch (Exception e) {
System.err.println("从缓存获取信息异常,尝试从数据库读取: " + keys + e.getMessage());
return function.getFromDb(keys);
}
}
public static interface CacheMultiFunction<K, V> {
public Map<K, V> getFromDb(Set<K> keys);
}
总结
以上实践通过函数式接口抽象了通用的业务逻辑,消除了重复代码。不过实际业务中需要考虑的问题还很多,比如同步问题、更新策略、缓存淘汰策略等,需要具体问题具体分析。