系统改造的时候,首先应该进行数据表的梳理。因为随着业务需求的不断增加,往往会出现为了更方便等情况,导致的实体表字段过多、表查询维度和用途多样、表之间关系混乱等情况。
或者按照领域驱动设计的思路,应该先确定服务界限,从上到下依次具体,这个时候或许是根据业务情况重新设计数据表,而不是根据当前的数据表进行改造。

1 用户中心

用户中心的主要功能是维护用户信息、用户权限和登录状态,它保存的数据大部分都属于读多写少的数据。用户中心常见的优化方式主要是将用户中心和业务彻底拆开,不再与业务耦合,并适当增加缓存来提高系统性能。
400
除了通过精简表的职能来提高表的性能和维护性外,我们还可以针对不同类型的表做不同方向的缓存优化,如下图用户中心表例子:
400
数据主要有四种:实体对象主表、辅助查询表、实体关系和历史数据,不同类型的数据所对应的缓存策略是不同的,如果我们将一些职能拆分不清楚的数据硬放在缓存中,使用的时候就会碰到很多烧脑的问题。
我之前就碰到过这样的错误做法——将用户来访记录这种持续增长的操作历史放到缓存里,这个记录的用途是统计有多少好友来访、有多少陌生人来访,但它同时保存着和用户是否是好友的标志。这也就意味着,一旦用户关系发生变化,这些历史数据就需要同步更新,否则里面的好友关系就“过时”了。
400
将历史记录和需要实时更新的好友状态混在一起,显然不合理。如果我们做归类梳理的话,应该拆分成三个职能表,分别进行管理:

  • 历史记录表,不做缓存,仅展示最近几条,极端情况临时缓存;
  • 好友关系(缓存关系,用于统计有几个好友);
  • 来访统计数字(临时缓存)

但是业务除了按 ID 查找外,还有一些需要通过组合条件查询的,比如:

  • 在 7 月 4 日下单购买耳机的订单有哪些?
  • 天津的用户里有多少新注册的用户?有多少老用户?
  • 昨天是否有用户名前缀是 rick 账户注册?

这种根据条件查询统计的数据是不太容易做缓存的,因为高并发服务缓存的数据通常是能够快速通过 Hash 直接匹配的数据,而这种带条件查询统计的数据很容易出现不一致、数据量不确定导致的性能不稳定等问题,并且如果涉及的数据出现变化,我们很难通过数据确定同步更新哪些缓存。
因此,这类数据只适合存在关系数据库或提前预置计算好结果放在缓存中直接使用,做定期更新。
除了组合条件查询不好缓存外,像 count() 、sum() 等对数据进行实时计算也有更新不及时的问题,同样只能定期缓存汇总结果,不能频繁查询。所以,我们应该在后续的开发过程中尽量避免使用数据库做计算。

感觉这一个栏目的内容,更偏向于实际使用时的设计问题,具体该如何实现。
其他的架构栏目,比较适合拓展广度,知道有哪些方法。
这个系统实战栏目,比较适合拓展深度,去真的设计一个高效的系统,要考虑到缓存中 KEY 的设计。

当我们拿它查询用户昵称中是否有“极客”两个字的时候,需要做很多额外的工作,需要对“用户昵称”这个字段增加索引,同时这种 like 查询会扫描全表数据进行计算。
如果这种查询的频率比较高,就会严重影响其他用户的登陆,而且新增的昵称索引还会额外降低当前表插入数据的性能,这也是为什么我们的后台系统往往会单独分出一个从库,做特殊索引。
一般来说,高并发用缓存来优化读取的性能时,缓存保存的基本都是实体数据。那常见的方法是先通过“key 前缀 + 实体 ID”获取数据(比如 user_info_9527),然后通过一些缓存中的关联关系再获取指定数据,比如我们通过 ID 就可以直接获取用户好友关系 key,并且拿到用户的好友 ID 列表。
我们整理实体表的核心思路主要有以下几点:

  • 精简数据总长度;
  • 减少表承担的业务职能;
  • 减少统计计算查询;
  • 实体数据更适合放在缓存当中;
  • 尽量让实体能够通过 ID 或关系方式查找;
  • 减少实时条件筛选方式的对外服务。

下面我们继续来看另外三种表结构,你会发现它们不太适合放在缓存中,因为维护它们的一致性很麻烦。

实体辅助表

为了精简数据且方便管理,我们经常会根据不同用途对主表拆分,常见的方式是做纵向表拆分
纵向表拆分的目的一般有两个,一个是把使用频率不高的数据摘出来。常见主表字段很多,经过拆分,可以精简它的职能,而辅助表的主键通常会保持和主表一致或通过记录 ID 进行关联,它们之间的常见关系为 1:1。
而放到辅助表的数据,一般是主要业务查询中不会使用的数据,这些数据只有在极个别的场景下才会取出使用,比如用户账号表为主体用于做用户登陆使用,而辅助信息表保存家庭住址、省份、微信、邮编等平时不会展示的信息。
辅助表的另一个用途是辅助查询,当原有业务数据结构不能满足其他维度的实体查询时,可以通过辅助表来实现。
比如有一个表是以“教师”为主体设计的,每次业务都会根据“当前教师 ID+ 条件”来查询学生及班级数据,但从学生的角度使用系统时,需要高频率以“学生和班级”为基础查询教师数据时,就只能先查出 “学生 ID”或“班级 ID”,然后才能查找出老师 ID”,这样不仅不方便,而且还很低效,这时候就可以把学生和班级的数据拆分出来,额外做一个辅助表包含所有详细信息,方便这种查询。
另外,我还要提醒一下,因为拆分的辅助表会和主体出现 1:n 甚至是 m:n 的数据关系,所以我们要定期地对数据整理核对,通过这个方式保证我们冗余数据的同步和完整。
不过,非 1:1 数据关系的辅助表维护起来并不容易,因为它容易出现数据不一致或延迟的情况,甚至在有些场景下,还需要刷新所有相关关系的缓存,既耗时又耗力。如果这些数据的核对通过脚本去定期执行,通过核对数据来找出数据差异,会更简单一些。
此外,在很多情况下我们为了提高查询效率,会把同一个数据冗余在多个表内,有数据更新时,我们需要同步更新冗余表和缓存的数据。
这里补充一点,行业里也会用一些开源搜索引擎,辅助我们做类似的关系业务查询,比如用 ElasticSearch 做商品检索、用 OpenSearch 做文章检索等。这种可横向扩容的服务能大大降低数据库查询压力,但唯一缺点就是很难实现数据的强一致性,需要人工检测、核对两个系统的数据。

实体关系表

接下来我们再谈谈实体之间的关系。
400
在对 1:n 或 m:n 关系的数据做缓存时,我们建议提前预估好可能参与的数据量,防止过大导致缓存缓慢。同时,通常保存这个关系在缓存中会把主体的 ID 作为 key,在 value 内保存多个关联的 ID 来记录这两个数据的关联关系。而对于读取特别频繁的的业务缓存,才会考虑把数据先按关系组织好,然后整体缓存起来,来方便查询和使用。
需要注意的是,这种关联数据很容易出现多级依赖,会导致我们整理起来十分麻烦。当相关表或条件更新的时候,我们需要及时同步这些数据在缓存中的变化。所以,这种多级依赖关系很难在并发高的系统中维护,很多时候我们会降低一致性要求来满足业务的高并发情况。
现在我们简单总结一下,到底什么样的数据适合做缓存。一般来说,根据 ID 能够精准匹配的数据实体很适合做缓存;而通过 String、List 或 Set 指令形成的有多条 value 的结构适合做(1:1、1:n、m:n)辅助或关系查询;最后还有一点要注意,虽然 Hash 结构很适合做实体表的属性和状态,但是 Hgetall 指令性能并不好,很容易让缓存卡顿,建议不要这样做。

动作历史表

介绍到这里,我们已经完成了大部分的整理,同时对于哪些数据可以做缓存,你也有了较深理解。为了加深你的印象,我再介绍一些反例。
一般来说,动作历史数据表记录的是数据实体的动作或状态变化过程,比如用户登陆日志、用户积分消费获取记录等。这类数据会随着时间不断增长,它们一般用于记录、展示最近信息,不建议用在业务的实时统计计算上。
所以,对于这种基于大量的数据统计后才能得到的结论数据,我不建议对外提供实时统计计算服务,因为这种查询会严重拖慢我们的数据库,影响服务稳定。即使使用缓存临时保存统计结果,这也属于临时方案,建议用其他的表去做类似的事情,比如实时查询领取记录表,效果会更好。

这种和 B 站中每个视频的播放量、点赞量相似,没办法也没必要做到强一致性,最终一致性就可以。

一般来说,数据可分为四类:实体表、实体辅助表、关系表和历史表,而判断是否适合缓存的核心思路主要是以下几点:

  • 能够通过 ID 快速匹配的实体,以及通过关系快速查询的数据,适合放在长期缓存当中;
  • 通过组合条件筛选统计的数据,也可以放到临时缓存,但是更新有延迟;
  • 数据增长量大或者跟设计初衷不一样的表数据,这种不适合、也不建议去做做缓存。

课后问题

用户邀请其他用户注册的记录,属于历史记录还是关系记录?
课后某人的设计:
400
老师的补充:当我们需要查询这个用户的邀请人时,还需要查询这个表,那么和我们对表的职能拆分有一定冲突,那么如何改进这个实现呢?
思考了下作为关系表也是可以的
在满足下面的注册邀请前提下:
1.邀请人用类似二维码分享方式,注册人主动扫码注册。(不使用点对点邀请,被邀请人可能不接受)
2.只能注册成功一次
这样每一条邀请记录都是一个用户的注册记录:可以定义如下字段
(邀请者,注册人,注册时间,邀请方式)
表的字段结构都非常简单,记录的总量最多就是账号量,并不会随时间不断膨胀。因此可以胜任关系表的查询需求。
看业务场景吧:
1,如果需要查看邀请人的邀请记录,邀请成功或者邀请未成功的记录,此时需要设计为邀请记录表吧
2,如果不需要查看邀请人记录,只关注成功邀请的话,而且一个人只能被邀请一次的话,可以设计为关系表
这个不同的业务需求划分归属就会不同吧,比如邀请记录不会发生变化且数据量庞大,对于历史记录访问较少,这些都属于冷数据,它应该作为历史记录,如果属于访问较平凡的数据,考虑一下分区,并且储存较少的字段,统计邀请人id和被邀请人id等少量信息,作为关系表存在

看了这些回答后,有点分不清“关系表”和“历史表”的区别在于什么地方。