[编程技术] 后端面试必备 - MySQL更新优化的几个阶段
作者:CC下载站 日期:2022-04-29 13:00:51 浏览:33 分类:编程开发
背景
现在的互联网大厂,都经历了数据量和访问量从零到亿级别的飞速增长。在这业务增长的过程中,也会面临很多技术的重构与优化,来支撑业务的快速扩张。
这篇文章,我就通过我们团队的经历,讲一下我们在业务量飞速增长的过程中,我们数据的更新优化的几个阶段。也许很多更新优化你都听说过,这里记录我们完整的优化过程,以及我们的思考。
前面几个阶段比较常见,最后一个优化的阶段是我们做的一个大胆的尝试,最终事实证明了,我们最后的优化帮我们节省了80%的DB资源,也帮我们安全度过了一个个大促活动。
零、初始版本
这是初始版本,假设你们公司要做一个电商网站,需要支持账号注册/登录
。
那我们最先考虑的就是有一张用户表
储存用户相关的基本信息,我们叫它user_tab
。
有了这个用户表,那我们网站的用户注册登录的基础信息就可以保存了。在网站的初始阶段,由于用户和访问量都不多,一切都很平稳。
一、加缓存
然后由于业务发展比较好,用户量和访问量达到了一定的规模,那么用户查询数据库就成了性能瓶颈。这个时候通常的做法就是加缓存。
我们给这个user_tab
加个memcached/Redis
的缓存。
一般缓存能保证最终一致性
,却很难保证强一致性。在绝大多数业务来说,也只需要保证最终一致性就可以。
一般来说,缓存最终一致性
有两种方案。
1. Cache Aside Pattern
Cache Aside Pattern
意为旁路缓存模式,是应用最为广泛的一种缓存策略。
- 在读请求中,首先请求缓存,若
缓存命中
,则直接返回缓存中的数据; - 若
缓存未命中
,则查询数据库
并将查询结果更新至缓存
,然后返回查询出的数据
。 - 在写请求中,先
更新数据库
,再删除缓存
。
这里最让人感觉疑惑的是,为什么要删除缓存,而不是更新缓存?
更新数据库后删除缓存
是用来保证最终一致性的。如果是更新缓存,数据库写和缓存写并非原子性,可能会导致以下问题:
- 并发写入同一份数据时,缓存写入顺序不一致,导致脏数据。
- 写入失败导致脏数据。
而更新数据库后删除缓存,则保证了有任何改动都去删除缓存,下次读的时候从DB同步到缓存,就能保证最终一致性。
基于数据库日志MySQL binlog
的增量解析、订阅和消费
这是很多企业使用的方案,为的是减少业务层对缓存操作导致的业务复杂性和易错性。
一个相对成熟的方案是通过异步订阅MySQL binlog
的消息,对增量日志进行解析和消费。
这里较为流行的是阿里巴巴开源的作为MySQL binlog
增量获取和解析的组件canal
。
canal sever
模拟MySQL slave
的交互协议,伪装为MySQL slave
,向MySQL master
发送dump
协议。MySQL master
收到dump
请求,开始推送binary log
给slave
(即canal sever
)。canal sever
解析binary log
对象(原始为 byte 流),可由canal client
拉取进行消费,同时canal server
也默认支持将变更记录投递到 MQ 系统中,主动推送给其他系统进行消费。- 在 ack 机制的加持下,不管是推送还是拉取,都可以有效的保证数据按照预期被消费。
由于我们使用的是go语言,使用了go语言版本的MySQL binlog订阅,github go-mysql,后续我会做一篇源码分析来分析MySQL binlog
相关的方方面面。
二、垂直拆分
缓存方案实际上适用于读多写少
的用户场景,因为每次数据更新都会导致缓存失效。但是我们的用户表的设计,因为用户每次登入登出,都需要修改一些易变的字段,login_time
和logout_time
, 每次修改都需要删除缓存,就会降低缓存的命中率。导致流量集中在DB上,影响我们业务整体的性能。
这个时候怎么办呢?我们第一步就是做垂直拆分。
垂直拆分是把易变的字段从表中拆分出来,形成一个单独的表。这样就有了两个表。
- 一个是
user_tab
, 用户主要信息表,主要流量是读,我们方便加缓存。如果大量的写会导致缓存的删除,发挥不了缓存的优势还会经常的访问数据库。 - 一个是
user_ext_tab
, 用户信息中易变的信息表,主要流量是写,我们可以拆分出来,如果写入出现瓶颈,可以使用后续途径优化。
这样由于user_tab
已经没有易变的数据,缓存能长时间保持有效,大大提高缓存的命中率,降低DB的访问QPS。提升我们整体的性能。
另一张表,user_ext_tab
,是一个经常需要更新的表,单独拆分出来,如果有性能问题,我们也方便单独优化。
三、水平拆分
当一张table的数据量过大,比如千万级及以上,会导致B+数的层级过高,而我们推荐的InnoDB的B+数层级是不超过3级,过高的层级会导致数据库操作时过多的磁盘IO,会影响数据库的读写性能。这个时候我们需要考虑水平拆分(分表)。
关于一颗B+树可以存放多少行数据,可以查看我之前的博客。后端面试之MySQL-InnoDB一颗B+树可以存放多少行数据?
常见的拆成10~1000个表,我们这里假设拆分成1000张表。
user_ext_tab_00000000
~user_ext_tab_00000999
, 我们通过userid%1000
对userid取模来决定某个userid对应的数据应该读写哪张表。
这样每个表的数据量就大大降低,保持我们的每个表的数据量维持在一个比较低的水平,保证了InnoDB的B+树的层级,降低了磁盘IO,提高了数据库的访问性能。
四、消息队列
拆分之后性能开始平稳了,一切看起来都很美好,然后公司要搞一个双十一活动。在双十一零点的时候,流量瞬间增加几十倍,导致数据库的压力瞬间增大,几乎承载不住。虽然这个流量是瞬间的,一会就恢复正常,但是这个峰值是一个风险,可能导致数据库宕机,影响所有的线上业务,这是一个很大的风险。
这个时候就需要用到消息队列,消息队列的特性解耦,异步,削峰
刚好满足我们当前的场景。
每当需要更新user_ext_tab
的时候,我们把更新的事件发送到消息队列
中,消息队列里面的消费者通过消费消息来更新数据库,我们可以加一个速率控制
,避免数据库的更新QPS过高导致数据库性能问题。这样即使遇到流量抖动,我们的数据库也能平稳的更新数据。
问题完美解决,可能这个时候你觉得可以睡个好觉了,再也不用担心性能问题了。
五、分库
业务发展太快,除了用户量提升之后,整体流量也提升了。流量提升就会导致访问数据库的QPS提升,这个时候数据库实例的网络IO
,CPU
,磁盘IO
都会跟着提升。即使我们做了水平拆分,但是我们单台机器能承载的流量是有上限的。所以我们下一步我们需要做分库。
由于我们之前已经做过分表,所以分库比较方便,直接把单一数据库分成10个数据库,部署在10台机器上。每个数据库就有100个表。
六、批量更新 - 终极杀招
以上的步骤都是比较常见的步骤,而这么不停的拆分和扩容也不是办法,毕竟机器都是钱。我们需要想办法优化来缩减资源并提升性能。
所以我们想出了这么一个终极大杀器,批量更新,它帮我们度过了一个又一个的大促活动。所以我就主要讲讲我们这一步怎么优化的。
我们的更新QPS一直在增长,即使分库分表之后,每个库的update QPS
也非常高,导致数据库的CPU,磁盘IO,网络IO
非常高。
最后我们使用批量更新的方式,把数据先更新到缓存,然后批量取固定量的数据一起更新DB。
批量更新说起来简单,但是操作起来却有很多细节,这里我们来讲讲我们设计和实现的那些细节。
我们的批量更新分为几个步骤:
- 数据写入Cache并记录在ZSET里面
- 任务调度,从ZSET批量取出需要更新的数据,并发读取缓存,执行批量更新。
- 自动调度,增加或减少调度器时能自动调整任务分片,保证数据不重复,不丢失。
下面来详细讲讲具体的流程。
写入Cache
我们要批量更新的第一步就是写入cache,我们使用Redis的Hash
来储存Cache。当然你想用String自己做序列化和反序列化也是可以的。
Key: {prefix}_userid
Value: `login_time`: timestamp
`logout_time`: timestamp
`...`: ...
有了Cache之后,我们想要批量更新,还需要一个把我们需要更新的列表列出来。
这里我使用了 Redis的ZSET
来储存需要批量更新的数据。由于上面我们已经分了1000个表,所以我们需要1000个ZSET,每个ZSET负责单一的表。
Key: {prefix}_[0,999]
Member: userid
score: timestamp
这样,每次我们需要把数据写入Cache的时候,把userid插入ZSET中,这样我们就知道哪些userid的数据需要批量写入DB中。我们的调度器就能通过ZSET知道哪些userid的数据需要更新,再通过Cache找到需要更新的具体数据,进行批量更新。
批量更新语法
批量更新的语法就是UPDATE SET CASE WHEN
的语法。
UPDATE user_ext_tab_00000000
SET login_time = (
CASE
WHEN userid = 11000 AND login_time < 1234567890 THEN 1234567890
WHEN userid = 12000 AND login_time < 1234567890 THEN 1234567890
WHEN userid = 13000 AND login_time < 1234567890 THEN 1234567890
ELSE login_time
END
),
login_out = (
CASE
WHEN userid = 11000 AND logout_time < 1234567890 THEN 1234567890
WHEN userid = 12000 AND logout_time < 1234567890 THEN 1234567890
WHEN userid = 13000 AND logout_time < 1234567890 THEN 1234567890
ELSE logout_time
END
)
WHEN userid IN (11000, 12000, 13000);
这里需要注意:
- 批量更新的数据量不能太大,如果更新1000条,占用的锁资源更大,如果刚好有其他SQL在访问这些行,就锁等待了。所以一般建议100行以内,我们选择的是20。
- 批量更新的IN里面最好为主键,且有序,因为MySQL的物理行储存是按主键的顺序,这相当于顺序IO,一次更新一片区域。
- 批量更新设计的好可以极大减少CPU/网络IO/磁盘IO的使用率。
批量更新流程
批量更新我们需要一个定时调度器(scheduler)来定时扫描ZSET,我们生产环境设置的是20ms,如果流量更大的情况下,我们可以调节这个值来控制更新的速率。
我们定义1000个表是1000个任务分片
。
- 不写入DB,而写入Cache,并把userid记录在ZSET里面,以便后续的批量更新。
- 启动一个定时调度器,对1000个任务分片进行循环POP ZSET。取出userid的列表,执行批量更新。
自动调度
我们现在能执行批量更新了,但是调度器是单实例的。如果调度器宕机或者无法支撑这么高的数据量,依旧会出问题。
所以我们需要能自动调度,可以部署多台,且每台只负责一部分的任务分片
。同时任务分片不重复,不遗漏。由于定时器是彼此独立的,如果没有中央服务器来进行调度的话,我们很难保证增加或者减少调度器的时候能自动调整自己负责的区域。
这里我们使用了Kafka的Partition机制来进行调度。
众所周知
,小学二年级我们学过,当Kafka的同一个group消费同一个topic消息的时候,每个consumer会负责1到多个partition,我们增加或者减少consumer的时候,会自动调整消费的partiton。所以我们的consumer的数量要小于等于partition的数量,否则有些consumer会无法消费消息。
这里我们有10个partition,三个consumer,如果增加或者减少一个consumer,partition会自动重新分配,保证consumer和partition一对多的映射关系。
我们利用Consumer负责的partition自动调整的机制,来实现我们的调度器。
假设,我们申请一个有10个partition的Kafka topic。我们初始化定时调度器的时候注册多个Consumer
。
那么,这几个Consumer就负责10个partition。我们设计一个映射算法,10个partition映射到1000个任务分片
上面。这个算法是固定的,也就是Partition和任务分片一定是一对多的对应关系。
我们每次调整调度器数量的时候,比如增加一个调度器,就会额外注册一个Consumer,Partition就会重新分配,依然保证了consumer和partition一对多的映射关系。这样就自动调整了调度器负责的任务分片
的数量。
而每一个任务分片就执行上述批量更新的流程,即使某一个调度器
宕机了,partition会自动分配到其他在线的Consumer上,导致其他的Consumer自动分配所有的Partition。最终依然会保证我们的调度器能够处理所有的任务分片。
用了这个批量更新的方式,我们的MySQL的CPU,磁盘IO,网络IO
都降低了80%以上。这也是我们更新的终极方案了。
结论
也许你会问,为啥不一步到位直接进行最后一步呢。
引用一句话,过早优化是万恶之源
。你永远不知道你的业务最终能到达什么程度,优化是在业务增长的过程中,一步一步进行的。
只要我们保持良好的代码风格,就可以进行很方便的优化。但是不要过度优化。
<全文完>
猜你还喜欢
- 03-29 [编程相关] Winform窗体圆角以及描边完美解决方案
- 03-29 [前端问题] has been blocked by CORS policy跨域问题解决
- 03-29 [编程相关] GitHub Actions 入门教程
- 03-29 [编程探讨] CSS Grid 网格布局教程
- 10-12 [编程相关] python实现文件夹所有文件编码从GBK转为UTF8
- 10-11 [编程算法] opencv之霍夫变换:圆
- 10-11 [编程算法] OpenCV Camshift算法+目标跟踪源码
- 10-11 [Python] python 创建 Telnet 客户端
- 10-11 [编程相关] Python 基于 Yolov8 + CPU 实现物体检测
- 03-15 [脚本工具] 使用go语言开发自动化脚本 - 一键定场、抢购、预约、捡漏
- 01-08 [编程技术] 秒杀面试官系列 - Redis zset底层是怎么实现的
- 01-05 [编程技术] 《Redis设计与实现》pdf
取消回复欢迎 你 发表评论:
- 精品推荐!
-
- 最新文章
- 热门文章
- 热评文章
[资料] 2025军队文职 公共课+专业课+真题+押题+面试【合集】
[课程] 《大师级航拍教程》63节课程视频 MP4格式 5.9G
[资料] 中医鬼才倪海厦全集完整版+资料全集
[课程] 聂佳判断推理绝版课程大集合【8G】
[电视剧] 芈月传 【全集81集全】【未删减版】【国语中字】【2015】【HD720P】【75G】
[电视剧] 封神榜 梁丽版 (1989) 共5集 480P国语无字 最贴近原著的一版【0.98 G】
[影视] 【雪山飞孤4个版本】【1985、1991、1999、2007】【1080P、720P】【中文字幕】【167.1G】
[资料] 24秋初中改版教材全集(全版本)[PDF]
[电影] 高分国剧《康熙王朝》(2001)4K 2160P 国语中字 全46集 78.2G
[动画] 迪士尼系列动画139部 国英双语音轨 【蓝光珍藏版440GB】
[书籍] 彭子益医书合集 [PDF/DOC]
[游戏] 《黑神话悟空》免安装学习版【全dlc整合完整版】+Steam游戏解锁+游戏修改工具!
[动画] 《名侦探柯南》名侦探柯南百万美元的五菱星 [TC] [MP4]
[动画] 2002《火影忍者》720集全【4K典藏版】+11部剧场版+OVA+漫画 内嵌简日字幕
[剧集] 《斯巴达克斯》1-4季合集 无删减版 1080P 内嵌简英特效字幕
[CG剧情] 《黑神话:悟空》158分钟CG完整剧情合集 4K120帧最高画质
[游戏] 黑神话悟空离线完整版+修改器
[电影] 《变形金刚系列》七部合集 [4K HDR 蓝光] 国英双语音轨 [内封精品特效字幕]【典藏版】235G
[图像处理] 光影魔术手v4.6.0.578绿色版
[动画] 西游记 (1999) 动画版 4K 全52集 高清修复版 童年回忆
[影视] 美国内战 4K蓝光原盘下载+高清MKV版/内战/帝国浩劫:美国内战(台)/美帝崩裂(港) 2024 Civil War 63.86G
[影视] 一命 3D 蓝光高清MKV版/切腹 / 切腹:武士之死 / Hara-Kiri: Death of a Samurai / Ichimei 2011 一命 13.6G
[影视] 爱情我你他 蓝光原盘下载+高清MKV版/你、我、他她他 2005 Me and You and Everyone We Know 23.2G
[影视] 穿越美国 蓝光原盘下载+高清MKV版/窈窕老爸 / 寻找他妈…的故事 2005 Transamerica 20.8G
[电影] 《黄飞鸿》全系列合集
[Android] 开罗游戏 ▎像素风格的模拟经营的游戏厂商安卓游戏大合集
[游戏合集] 要战便战 v0.9.107 免安装绿色中文版
[书籍] 彭子益医书合集 [PDF/DOC]
[资源] 精整2023年知识星球付费文合集136篇【PDF格式】
[系统]【黑果小兵】macOS Big Sur 11.0.1 20B50 正式版 with Clover 5126 黑苹果系统镜像下载
- 最新评论
-
找了好久的资源bjzchzch12 评论于:11-07 谢谢分享感谢ppy2016 评论于:11-05 谢谢分享感谢ppy2016 评论于:11-05 怎么没有后续闲仙麟 评论于:11-03 怎么没后续闲仙麟 评论于:11-03 有靳东!嘻嘻奥古斯都.凯撒 评论于:10-28 流星花园是F4处女作也是4人集体搭配的唯一一部!奥古斯都.凯撒 评论于:10-28 找了好久的资源,终于在这里找到了。感谢本站的资源和分享。谢谢AAAAA 评论于:10-26 找了好久的资源,终于在这里找到了。感谢本站的资源和分享。谢谢password63 评论于:10-26 找了好久的资源,终于在这里找齐了!!!!blog001 评论于:10-21
- 热门tag