这将是关于MySQL数据库的优化。
这是在我们制作了电子邮件通讯系统时发生的。 我们的系统应该每天发送数千万封信件。 尽管一切看起来都很原始,但发送一封信并非易事:
- 从html广告素材收集一封信函,以替代个性化数据。
- 添加消息查看像素,用您自己的消息替换所有链接-跟踪点击。
- 在发送前检查电子邮件是否不在黑名单中。
- 发送电子邮件到特定的池。
我将详细介绍第二段:
微服务邮件生成器正在准备发送一封信:
- 在信件中找到所有链接;
- 每个链接生成唯一的32个字符的uuid;
- 用新链接替换原始链接,并将数据保存在数据库中。
因此,所有源链接将被uuid替换,并且域将更改为我们的域。 当您使用此链接收到GET请求时,我们将代理原始图像或重定向到原始链接。 保存发生在MySQL数据库中,我们将生成的uuid与原始链接以及一些元信息(用户电子邮件,邮件ID和其他数据)一起保存。 非规范化可以帮助我们在一个请求中获取所有必要的数据以保存统计信息,或启动某种触发链。
问题编号1
我们中uuid的生成取决于时间戳。
由于邮件通常发生在一定的时间段内,并且启动了许多用于组装字母的微服务实例,因此事实证明某些uuid非常相似。
这给出了低选择性。 UPD:因为数据相似,所以使用双树不是很有效。
我们使用没有时间依赖性的python中的uuid模块解决了此问题。
这种隐含的事情降低了索引的速度。
储存情况如何?
该表的结构如下:
CREATE TABLE IF NOT EXISTS `Messages` ( `UUID` varchar(32) NOT NULL, `Message` json NOT NULL, `Inserted` DATE NOT NULL, PRIMARY KEY (`UUID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在创建时,一切看起来都很合理:
UUID是主键,也是聚簇索引。 在此字段上进行选择时,我们只需选择记录,因为所有值都存储在此处。 这是一个故意的决定。
了解有关聚簇索引的更多信息。一切都很棒,直到桌子长大为止。
问题编号2
如果您了解有关群集索引的更多信息,则可以了解以下细微差别:
当向表中添加新行时,它不会被添加到文件末尾,未添加到平面列表的末尾,而是通过排序添加到与之对应的树结构的所需分支中。
因此,随着负载增加,插入时间增加。
解决方案是使用不同的表结构。
CREATE TABLE IF NOT EXISTS `Messages` ( `ID` int(11) NOT NULL AUTO_INCREMENT, `UUID` varchar(32) NOT NULL, `Message` json NOT NULL, `Inserted` DATE NOT NULL, PRIMARY KEY (`ID`), UNIQUE KEY `UUID` (`UUID`, `Inserted`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
由于主键现在是自动递增的,并且mysql存储了最后一个插入位置的缓存,因此现在插入总是在末尾进行,即Innodb已针对写入顺序递增的值进行了优化。
我在
postgres源代码中找到了此优化的详细信息
。 Mysql实现了非常相似的优化。
当然,我必须添加一个唯一的密钥,以便没有冲突,但是我们提高了插入速度。
随着基础的增长,我们考虑删除旧数据。 在Inserted字段上使用DELETE绝对不是最佳选择-这是一个很长的时间,并且只有在我们执行
optimize table命令之后,空间才会被释放。 顺便说一下,此操作完全阻塞了表-根本不适合我们。
因此,我们决定将表拆分为多个分区。
1天-1个分区,旧分区会在时间到时自动删除。
问题编号3
我们有机会删除旧数据,但没有机会从所需的分区中进行选择,因为使用select`e我们仅指定uuid,mysql不知道我们应该在哪个分区中寻找并且正在寻找所有分区。
该解决方案源于问题#1-向生成的uuid添加时间戳。 只是这次不同了:在行中的任意位置而不是在开始或结束处插入时间戳。 他们添加
了破折号之前和之后,以便可以使用正则表达式获得它。
通过这种优化,我们能够获得生成uuid的日期,并且已经进行选择以指示“插入”字段的特定值。 现在,我们立即从所需的分区中读取数据。
另外,由于
有了诸如
ROW_FORMAT = COMPRESSED之类的东西并将编码更改为
latin1 ,我们节省了硬盘驱动器上更多的空间。