Skip to main content

批量缓存框架


批量缓存框架

盘路缓存

盘路缓存是我们最常使用的缓存模式,也就是获取数据的时候,如果获取不到,那么就去数据库查询一次,然后加载入缓存,再返回。

批量缓存查询

我们可以一次性查询多条缓存,这里的多条缓存是指只查询我们想要的,例如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中获取数据、缺失数据的加载与缓存更新。
  • deletedeleteBatch方法用于删除单个或多个缓存值。

盘路缓存

@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)是一种行为设计模式,它定义了一个算法的骨架,并允许子类在不改变算法结构的情况下,重新定义算法的某些步骤。模板方法模式通过基类提供一个模板方法,该方法按步骤调用一些抽象方法或具体方法,而具体的实现由子类完成。

模板方法模式的核心思想是:

  1. 定义算法骨架:在基类中定义算法的基本结构,并实现一些通用的方法。
  2. 推迟具体实现:将一些特定步骤的实现推迟到子类中完成。

模板方法模式的结构

模板方法模式通常包含以下几个部分:

  • 抽象类(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修饰符具有以下特性:

  1. 同一个包中的类protected成员可以被同一个包中的其他类访问。
  2. 子类:无论子类是否在同一个包中,protected成员都可以被子类访问。这意味着子类可以继承和使用父类中定义的protected成员。

在上面的代码中,AbstractRedisStringCache类中的构造方法和抽象方法使用protected修饰符,目的是:

  • 限制访问范围:这些成员只能被同包中的类或继承AbstractRedisStringCache的子类访问,防止包外的非子类直接访问。
  • 允许子类继承和使用:子类如UserInfoCache可以访问和实现这些protected方法,从而完成特定功能。

为什么选用protected而不是public

封装性和继承的需求

  1. 封装实现细节AbstractRedisStringCache类中的抽象方法如getKey, getExpireSeconds, 和 load是实现缓存逻辑的关键步骤。这些方法的具体实现对于外部使用者来说并不重要,只需要知道如何调用getgetBatchdeletedeleteBatch等公开方法即可。因此,将这些方法定义为protected可以隐藏内部实现细节,防止外部类直接调用和依赖这些内部方法。

  2. 继承与重写protected允许子类重写这些方法,从而定制缓存框架的具体行为。将这些方法定义为protected明确表示这些方法是给子类重写和使用的,而不是给所有外部类直接调用的。子类如UserInfoCache可以通过重写这些方法来实现特定的缓存逻辑。

  3. 明确设计意图:使用protected可以清晰地传达设计意图,即这些方法是框架的内部实现细节,应该由子类来实现和使用。框架的使用者只需要关注公开的接口方法(public方法)即可,而不需要关心这些内部细节。

  4. 限制访问范围:将方法定义为protected可以防止不相关的类(即非子类和不同包中的类)直接访问和修改这些方法,从而减少潜在的误用和错误,提升代码的安全性和可靠性。