侧边栏壁纸
博主头像
博主等级

  • 累计撰写 19 篇文章
  • 累计创建 34 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

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

前尘一梦
2022-07-01 / 0 评论 / 21 点赞 / 181 阅读 / 6179 字

前言

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

几个问题

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

缓存穿透

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

  • 解决办法

    • 使用布隆过滤器。

    • 缓存空对象。

缓存击穿

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

  • 解决办法

    • 更新缓存时加锁,只允许一个线程更新。

    • 缓存数据本身永不过期,通过额外字段设置过期时间。

缓存雪崩

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

  • 解决办法

    • 设置不同过期时间。

    • 更新缓存时加锁,只允许一个线程更新。

    • 缓存永不过期。

缓存一致性

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

  • 解决办法

    • Cache Aside(旁路缓存)策略

      • 写策略:先更新数据库中的数据,再删除缓存中的数据。

      • 读策略

        • 如果读取的数据命中了缓存,则直接返回数据。

        • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

    • 延时双删:先删除一次缓存,让数据重新从数据库加载,然后更新数据库,最后再次删除缓存。

方案

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

  • 缓存穿透:和缓存正常数据一样,缓存空值即可避免。

  • 缓存击穿:更新缓存的时候加锁,防止多个并发请求全部请求数据库。

  • 缓存雪崩:设置缓存数据永不过期,通过额外字段控制数据刷新。

  • 缓存一致性:

    • 对于实时性要求较高的数据,比如商品上下架状态更新,使用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

接口流程设计:

外部更新缓存数据:

样板代码如下:

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

支持分布式加本地两级缓存,感兴趣的伙伴可以去看看。

21

评论区