有如下表

t_id	code
1	c1
1	c2
2	c2

t_id和code是多对多关系,应用中频繁根据code列表获取t_id列表,类似一个关联配置表,且表的数据不多。

这时,我们可以使用redis的hash结构全量缓存数据。key为自定义的固定值,field为code,value为t_id的逗号分隔,取时使用hmget拿到所有value分隔逗号,然后set去重即可根据codeList拿到t_id列表。

同时在取数据时判断key是否存在,不存在则reload,否则只读缓存,这样就能尽可能的减轻数据库压力。

示例:

public List<Long> getIdListByCodeList(List<String> codeList) {
    // 参数校验
    if (CollectionUtils.isEmpty(codeList)) {
        return new ArrayList<>();
    }
    if (!cacheService.existKeyIdsByCode()) {
        reloadCacheIdsByCode();
    }
    // 封装了相应转换
    return cacheService.hmgetIdListByCodeList(codeList);
}

public void reloadCacheIdsByCode() {
    Map<String, String> codeIdsMap = new HashMap<>();
    // todo,从数据库查询全量数据放到codeIdsMap中
    cacheService.delKeyIdsByCode();
    // 防止数据库本身没数据get时一直reload的情况
    if (codeIdsMap.isEmpty()) {
        codeIdsMap.put("_EMPTY_FIELD", "");
    }
    cacheService.hmsetIdsByCode(cacheMap);
}

现在基本解决了问题,但是有几个小问题:

  1. 多线程分布式下并发reload带来的开销,可以考虑加锁,如:
public List<Long> getIdListByCodeList(List<String> codeList) {
    // 参数校验
    if (CollectionUtils.isEmpty(codeList)) {
        return new ArrayList<>();
    }
    if (!cacheService.existKeyIdsByCode()) {
        // 避免高并发下多线程reload造成的开销,暂不考虑分布式
        boolean locked = lock.tryLock();
        if (locked) {
            try {
                reloadCacheIdsByCode();
            } finally {
                lock.unlock();
            }
        } else {
            try {
                // 50毫秒足够reload,考虑上下文切换,不能设置过小
                int times = 0;
                while (times++ <= 100 && lock.isLocked()) {
                    Thread.sleep(50L);
                }
                // 注意如果reload异常了,会返回空(第一次写key、过期)或者旧数据(修改的情况)
            } catch (Exception ignore) {

            }
        }
    }
    // 封装了相应转换
    return cacheService.hmgetIdListByCodeList(codeList);
}
  1. reload(特别是修改时)是先删除key,再加载,中间hmget可能会返回null,可以考虑使用redis的pipeline,将删除和加载放一块;或者使用双key切换,即新key完全加载成功后,才将缓存的key指向新的。