• 种草社区缓存设计


    本文来自网易云社区

    作者:刘魏威


    引言

    从数据模型来看,种草社区 = 内容 + 关系 + 计数。在业务上,具体展开就是,

    • 内容:即提问、回答、心得、用户、消息

    • 关系:即提问->回答, 用户->回答, 用户->消息

    • 计数:即点赞数、粉丝数、关注数、回答数等各种计数

    如何高效处理这几个主要元素,决定了社区系统的用户体验和服务容量。

    最初项目为了尽快上线,这些数据都是直接到db里查询,前期访问量小,没什么关系。但到了后面,一旦访问量放大,db的资源瓶颈就会凸显。后来的压测结果也的确反应了这一点,在访问量稍微增长,数据库qps上升时,ddb的响应时间明显变长,平均响应时间有40~50ms。因此急需对这些数据进行缓存,以抵挡直接访问db的大部分流量。

    实现这些数据的缓存做法很简单,但若是要把他做好,且各个业务模型能高效率的接入使用,则需要好好考量下。

    内容缓存

    内容缓存,这里特指DAO缓存,以数据库主键为查询key,数据库行记录为value。

    DAO缓存的实现有一些开源框架可以直接拿来用,如spring->缓存key的设计

    缓存key需要包含哪些元素?先来列举下之前遇到过的问题:

    • 线上环境和预发环境共用一套缓存,测试时修改预发布环境缓存会有风险

    • 缓存Value变更,比如缓存对象增加一个业务相关的字段,新老缓存可能同时存在,无法做到无缝发布,对业务无影响

    • key前缀分布较随意,在代码里没有一个集中管理的地方,不同的业务有可能会冲突

    • 批量删除或迁移缓存

    解决上述问题,key就需要包含:

    • 环境信息,隔离环境,防止相互影响

    • 版本,增加数据库字段时,可以修改版本,使业务读不到缓存从而强制刷新缓存

    • 集中的前缀管理,简单实现就是一个枚举常量,所有业务DAO前缀定义放在一起

    缓存未命中如何处理

    缓存miss,需要知道是数据真的不存在,还是仅仅缓存过期了。有些黑客可能会恶意构造数据,导致缓存无限击穿。所以需要设计一个标识不存在的对象,从缓存里取出数据时做下判断,如果是特定的空对象,则不需要再去db获取了。

    缓存未命中时读数据库,如果是单条数据,则同步设置到缓存,如果是多条,则异步设置到缓存。

    关系缓存

    关系数据通常用于列表场景,批量取符合条件的数据,然后按指定字段排序,分页展示。

    这块比较难处理的就是过滤+排序+分页。业务体量小时可以不使用缓存,建立专门的索引表,把需要作为过滤条件的字段包含到索引表里,利用数据库去处理排序、过滤。然而访问量大了之后,数据库就不适合干这个事情了。为了解决这类问题,种草社区实现了一套基于redis sorted set的通用关系缓存API。大致的接口如下:

    /**
     * 以索引为边界批量获取有序集合中的数据
     *
     * @param keys 键名列表
     * @param begin 偏移量开始
     * @param end 偏移量结束
     * @param orderType 排序类型
     * @param relationCacheFilter 过滤器
     * @return
     */
    Map<K, Set<V>> multiGetByIndex(final List<K> keys, final long begin, final long end, final OrderType orderType,
            RelationCacheFilter<V> relationCacheFilter);
            
    /**
     * 以分数为边界批量获取有序集合中的数据
     *
     * @param keys 键名列表
     * @param min 最小分数
     * @param max 最大分数
     * @param offset 偏移量
     * @param limit 条数
     * @param orderType 排序类型
     * @param relationCacheFilter 过滤器
     * @return
     */
    Map<K, Set<V>> multiGetByScore(final List<K> keys, final double min, final double max, final long offset,
            final long limit, final OrderType orderType, RelationCacheFilter<V> relationCacheFilter);
            
    /**
     * 构建缓存key
     * 
     * @param k 键名
     * @return 缓存key
     */
    String buildCacheKey(K k);
    
    /**
     * 返回分区
     * 
     * @return
     */
    String getRegion();
    
    /**
     * 获取过期时间,单位秒
     */
    Long getExpireSeconds();
    
    /**
     * 获取全量初始化任务线程
     * 
     * @return
     */
    RelationCacheInitRunnable getCacheInitRunnable(K k);


    实现要点:

    • Multiget通过redis pipeline实现,节省网络开销,但使用时需要注意数量限制,毕竟是批量操作

    • 数据过滤,业务方提供一个回调函数,回调函数里可实现复杂的业务逻辑

    • 数据分页,提供两种方式,基于偏移量和基于score分值,以满足比较常见的场景,如取前n条心得,取时间段范围内的limit条数据。

    • 缓存未命中,分两种情况处理:批量获取,异步初始化;单条获取,同步初始化;业务方提供缓存初始化线程。单条处理同步初始化是考虑到如个人主页可能会刷不到数据,体验较差。而批量获取异步初始化,某一个用户的内容没拉取到,关系不大。真是比较重要的场景,可考虑定时刷缓存。

    计数缓存

    计数主要面临的问题有几点:

    • 高频率的读写,如何解决性能问题

    • 有限的内存存储,缓存过期初始化导致db压力大问题

    • 高并发更新及系统故障带来的数据一致性问题

    种草社区的计数缓存基于redis,数据结构上主要用到了:

    • 普通的string/value,存储单个计数

    • hash表,存储业务上有关联的一组计数,便于批量读取

    大致的数据流如下图: 

    之前专门写过一遍文章,详细可点击此处查看。



    网易云大礼包:https://www.163yun.com/gift

    本文来自网易云社区,经作者刘魏威授权发布


    相关文章:
    【推荐】 Question | 网站被黑客扫描撞库该怎么应对防范?
    【推荐】 互联网金融中的数据挖掘技术应用
    【推荐】 nej+regular环境使用es6的低成本方案

  • 相关阅读:
    mybatis的mapper特殊字符转移以及动态SQL条件查询
    MySQL查询结果集字符串操作之多行合并与单行分割
    MySQL查询之内连接,外连接查询场景的区别与不同
    SpringBoot异步使用@Async原理及线程池配置
    SpringBoot 属性配置文件数据注入配置和yml与properties区别
    MySQL实战45讲第33讲
    Beta冲刺第1次
    Beta冲刺第5次
    Beta冲刺第4次
    Beta冲刺第3次
  • 原文地址:https://www.cnblogs.com/163yun/p/9602621.html
Copyright © 2020-2023  润新知