高并发场景中的接口缓存策略

469 阅读5分钟

一、前言

缓存、异步、多线程堪称高并发编程的三把利器,在高并发场景,很多接口比如商城首页banner,配置,商品列表等是用户一进入应用就会访问的,这些称为热点接口,部分接口查询逻辑比较复杂,可能会遍及多个表,如果每次访问都要请求数据库,会对数据库造成巨大的压力,当压力达到一定瓶颈时,数据库就会响应变慢甚至宕机,应用卡顿,用户体验变差,公司业务受损。这种情况下最有效的办法就是在接口层做数据缓存,接下来聊一下如何正确的实现。

二、几个问题

说起缓存,不得不先简单聊一下缓存穿透,缓存击穿,缓存雪崩,缓存一致性几个问题。

缓存穿透

  • 描述: 缓存穿透是指用户请求的数据在缓存中不存在即没有命中,同时在数据库中也不存在,导致用户每次请求该数据都要去数据库中查询一遍,然后返回空。

  • 解决办法

    • 使用布隆过滤器。
    • 缓存空对象。

缓存击穿

  • 描述: 指一个非常热点的key,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大流量就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。

  • 解决办法

    • 更新缓存时加锁,只允许一个线程更新。
    • 缓存数据本身永不过期,通过额外字段设置过期时间。

缓存雪崩

  • 描述:缓存中数据大批量到过期时间,而查询数据量巨大,请求直接落到数据库上,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。

  • 解决办法:

    • 设置不同过期时间。
    • 更新缓存时加锁,只允许一个线程更新。
    • 缓存永不过期。

缓存一致性

  • 描述:如何保证数据库中的数据和缓存一致。

  • 解决办法:

    • Cache Aside(旁路缓存)策略
      • 写策略:先更新数据库中的数据,再删除缓存中的数据。

      • 读策略

        • 如果读取的数据命中了缓存,则直接返回数据。
        • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。
    • 延时双删:先删除一次缓存,让数据重新从数据库加载,然后更新数据库,最后再次删除缓存。

三、方案

从上面内容总结下设计一个接口缓存需要考虑的点和对应解决方案

  1. 缓存穿透:和缓存正常数据一样,缓存空值即可避免。
  2. 缓存击穿:更新缓存的时候加锁,防止多个并发请求全部请求数据库。
  3. 缓存雪崩:设置缓存数据永不过期,通过额外字段控制数据刷新。
  4. 缓存一致性:
    • 对于实时性要求较高的数据,比如商品上下架状态更新,使用Cache Aside中的写策略,在更新数据库后更新缓存中的数据。
    • 对于实时性要求一般的数据,可以不用做更新操作,因为如果数据本身的过期时间很短,那么这块数据不一致的时间也很短,对业务来说一般是可以接受的,如果数据本身的过期时间很长,那么这项数据本身就没要求高实时性。
    • 不能使用延时双删,会造成缓存击穿

方案确定以后开始设计流程

分析:一般接口会有多个入参,每个参数有不同的值,不同入参返回的数据是不一样。

相关key设计:

  • 业务入参组合:

    • 拼接参数即可
    • 例:String paramKey = request.getActivityType() + request.getPageNum() + LIMITER + request.getPageSize()
  • 缓存key:

    • 前缀 + 业务入参组合
    • 例: HOME_GOODS_LIST: + paramKey
  • 缓存刷新key:

    • 前缀 + 业务入参组合
    • 例: HOME_GOODS_LIST:REFRESH + paramKey
  • 缓存刷新加锁的key:

    • 前缀 + 业务入参组合
    • 例: HOME_GOODS_LIST:REFRESH_LOCK + paramKey

接口流程设计:

image.png

外部更新缓存数据:

image.png

样板代码如下:

public PageInfo<ComposeListResponse> composeList(DecomposeReqeuest request) {
    String paramKey = SEPARATOR + request.getPageNum() + LIMITER + request.getPageSize() + LIMITER + request.getActivityType();
    String composePageCacheKey = FREE_COMPOSE_LIST + paramKey;
    String cacheRefreshKey =  FREE_COMPOSE_LIST_REFRESH + paramKey;
    String cacheRefreshLockKey = FREE_COMPOSE_LIST_REFRESH_LOCK + paramKey;

    String composeListCache = redisTemplateCluster.opsForValue().get(composePageCacheKey);


    PageInfo<ComposeListResponse> composeList = null;
    if (StringUtils.isNotBlank(composeListCache)) {
        try {
            composeList = JsonUtil.parse(composeListCache, new TypeReference<>() {
            });
            // 判断是否需要加锁更新缓存
            String cacheRefresh = redisTemplateCluster.opsForValue().get(cacheRefreshKey);
            if (StringUtils.isBlank(cacheRefresh)) {
                boolean locked = LockUtil.tryLock(cacheRefreshLockKey);
                if (locked) {
                    try {
                        composeList = getDataFromDbAndFlushCache(cacheRefreshKey, composePageCacheKey, request);
                    } catch (Exception e) {
                        log.error("composeList cacheRefresh error", e);
                    } finally {
                        LockUtil.unlock(cacheRefreshLockKey);
                    }
                }
            }
        } catch (Exception e) {
            log.error("composeList to cache error", e);
        }
    } else {
        composeList = getDataFromDbAndFlushCache(cacheRefreshKey, composePageCacheKey, request);
    }
    return composeList;
}


private PageInfo<ComposeListResponse> getDataFromDbAndFlushCache(String cacheRefreshKey, String composePageCacheKey, DecomposeReqeuest request) {
  // 查询数据
    redisTemplateCluster.opsForValue().set(composePageCacheKey, JsonUtil.json(responsePageInfo));
    redisTemplateCluster.opsForValue().set(cacheRefreshKey, "1", 10, TimeUnit.SECONDS);
    return responsePageInfo;
}

四、结论

从上面的流程可以看出接口缓存实现并不复杂,只是得注意其中不同功能对应key不能用错,这种写法的优点是灵活简单,可基于实际业务做更多控制,适用于项目中需要用到缓存的接口不算很多的情况,而缺点是业务入侵性比较大,如果很多的话,可以通过Aop加自定义注解来简化写法,通过后台线程统一更新缓存,或者引入外部比较成熟的缓存框架如阿里的jetcache,支持分布式加本地两级缓存,感兴趣的伙伴可以去看看。

最后

① 感谢阅读~
② 如果对于文中所持观点有不同的看法,欢迎大佬指点,交流~