第8章通用计数系统——8.2 如何存储计数数据

8.2 如何存储计数数据

下面通过计数数据的特点,详细介绍为什么计数要单独存储,以及如何进行存储选型。

8.2.1 计数数据的特点

计数数据一般具有如下特点。

  • 读请求量巨大:无论是作品维度的计数,还是用户维度的计数,都有较大的访问量,所以计数的展示需要支持高并发读取。
  • 写请求量巨大:对于点赞、点踩、评论、转发、收藏等这些场景,由于用户触发动作的成本很低,计数会以较高的并发量增加,所以计数的增加需要支持高并发写入。
  • 非产品的绝对强依赖:与用户钱包账户余额数据不同,具有累计性质的计数数据是否能正常展示通常不会影响用户使用产品的功能。在极端情况下,如发生网络抖动、服务Bug、请求击垮机房、机房故障等,计数作为一种纯粹的加成效果,在无法展示时不会过于影响用户的心情。
  • 对数据的精确性要求与数值的增加成反比:例如,当点赞数少于一定的数量时,用户会非常在意点赞数量并查看有哪些人为他点赞了;但是当点赞数已经达到千或万级别时,用户对点赞总数的关注会变得更加粗糙,不会再关注点赞数个位数的增加情况,而是更关注点赞数的级别跃迁,如点赞数从10000个增加到20000个。大部分用户产品也是按照这样的数据规律来做设计的 ,在计数达到一定的级别后会改为模糊化表示。例如,当点赞数为1090个时会显示1100,当点赞数达到12345个时会显示12000。即计数值越大,对数据的精确性要求越低。

8.2.2 关系型数据库的困境

以作品点赞数为例:某作品被哪些用户点赞过,会以点赞记录的形式被存储到关系型数据库MySQL中。为了获取此作品的点赞总数,我们需要执行SQL count(1)语句。这种方式简单粗暴。这条SQL语句在不同MySQL存储引擎中的实现方式不同,所以执行效率也不同。

  • MylSAM引擎把一个表的总行数存储在磁盘上,因此在执行count(1)时会直接返回这个数,效率很高。

  • InnoDB引擎在执行count(1)时,需要把数据一行行地从引擎中读出来,然后进行累计计数。

但是由于InnoDB提供了较好的事务保证,在绝大多数业务场景中,MySQL都会默认选择InnoDB作为存储引擎。对于点赞数较少的作品,count(1)仅涉及扫描较少的数据,使用这种方式获取总数问题不大;但是对于点赞数成千上万的作品(这样的作品并不在少数),count(1)会扫描MySQL中的所有点赞记录行,这会对MySQL的性能造成较大的影响。试想一下,我们仅仅想获取一个数据的总数而已,但是为了这个总数我们必须要关心每一条数据记录,这逻辑本身就很笨重。所以,依赖统计记录总数的形式来展示计数数据的做法被普遍认为是不可取的。

既然通过统计数据记录的总数来获取计数很低效,那么像MylSAM引擎一样把计数单独存储到一个数据表中是不是就行了?这种做法依然是不可取的。因为计数类场景的写请求量巨大,让关系型数据库来承载这样的高并发写请求风险极大。

8.2.3 是否要使用关系型数据库

通常来说:

  • 如果关系型数据库无法应对写压力,则可以使用异步方式对数据进行更新:先更新缓存数据,再使用消息队列异步通知关系型数据库进行数据更新;
  • 如果关系型数据库无法应对读压力,则可以使用缓存对外提供服务。

对于大部分业务场景,这样的架构设计没有什么问题。不过,计数的读/写请求量巨大,几乎任何时间都在与缓存系统直接交互,在缓存系统的下一层再维护一份关系型数据库有点儿多此一举;而且,计数数据一般不是绝对不可丢失的数据,它可以由数据记录流水总数反向推出。以笔者的观点,对于计数数据,我们干脆将内存型数据库如Redis直接作为数据存储系统,不再使用关系型数据库。这样一来,计数系统不仅依然可以满足高并发的服务能力要求,而且减少了非必要的数据存储,整体架构会变得异常简洁,可维护性大大增加。

8.2.4 使用Redis存储计数数据

熟悉Redis的读者都知道,Redis本身提供了INCRBY、DECRBY、HINCRBY等增量操作命令来进行数字的增减,我们可以很方便地使用Redis来维护计数值,Redis在计数场景中有天然的易用性。

而在高并发请求方面,由于Redis是一个基于内存的单进程Reactor高性能服务器,所以其本身对高并发的读/写请求有较好的支持;而且,业界有很多成熟的Redis分布式集群架构(如Codis),可以提供较好的高可用性和可扩展性,能够很好地支持海量用户请求。

Redis是内存型数据库,一般用于缓存系统,不太在意数据的丢失。但是,计数服务选型Redis并不表示我们完全不在意数据的丢失,计数服务反而是把Redis视为数据的最终存储层来使用的,所以我们需要格外考虑数据丢失问题。幸运的是,Redis提供了比较成熟的持久化方案,包括RDB持久化和AOF持久化。

  • RDB持久化:将 Redis内存数据内容以文件的形式保存到磁盘上,这个文件就是RDB文件,它是经过压缩的二进制文件,小巧而紧凑。Redis内存型数据库可以被转换为RDB文件,RDB文件也可以被转换为内存型数据库。Redis服务器会周期性地将数据持久化到RDB文件中。
  • AOF持久化:RDB持久化是把数据库内容写到RDB文件中,而AOF持久化是通过把写Redis数据库的命令保存到AOF文件中来实现的,AOF持久化对增量数据有天然的良好支持。如果Redis启用了AOF持久化功能,那么Redis服务器在结束一次事件循环之前,都会调用flushAppendOnlyFile函数将AOF缓冲区的内容全部写入AOF文件,并决定是否进行AOF文件的同步。

具体来说,flushAppendOnlyFile函数将检查Redis服务器配置项appendfsync的值。

  • always:将命令写入文件缓冲区,并调用 fsync函数把文件缓冲区的数据刷写到AOF文件中,效果是将数据同步写到AOF文件中。
  • everysec:将命令写入文件缓冲区;如果此时距离上次同步刷写的时间超过1s,则再将文件缓冲区的数据全部刷写到AOF文件中,效果是每1s将数据同步写到AOF文件中。
  • no:仅将命令写入文件缓冲区,不同步刷写AOF文件,而是交给操作系统来适时执行刷写。一般操作系统会在文件缓冲区满了以后刷写文件,同时也会周期性地执行刷写文件。

无论是何种配置,只要打开AOF开关,就一定会将命令写入文件缓冲区,Redis配置只决定以何种方式将数据同步刷写到AOF文件中。

使用周期性RDB持久化配合everysec配置的AOF持久化方式,可以防止绝大部分情 况下Redis数据的丢失。在最极端的情况下,虽然可能有1s的数据丢失,但是其概率在我们可承受的范围内,而且我们可以很容易地进行数据修复,比如以异步检查的形式,周期性地使用与计数相关的数据记录总行数来修正丢失的计数,或者人工介入。

总结

计数数据一般具有哪些特点?

  • 读请求量巨大:无论是作品维度的计数,还是用户维度的计数,都有较大的访问量,所以计数的展示需要支持高并发读取。
  • 写请求量巨大:对于点赞、点踩、评论、转发、收藏等这些场景,由于用户触发动作的成本很低,计数会以较高的并发量增加,所以计数的增加需要支持高并发写入。
  • 非产品的绝对强依赖:与用户钱包账户余额数据不同,具有累计性质的计数数据是否能正常展示通常不会影响用户使用产品的功能。在极端情况下,如发生网络抖动、服务Bug、请求击垮机房、机房故障等,计数作为一种纯粹的加成效果,在无法展示时不会过于影响用户的心情。
  • 对数据的精确性要求与数值的增加成反比:例如,当点赞数少于一定的数量时,用户会非常在意点赞数量并查看有哪些人为他点赞了;但是当点赞数已经达到千或万级别时,用户对点赞总数的关注会变得更加粗糙,不会再关注点赞数个位数的增加情况,而是更关注点赞数的级别跃迁,如点赞数从10000个增加到20000个。大部分用户产品也是按照这样的数据规律来做设计的 ,在计数达到一定的级别后会改为模糊化表示。例如,当点赞数为1090个时会显示1100,当点赞数达到12345个时会显示12000。即计数值越大,对数据的精确性要求越低。

什么是RDB持久化和AOF持久化?

  • RDB持久化:将 Redis内存数据内容以文件的形式保存到磁盘上,这个文件就是RDB文件,它是经过压缩的二进制文件,小巧而紧凑。Redis内存型数据库可以被转换为RDB文件,RDB文件也可以被转换为内存型数据库。Redis服务器会周期性地将数据持久化到RDB文件中。
  • AOF持久化:RDB持久化是把数据库内容写到RDB文件中,而AOF持久化是通过把写Redis数据库的命令保存到AOF文件中来实现的,AOF持久化对增量数据有天然的良好支持。如果Redis启用了AOF持久化功能,那么Redis服务器在结束一次事件循环之前,都会调用flushAppendOnlyFile函数将AOF缓冲区的内容全部写入AOF文件,并决定是否进行AOF文件的同步。

AOF同步策略?

具体来说,flushAppendOnlyFile函数将检查Redis服务器配置项appendfsync的值。

  • always:将命令写入文件缓冲区,并调用 fsync函数把文件缓冲区的数据刷写到AOF文件中,效果是将数据同步写到AOF文件中。
  • everysec:将命令写入文件缓冲区;如果此时距离上次同步刷写的时间超过1s,则再将文件缓冲区的数据全部刷写到AOF文件中,效果是每1s将数据同步写到AOF文件中。
  • no:仅将命令写入文件缓冲区,不同步刷写AOF文件,而是交给操作系统来适时执行刷写。一般操作系统会在文件缓冲区满了以后刷写文件,同时也会周期性地执行刷写文件。

怎么使用RDB持久化和AOF持久化?

  • 使用周期性RDB持久化配合everysec配置的AOF持久化方式,可以防止绝大部分情 况下Redis数据的丢失。
  • 在最极端的情况下,虽然可能有1s的数据丢失,但是其概率在我们可承受的范围内,而且我们可以很容易地进行数据修复,比如以异步检查的形式,周期性地使用与计数相关的数据记录总行数来修正丢失的计数,或者人工介入。