3.2 北冥有鱼,其名为鲲

北冥有鱼,其名为鲲,鲲之大,一锅炖不下。

数据库最怕什么,怕大。

单表过大会导致数据库需要花更多的时间来扫描、过滤和排序大量数据,查询和写入操作变得缓慢。而且还需要大量的内存来缓存数据以支持高效的查询。同时也会增加索引的维护成本,因为每次写入操作都可能导致索引的更新。

在InnoDB存储引擎中,text字段通常不会与其他列存储在同一数据页上。InnoDB引擎采用了一种称为"行格式"的方式来组织数据,并将大文本textblob字段存储在单独的页中,而不是与行的其他列存储在同一数据页上。

这里就带来了2个知识点:

  1. 在查询数据时,尽量只查询必要的字段,如无必要尤其避开text。例如:

    SELECT `prompt_id`, `title`, `creator_id`, `usage_count`
    FROM `prompts`
    WHERE `status` = 1
  2. 如果该部分的数据不需要作为查询条件,则完全可以不放在关系型数据库中。

接下来,我们就针对第二种情况进行展开。

3.2.1 Redis的持久存储

Redis通常被用作内存数据库和缓存,但它也可以作为持久存储来满足某些需求。

我们之所以需要将它作为持久存储来使用,主要是因为它跟MySQL相比,有着天然的优势:

  1. 速度:Redis使用内存存储数据,访问速度远快于MySQL等磁盘存储系统。即便Redis也支持持久化,通过将内存中的数据不定期写入磁盘或者使用AOF(Append Only File)方式记录每次写入,它的操作速度仍然比传统数据库快得多。

  2. 数据结构:Redis支持字符串、哈希、列表、集合、有序集合等多种数据结构,而MySQL主要支持表格形式的数据结构。对于需要高效进行复杂数据操作的场景,如计数器、队列、排行榜等,Redis能提供更加灵活和快速的处理方式。

  3. 简化设计:由于Redis的数据结构丰富,我们可以用更简洁的数据模型来解决问题,减少了像在MySQL中那样需要进行复杂的表连接和多次查询的情况。

  4. 可扩展性:Redis原生支持主从复制、哨兵模式和集群,可以较容易地通过增加更多节点来实现水平扩展,而对于MySQL,尤其是在读写分离和分片方面,扩展往往更加复杂。

  5. 操作灵活性:Redis的操作通常是原子性的,这为复杂的数据操作提供了便利。例如,我们可以在一条命令中对多个键进行操作,而在MySQL中可能需要多次查询和写入。

而在我们的项目中,将不需要模糊查询的长字符串字段,拆分存储到使用AOF(Append Only File)方式的Redis中,实在是太合适不过了。 (这里假设业务场景中,不存在通过一个列表返回包含提示词全文的情况,因为这样的返回的字符长度通常会非常大,我们也会在3.4中继续讨论业务场景合理性的问题。)

  1. 首先,便宜啊!MySQL对机器的要求可能会高一些,而Redis呢?只要有内存有硬盘,不管多低配置的机器,都能运行,占用资源少,性价比高。当项目规模不大时,哪怕是在云服务器上,也能很方便很轻松的和项目代码放在同一台服务器上,仅需要消耗很少额外的资源。

  2. 其次,高可扩展性。如果我们业务增长非常快,迁移Redis和垂直分片非常方便,而且还有Redis Cluster和Twemproxy也能很方便的将多个Redis实例组合成一个逻辑数据库,并提供高可用性和水平扩展。

  3. 再次,简单的配置和部署。从一个管理者的角度来看,我很喜欢学习曲线平滑的产品,这样团队里很多人都能快速上手这些产品的维护工作,不会出现所有的生产环境故障都需要运维人员处理。

  4. 最后,从业务角度讲,用Redis存放的类似提示词正文的字段,它通常不需要在列表中被查询,如果通过列表返回大量的长数据的话,则会带来传输延迟,让用户体验变差(如点击链接后加载时间过过长)。如果是在移动网络环境下,可能会导致用户的网络费用增加。同时,也会对服务器和网络设施造成较大的压力。

那么什么是Redis的AOF?

Redis的AOF(Append Only File)是一种用于数据持久化的机制,它记录了执行过的所有写操作命令,并以Redis协议的格式追加存储到AOF文件的末尾。AOF持久化旨在为Redis提供一种更强的数据持久性保证。

工作原理: 当AOF持久化开启时,Redis会将每个写操作命令追加到AOF文件中。这个过程分为三个步骤:

  1. 命令追加:客户端发出的写命令被追加到AOF缓冲区中。

  2. 文件同步:根据配置,Redis会定期将缓冲区的命令同步到磁盘上的AOF文件中。同步频率可以是每个命令、每秒或者完全由操作系统决定。

  3. 文件重写:为了防止AOF文件无限增长,Redis提供了AOF重写的机制,这个过程会生成一个新的AOF文件,只包含最小的命令集合来重建当前的数据库状态。

AOF与RDB的区别: Redis还提供了另一种持久化机制,叫做RDB(Redis Database)。与AOF相比,RDB会在指定的时间间隔生成数据集的快照。AOF则是记录下所有的写操作,可以提供更精确的数据恢复能力。

显然我们作为非关系型数据库而不是缓存的方式来使用Redis,必须采用AOF,否则会导致数据的丢失。

RDB通过创建数据集的快照来持久化数据。

  • 如果Redis设置为每隔一定时间间隔保存一次快照,那么在两次快照之间的数据更新在发生故障时会丢失。

  • 如果在快照保存过程中发生崩溃,新的快照可能不会被保存,这也会导致自上次快照以来的所有数据变更丢失。

  • 如果在写快照到磁盘的过程中,系统崩溃或者Redis进程崩溃,可能导致快照文件不完整。

AOF每次写入都会操作磁盘,但是为了业务数据的完整性,这些性能开销是不能省的,相比MySQL对于磁盘的操作,不用维护索引的Redis显然更节约资源。

3.2.2 Redis使用细节

讨论任何一个技术的前提,都必须是在一个具体的场景下。

因此,我们这一章节来讨论下,什么情况下合适,什么情况下不合适。

3.2.2.1 什么场景下使用性价比高

我们在进行技术栈的选择前,首先是要分析业务场景,比如我们在预估redis存储哪些数据(提示词等无需检索的高频使用数据)。接下来就要预估这些数据有多大。 提示词有长有短,有中文有英文可能还有符号。

在Go语言中,字符串默认以UTF-8格式编码。UTF-8是一种可变长度的字符编码方式,可以使用1到4个字节表示一个Unicode码点。在UTF-8编码中:

  • 一个在ASCII中的字母,比如英文字母和数字,占用1个字节。

  • 一个中文字符通常占用3个字节。

在Redis中所占用的空间也是如此,那么以一条常规的提示词字数来看,通常是几百字,中英文情况各异,不排除有些特别长的。但是Redis一条记录最长支持512M,因此我们完全不用担心长度不够。

这里顺便在讲下MySQL的text类型:

  • TINYTEXT: 最大长度为 255 字节

  • TEXT: 最大长度为 65,535 字节(约64KB)

  • MEDIUMTEXT: 最大长度为 16,777,215 字节(约16MB)

  • LONGTEXT: 最大长度为 4,294,967,295 字节(约4GB)

当然这只是理论上可以存储大量的文本数据,但实际操作中可能会受到其它因素的限制,比如服务器的可用内存、磁盘空间、以及配置的最大包长度(max_allowed_packet)等。如果尝试存储的数据大小超过了 max_allowed_packet 的设置值,MySQL将会报错。所以,在实际应用中,还需要考虑这些实际操作环境的限制因素。

话说回来,我们的实际业务里,一条数据平均下来应该能在2KB以内。 那么即使我们的数据能达到1000万条,那么这个数据也只有不到20GB,而一个这种Redis单节点的成本每个月仅需要300美金。 显然只需要这么少的代价,就能让核心业务的长字符串全部在内存中加速读取,性价比非常高。

如果数据量更大,与其考虑增加单节点的内存,不如拆分成新的节点,通过集群的方式增加效率和可用性。

3.2.2.2 如何高效的使用和管理Redis数据

在Redis中组织数据时,有一些规则和原则可以帮助我们合理、高效地管理数据:

  1. 使用有意义的键名: 给键(key)分配有意义的名称,以便更容易理解和维护。例如,如果我们存储用户数据,可以使用像"user:123"这样的键名来表示用户123的数据。

  2. 使用命名空间: 使用命名空间来组织键名,以避免键名冲突。例如,为了将用户数据和订单数据区分开,我们可以使用"user:123"和"order:456"这样的键名。

  3. 使用合适的数据结构: 根据数据的性质和访问模式,选择合适的数据结构。Redis支持字符串、列表、集合、有序集合、哈希表等多种数据结构,每种结构都有其特定的用途。

  4. 批量操作: 尽量减少单个请求和单个键的操作,而是使用Redis提供的批量操作来提高性能。例如,使用MSET一次性设置多个键值对,而不是多次调用SET

  5. 利用数据过期: 使用Redis的过期功能,为数据设置生存时间,以确保数据不会永久存储在内存中。这对于缓存和短期数据非常有用。(后期我们会在社区互动的章节中聊到具体的使用细节)

  6. 考虑数据序列化: 如果需要存储非字符串类型的数据,可以考虑使用数据序列化方法(如JSON、MessagePack等)将数据转换为字符串,然后存储在Redis中。虽然像PHP有serialize这样的方法,但是我仍然更建议使用JSON格式,因为在我们的常用项目中,如无必要甚至可以在后端不经过处理的情况下,直接返回给前端,减少服务端工作。

  7. 合理使用分布式数据结构: 如果我们需要在多个客户端之间共享数据或实现分布式锁等功能,可以使用Redis提供的分布式数据结构(如分布式锁、分布式队列)。

  8. 合理设置内存策略:maxmemory选项下设置合理的内存限制和内存策略,以控制内存使用。

这些规则和原则有助于确保我们的Redis数据库在组织和管理数据时保持整洁、高效,并且易于维护。具体的组织方式应根据我们的应用需求和数据模型来进行选择。

现在我们一条数据,既有通过MySQL存储的需要索引和查询的部分,也有保存在内存中的部分。我们要如何简化业务代码,让可读性和复用性提高?别急,我们在后续的经典架构模式章节会阐述如何让查询过程向业务透明,让业务逻辑不用考虑数据的查询。

Last updated