重构,于我而言,很大的快乐在于能够解决问题。

第一次重构是重构一个c#版本的彩票算奖系统。当时的算奖系统在开奖后,算奖经常超时,导致用户经常投诉。接到重构的任务,既兴奋又紧张,花了两天时间,除了吃饭睡觉,都在撸代码。重构效果也很明显,算奖耗时从原来的1个小时减少到10分钟。

去年,我以架构师的身份参与了家校朋友圈应用的重构。应用麻雀虽小,五脏俱全,和诸君分享架构设计的思路。

01 应用背景

1. 应用介绍

移动互联网时代,Feed流产品是非常常见的,比如我们每天都会用到的朋友圈,微博,就是一种非常典型的Feed流产品。
Feed(动态):Feed流中的每一条状态或者消息都是Feed,比如朋友圈中的一个状态就是一个Feed,微博中的一条微博就是一个Feed。
Feed流:持续更新并呈现给用户内容的信息流。每个人的朋友圈,微博关注页等等都是一个Feed流。

家校朋友圈是校信app的一个子功能。学生和老师可以发送图片,视频,声音等动态信息,学生和老师可以查看班级下的动态聚合。

为什么要重构呢?

▍ 代码可维护性

服务端端代码已经有四年左右的历史,随着时间的推移,人员的变动,不断的修复Bug,不断的添加新功能,代码的可读性越来越差。而且很多维护的功能是在没有完全理解代码的情况下做修改的。新功能的维护越来越艰难,代码质量越来越腐化。

▍ 查询瓶颈
服务端使用的mysql作为数据库。Feed表数据有两千万,Feed详情表七千万左右。
服务端大量使用存储过程(200+)。动态查询基本都是多张千万级大表关联,查询耗时在5s左右。DBA同学反馈sql频繁超时。

2. 重构过程

《重构:改善既有代码的设计》这本书重点强调: “不要为了重构而重构”。 重构要考虑时间(2个月),人力成本(3人),需要解决核心问题。

1、功能模块化, 便于扩展和维护

2、灵活扩展Feed类型, 支撑新业务接入

3、优化动态聚合页响应速度

基于以上目标, 我和小伙伴按照如下的工作。

1)梳理朋友圈业务,按照清晰的原则,将单个家校服务端拆分出两个模块

  • 1 space-app: 提供rest接口,供app调用
  • 2 space-task: 推送消息, 任务处理

2)分库分表设计, 去存储过程, 数据库表设计

数据库Feed表已达到2000万, Feed详情表已达到7000万+。为了提升查询效率,肯定需要分库分表。但考虑到数据写入量每天才2万的量级,所以分表即可。

数据库里有200+的存储过程,为了提升数据库表设计效率,整理核心接口调用存储过程逻辑。在设计表的时候,需要考虑shardingKey冗余。 按照这样的思路,梳理核心逻辑以及新表设计的时间也花了10个工作日。

产品大致有三种Feed查询场景

  • 班级维度: 查询某班级下Feed动态列表
  • 用户维度:查询某用户下Feed动态列表
  • Feed维度: 查询feed下点赞列表

3)架构设计
在梳理业务,设计数据库表的过程中,并行完成各个基础组件的研发。

基础组件的封装包含以下几点:

  • 分库分表组件,Id生成器,springboot starter
  • rocketmq client封装
  • 分布式缓存封装

03 分库分表

3.1 主键

分库分表的场景下我选择非常成熟的snowflake算法。

第一位不使用,默认都是0,41位时间戳精确到毫秒,可以容纳69年的时间,10位工作机器ID高5位是数据中心ID,低5位是节点ID,12位序列号每个节点每毫秒累加,累计可以达到2^12 4096个ID。

我们重点实现了12位序列号生成方式。中间10位工作机器ID存储的是

 Long workerId = Math.abs(crc32(shardingKeyValue) % 1024)//这里我们也可以认为是在1024个槽里的slot

底层使用的是redis的自增incrby命令。

   //转换成中间10位编码Integer workerId = Math.abs(crc32(shardingKeyValue) % 1024);String idGeneratorKey = IdConstants.ID_REDIS_PFEFIX + currentTime;Long counter = atomicCommand.incrByEx(idGeneratorKey,IdConstants.STEP_LENGTH,IdConstants.SEQ_EXPIRE_TIME);Long uniqueId = SnowFlakeIdGenerator.getUniqueId(currentTime, workerId.intValue(), counter);

为了避免频繁的调用redis命令,还加了一层薄薄的本地缓存。每次调用命令的时候,一次步长可以设置稍微长一点,保持在本地缓存里,每次生成唯一主键的时候,先从本地缓存里预取一次,若没有,然后再通过redis的命令获取。

3.2 策略

因为早些年阅读cobar源码的关系,所以采用了类似cobar的分库方式。

举例:用户编号23838,crc32(userId)%1024=562,562在区间[256,511]之间。所以该用户的Feed动态会存储在t_space_feed1表。

3.3 查询

带shardingkey的查询,比如就通过用户编号查询t_space_feed表,可以非常容易的定位表名。

假如不是shardingkey,比如通过Feed编号(主键)查询t_space_feed表,因为主键是通过snowflake算法生成的,我们可以通过Feed编号获取workerId(10位机器编号), 通过workerId也就确定数据位于哪张表了。

模糊查询场景很少。方案就是走ES查询,Feed数据落库之后,通过MQ消息形式,把数据同步ES,这种方式稍微有延迟的,但是这种可控范围的延迟是可以接受的。

3.4 工程

分库分表一般有三种模式:

  1. 代理模式,兼容mysql协议。如cobar,mycat,drds。
  2. 代理模式,自定义协议。如艺龙的DDA。
  3. 客户端模式,最有名的是shardingsphere的sharding-jdbc。

分库分表选型使用的是sharding-jdbc,最重要的原因是轻便简单,而且早期的代码曾经看过一两次,原理有基础的认识。

核心代码逻辑其实还是蛮清晰的。

ShardingRule shardingRule = new ShardingRule(
shardingRuleConfiguration,
customShardingConfig.getDatasourceNames());
DataSource dataSource = new ShardingDataSource(dataSourceMap,shardingRule, properties);

请注意: 对于整个应用来讲,client模式的最终结果是初始化了DataSource的接口

  1. 需要定义初始化数据源信息
    datasourceNames是数据源名列表,
    dataSourceMap是数据源名和数据源映射。
  2. 这里有一个概念逻辑表和物理表。
逻辑表 物理表
t_space_feed (动态表) t_space_feed_0~3
  1. 分库算法:
    DataSourceHashSlotAlgorithm:分库算法
    TableHashSlotAlgorithm:分表算法
    两个类的核心算法基本是一样的。

    • 支持多分片键
    • 支持主键查询
  2. 配置shardingRuleConfiguration。
    这里需要为每个逻辑表配置相关的分库分表测试。
    表规则配置类:TableRuleConfiguration。它有两个方法

  • setDatabaseShardingStrategyConfig
  • setTableShardingStrategyConfig

整体来看,shardingjdbc的api使用起来还是比较流畅的。符合工程师思考的逻辑。

04 Feed流

班级动态聚合页面,每一条Feed包含如下元素:

  • 动态内容(文本,音频,视频)
  • 前N个点赞用户
  • 当前用户是否收藏,点赞数,收藏数
  • 前N个评论

聚合首页需要显示15条首页动态列表,每条数据从数据数据库里读取,那接口性能肯定不会好。所以我们应该用缓存。那么这里就引申出一个问题,列表如何缓存?

4.1 列表缓存

列表如何缓存是我非常渴望和大家分享的技能点。这个知识点也是我 2012 年从开源中国上学到的,下面我以「查询博客列表」的场景为例。

我们先说第1种方案:对分页内容进行整体缓存。这种方案会 按照页码和每页大小组合成一个缓存key,缓存值就是博客信息列表。 假如某一个博客内容发生修改, 我们要重新加载缓存,或者删除整页的缓存。

这种方案,缓存的颗粒度比较大,如果博客更新较为频繁,则缓存很容易失效。下面我介绍下第 2 种方案:仅对博客进行缓存。流程大致如下:

1)先从数据库查询当前页的博客id列表,sql类似:

select id from blogs limit 0,10

2)批量从缓存中获取博客id列表对应的缓存数据 ,并记录没有命中的博客id,若没有命中的id列表大于0,再次从数据库中查询一次,并放入缓存,sql类似:

select id from blogs where id in (noHitId1, noHitId2)

3)将没有缓存的博客对象存入缓存中

4)返回博客对象列表

理论上,要是缓存都预热的情况下,一次简单的数据库查询,一次缓存批量获取,即可返回所有的数据。另外,关于 缓 存批量获取,如何实现?

  • 本地缓存:性能极高,for 循环即可
  • memcached:使用 mget 命令
  • Redis:若缓存对象结构简单,使用 mget 、hmget命令;若结构复杂,可以考虑使用 pipleline,lua脚本模式

第 1 种方案适用于数据极少发生变化的场景,比如排行榜,首页新闻资讯等。

第 2 种方案适用于大部分的分页场景,而且能和其他资源整合在一起。举例:在搜索系统里,我们可以通过筛选条件查询出博客 id 列表,然后通过如上的方式,快速获取博客列表。

4.2 聚合

Redis:若缓存对象结构简单,使用 mget 、hmget命令;若结构复杂,可以考虑使用 pipleline,lua脚本模式

这里我们使用的是pipeline模式。客户端采用了redisson
伪代码:

//添加like zset列表ZsetAddCommand zsetAddCommand = new ZsetAddCommand(LIKE_CACHE_KEY + feedId, spaceFeedLike.getCreateTime().getTime(), userId);
pipelineCommandList.add(zsetAddCommand);
//设置feed 缓存的加载数量
HashMsetCommand hashMsetCommand = new HashMsetCommand(FeedCacheConstant.FEED_CACHE_KEY + feedId, map);
pipelineCommandList.add(hashMsetCommand);
//一次执行两个命令
List<?> result = platformBatchCommand.executePipelineCommands(pipelineCommandList);

聚合页面查询流程:

  1. 通过classId查询feedIdList
  2. 分别通过pipeline的模式,依次获取动态, 点赞,收藏,评论数据
模块 redis存储格式
动态 HASH 动态详情
点赞 ZSET 存储userId ,前端显示用户头像,用户缓存使用string存储
收藏 string存储用户是否收藏过该feed
评论 ZSET 存储评论Id,评论详情存储在string存储

05 消息队列

我们参考阿里ons client 模仿他的设计模式,做了rocketmq的简单封装。

封装的目的在于方便工程师接入,减少工程师在各种配置上心智的消耗。

  1. 支持批量消费和单条消费;
  2. 支持顺序发送;
  3. 简单优化了rocketmq broker限流情况下,发送消息失败的场景。

写在最后

这篇文字主要和大家分享应用重构的架构设计。
其实重构有很多细节需要处理。

  1. 数据迁移方案
  2. 团队协作,新人培养
  3. 应用平滑升级

每一个细节都需要花费很大的精力,才可能把系统重构好。

feed流系统重构-架构篇相关推荐

  1. 让人欲罢不能的Feed流系统是如何设计的?

    作者:少强 原文:https://yq.aliyun.com/articles/706808?utm_content=g_1000064616 简介 差不多十年前,随着功能机的淘汰和智能机的普及,互联 ...

  2. 亿级规模的 Feed 流系统,如何轻松设计?

    阿里妹导读:互联网进入移动互联网时代,最具代表性的产品就是各种信息流,像是朋友圈.微博.头条等.这些移动化联网时代的新产品在过去几年间借着智能手机的风高速成长.这些产品都是Feed流类型产品,由于Fe ...

  3. 动动手指头, Feed 流系统亿级规模不用愁

    戳蓝字"CSDN云计算"关注我们哦! 作者 | 少强 责编 | 阿秃 导读:互联网进入移动互联网时代,最具代表性的产品就是各种信息流,像是朋友圈.微博.头条等.这些移动化联网时代的 ...

  4. 别瞎搞了!微博、知乎就是这么设计Feed流系统的~

    点击上方 好好学java ,选择 星标 公众号重磅资讯,干货,第一时间送达 今日推荐:14 个 github 项目!个人原创100W +访问量博客:点击前往,查看更多 # 简介 差不多十年前,随着功能 ...

  5. 如何设计一个超级牛牛牛逼的 Feed 流系统

    作者:少强 简介 差不多十年前,随着功能机的淘汰和智能机的普及,互联网开始进入移动互联网时代,最具代表性的产品就是微博.微信,以及后来的今日头条.快手等.这些移动化联网时代的新产品在过去几年间借着智能 ...

  6. 如何设计一个牛逼的 Feed 流系统

    点击上方"肉眼品世界",选择"设为星标" 深度价值体系传递 作者:少强 简介 差不多十年前,随着功能机的淘汰和智能机的普及,互联网开始进入移动互联网时代,最具代 ...

  7. 基于 MySQL + Tablestore 分层存储架构的大规模订单系统实践-架构篇

    简介: 本文简要介绍了基于 MySQL 结合 Tablestore 的大规模订单系统方案.这种方案支持大数据存储.高性能数据检索.SQL搜索.实时与全量数据分析,且部署简单.运维成本低. 作者 | 弘 ...

  8. 朋友圈不知你看到的那么简单,千万Feed流系统的存储技术解密

    摘要:阿里巴巴高级技术专家木洛在2018云栖大会·深圳峰会中就Feed流的概念介绍.概念架构以及TableStore场景的Timeline模型等方面的内容做了深入的分析.本文带领大家一起了解并学习如何 ...

  9. 如何打造千万级Feed流系统

    摘要: Feed流是一个目前非常常见的功能,在众多产品中都有展现,通过Feed流可以把动态实时的传播给订阅者,是用户获取信息流的一种有效方式.在大数据时代,如何打造一个千万级规模的Feed流系统仍然是 ...

最新文章

  1. 并发编程之 Java 内存模型 + volatile 关键字 + Happen-Before 规则
  2. JS子元素oumouseover触发父元素onmouseout
  3. java wifimanager_Java WifiManager.disableNetwork方法代碼示例
  4. 在知乎引发众多分布式数据库大佬争相回答的问题
  5. 大学生计算机大赛课题,第14届中国大学生计算机设计大赛云南赛区决赛举行 32个项目胜出...
  6. Java期末设计(十三周)
  7. 鸿蒙曰意心养翻译,文言文情话及翻译
  8. java请输入第一个人,Java-每日编程练习题③
  9. 手机罗盘(指南针)校准方法
  10. flask制作电影天堂的API接口
  11. linux桌面 任务栏,状态栏消失恢复
  12. 跳槽or裸辞?2022年真不建议···
  13. 想参加多人运动?并行流(ParallelStream)模式教你成为时间管理大师
  14. 无人机坐标系定义与转换
  15. 十道简单算法题二【Java实现】
  16. 计算机桌面进入安全模式,电脑如何进入安全模式呢?
  17. 使用JavaScript动态添加HTML语句后,事件失效的解决办法
  18. Redis分布式锁失效场景分析
  19. android qq账号登陆验证手机号码,qq绑定的手机号换了,登陆需要手机验证,怎么办?...
  20. 每日一解 电话号码的字母组合

热门文章

  1. 离零服务费再进一步 摩根大通推出最低收费美股ETF
  2. 线程池之 ScheduledThreadPoolExecutor中scheduleAtFixedRate执行定时任务
  3. 67家基金子公司背景脉络梳理
  4. iView中Message与Notice消息提示警告内容进行换行
  5. 3857墨卡托坐标系转换为4326 (WGS84)经纬度坐标
  6. 强势回归,Linux blk用实力证明自己并不弱!
  7. MapReduce和sparks运行wordcount案例过程分析
  8. Python之Unicode编码
  9. 制作水果忍者-JS-2
  10. [年终总结]过去,现在,未来