批量缓存框架
批量缓存框架
盘路缓存
盘路缓存是我们最常使用的缓存模式,也就是获取数据的时候,如果获取不到,那么就去数据库查询一次,然后加载入缓存,再返回。
批量缓存查询
我们可以一次性查询多条缓存,这里的多条缓存是指只查询我们想要的,例如uid=1,2,4
public Map<Long, User> getUserInfoBatch(Set<Long> uids) {
// 批量组装key
List<String> keys = uids.stream().map(a -> RedisKeys.getKey(RedisKeys.USER_INFO_STRING, a)).collect(Collectors.toList());
// 批量get
List<User> mget = RedisUtils.mget(keys, User.class);
Map<Long, User> map = mget.stream().filter(Objects::nonNull).collect(Collectors.toMap(User::getId, Function.identity()));
// 发现差集——还需要load更新的uid
List<Long> needLoadUidList = uids.stream().filter(a -> !map.containsKey(a)).collect(Collectors.toList());
if (CollUtil.isNotEmpty(needLoadUidList)) {
// 批量load
List<User> needLoadUserList = userService.listByIds(needLoadUidList);
Map<String, User> redisMap = needLoadUserList.stream().collect(Collectors.toMap(a -> RedisKeys.getKey(RedisKeys.USER_INFO_STRING, a.getId()), Function.identity()));
RedisUtils.mset(redisMap, 5 * 60);
// 加载回redis
map.putAll(needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity())));
}
return map;
}
批量缓存框架
将上面代码中的一些部分做抽象,使得不止User可以使用,任何需要缓存的都可以使用
只有组装key的逻辑和userService.listByIds需要特定的类去实现
接口
public interface BatchCache<IN, OUT> {
/**
* 获取单个
*/
OUT get(IN req);
/**
* 获取批量
*/
Map<IN, OUT> getBatch(List<IN> req);
/**
* 修改删除单个
*/
void delete(IN req);
/**
* 修改删除多个
*/
void deleteBatch(List<IN> req);
}
这个接口定义了一个泛型缓存操作的基本方法:
get(IN req)
:获取单个缓存值。getBatch(List<IN> req)
:批量获取缓存值。delete(IN req)
:删除单个缓存值。deleteBatch(List<IN> req)
:批量删除缓存值。
抽象类
public abstract class AbstractRedisStringCache<IN, OUT> implements BatchCache<IN, OUT> {
private Class<OUT> outClass;
protected AbstractRedisStringCache() {
ParameterizedType genericSuperclass = (ParameterizedType) this.getClass().getGenericSuperclass();
this.outClass = (Class<OUT>) genericSuperclass.getActualTypeArguments()[1];
}
protected abstract String getKey(IN req);
protected abstract Long getExpireSeconds();
protected abstract Map<IN, OUT> load(List<IN> req);
@Override
public OUT get(IN req) {
return getBatch(Collections.singletonList(req)).get(req);
}
@Override
public Map<IN, OUT> getBatch(List<IN> req) {
if (CollectionUtil.isEmpty(req)) {//防御性编程
return new HashMap<>();
}
//去重
req = req.stream().distinct().collect(Collectors.toList());
//组装key
List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList());
//批量get
List<OUT> valueList = RedisUtils.mget(keys, outClass);
//差集计算
List<IN> loadReqs = new ArrayList<>();
for (int i = 0; i < valueList.size(); i++) {
if (Objects.isNull(valueList.get(i))) {
loadReqs.add(req.get(i));
}
}
Map<IN, OUT> load = new HashMap<>();
//不足的重新加载进redis
if (CollectionUtil.isNotEmpty(loadReqs)) {
//批量load
load = load(loadReqs);
Map<String, OUT> loadMap = load.entrySet().stream()
.map(a -> Pair.of(getKey(a.getKey()), a.getValue()))
.collect(Collectors.toMap(Pair::getFirst, Pair::getSecond));
RedisUtils.mset(loadMap, getExpireSeconds());
}
//组装最后的结果
Map<IN, OUT> resultMap = new HashMap<>();
for (int i = 0; i < req.size(); i++) {
IN in = req.get(i);
OUT out = Optional.ofNullable(valueList.get(i))
.orElse(load.get(in));
resultMap.put(in, out);
}
return resultMap;
}
@Override
public void delete(IN req) {
deleteBatch(Collections.singletonList(req));
}
@Override
public void deleteBatch(List<IN> req) {
List<String> keys = req.stream().map(this::getKey).collect(Collectors.toList());
RedisUtils.del(keys);
}
}
AbstractRedisStringCache
类实现了BatchCache
接口,并使用了模板方法设计模式。具体实现了以下几个方法:
- 构造函数中通过反射获取泛型类型。
getKey
方法用于生成Redis中的键。getExpireSeconds
方法用于获取缓存过期时间。load
方法用于从数据库中加载缺失的数据并写入Redis。getBatch
方法实现了批量获取缓存逻辑,包含去重、从Redis中获取数据、缺失数据的加载与缓存更新。delete
和deleteBatch
方法用于删除单个或多个缓存值。
盘路缓存
@Component
public class UserInfoCache extends AbstractRedisStringCache<Long, User> {
@Autowired
private UserService userService;
@Override
protected String getKey(Long uid) {
return RedisKeys.getKey(RedisKeys.USER_INFO_STRING, uid);
}
@Override
protected Long getExpireSeconds() {
return 5 * 60L;
}
@Override
protected Map<Long, User> load(List<Long> uidList) {
List<User> needLoadUserList = userService.listByIds(uidList);
return needLoadUserList.stream().collect(Collectors.toMap(User::getId, Function.identity()));
}
}
UserInfoCache
类继承了AbstractRedisStringCache
,并具体实现了三个抽象方法:
getKey(Long uid)
:生成用户信息在Redis中的键。getExpireSeconds()
:缓存过期时间为5分钟。load(List<Long> uidList)
:通过UserService
从数据库中加载用户信息。
实现细节
模版方法设计模式
模板方法模式(Template Method Pattern)是一种行为设计模式,它定义了一个算法的骨架,并允许子类在不改变算法结构的情况下,重新定义算法的某些步骤。模板方法模式通过基类提供一个模板方法,该方法按步骤调用一些抽象方法或具体方法,而具体的实现由子类完成。
模板方法模式的核心思想是:
- 定义算法骨架:在基类中定义算法的基本结构,并实现一些通用的方法。
- 推迟具体实现:将一些特定步骤的实现推迟到子类中完成。
模板方法模式的结构
模板方法模式通常包含以下几个部分:
- 抽象类(Abstract Class):定义算法的骨架和一些具体的方法,同时包含一些抽象方法让子类实现。
- 具体类(Concrete Class):继承抽象类,实现抽象方法,从而完成具体的算法步骤。
在上面的抽象类中:
get(IN req)
和delete(IN req)
是具体的方法,已经实现。getBatch(List<IN> req)
是一个模板方法,定义了缓存获取的骨架流程:先从缓存中获取数据,如果数据不存在则从数据库中加载,并将加载的数据写入缓存。getKey(IN req)
,getExpireSeconds()
, 和load(List<IN> req)
是抽象方法,具体实现由子类完成。
protected
在Java中,protected
修饰符的作用是在一定范围内控制成员的可见性和访问权限。具体来说,protected
修饰符具有以下特性:
- 同一个包中的类:
protected
成员可以被同一个包中的其他类访问。 - 子类:无论子类是否在同一个包中,
protected
成员都可以被子类访问。这意味着子类可以继承和使用父类中定义的protected
成员。
在上面的代码中,
AbstractRedisStringCache
类中的构造方法和抽象方法使用protected
修饰符,目的是:
- 限制访问范围:这些成员只能被同包中的类或继承
AbstractRedisStringCache
的子类访问,防止包外的非子类直接访问。- 允许子类继承和使用:子类如
UserInfoCache
可以访问和实现这些protected
方法,从而完成特定功能。
为什么选用protected而不是public
封装性和继承的需求
封装实现细节:
AbstractRedisStringCache
类中的抽象方法如getKey
,getExpireSeconds
, 和load
是实现缓存逻辑的关键步骤。这些方法的具体实现对于外部使用者来说并不重要,只需要知道如何调用get
、getBatch
、delete
、deleteBatch
等公开方法即可。因此,将这些方法定义为protected
可以隐藏内部实现细节,防止外部类直接调用和依赖这些内部方法。继承与重写:
protected
允许子类重写这些方法,从而定制缓存框架的具体行为。将这些方法定义为protected
明确表示这些方法是给子类重写和使用的,而不是给所有外部类直接调用的。子类如UserInfoCache
可以通过重写这些方法来实现特定的缓存逻辑。明确设计意图:使用
protected
可以清晰地传达设计意图,即这些方法是框架的内部实现细节,应该由子类来实现和使用。框架的使用者只需要关注公开的接口方法(public
方法)即可,而不需要关心这些内部细节。限制访问范围:将方法定义为
protected
可以防止不相关的类(即非子类和不同包中的类)直接访问和修改这些方法,从而减少潜在的误用和错误,提升代码的安全性和可靠性。