专栏首页微观技术单台 MySQL 支撑不了这么多的并发请求,我们该怎么办?

单台 MySQL 支撑不了这么多的并发请求,我们该怎么办?

关系型数据库的事务特性可以帮我们解决很多难题,比如数据的一致性问题,所以常规业务持久化存储都会mysql 来兜底。但mysql 的性能是有限的。当业务规模发展到上百万用户,访问量达到上万QPS时,单台mysql实例很难应付。

有哪些解决方案?

1、首先我们会想到给数据库找一个搭档,也就是缓存

目前市面上经典组合是mysql+redis。Redis 作为 MySQL 的前置缓存,可以替 MySQL 挡住绝大部分查询请求,很大程度上缓解了 MySQL 并发请求的压力。

Redis 是一个使用内存保存数据的高性能 KV 数据库,它的高性能主要来自于:

  • 简单的数据结构;
  • 使用内存存储数据

内存是一种易失性存储,所以使用内存保存数据的 Redis 不能保证数据可靠存储。从设计上来说,Redis 牺牲了大部分功能,牺牲了数据可靠性,换取了高性能。但也正是这些特性,使得 Redis 特别适合用来做 MySQL 的前置缓存。

Redis提供的api比较简单,开箱即用,社区成熟,网上的资料也比较全,常见的问题网上基本都能找到解决方案,如果一个公司要使用缓存,90%以上都会选择redis。

虽然redis入门很容易,但用好redis也不是一件容易的事。要考虑缓存穿透、缓存雪崩、缓存热点、缓存命中率低、以及缓存数据不一致等问题。关于这一系列缓存问题,之前有整理过三篇文章,感兴趣可以点击下面链接阅读

2、读写分离

缓存缺失是可以解决大部分的性能问题,业界也有句话,“性能不够,缓存来凑”。像商品详情页、秒杀等场景,典型的读多写少,非常适合使用缓存,缓存的命中率可以达到90% 以上,将缓存的价值发挥到了极致。

但有些用户维度业务场景,比如:用户订单列表、账户系统、购物车系统。这些业务场景都是与用户挂钩,作用范围较窄,缓存效果大打折扣。

总结一下,互联网业务大部分都是读多写少,区别在于影响范围。有些是全局性的,如“修改一件商品信息,所有用户看到的是一份数据”;有些是局部性,如“用户小张刚下了一笔订单,查订单列表时,要带出最新的这条订单信息”。

全局性的读多写少,我们可以引入缓存。但是局部性的读多写少呢?这部分流量通常还是打在了mysql上,但是单台 MySQL 支撑不了这么多的并发请求时,我们该怎么办?

一个简单而且非常有效的方案是,使用多个具有相同数据的 MySQL 实例来分担大量的查询请求,这种方法通常称为“读写分离”。

一个分布式的存储系统,想要做分布式写是非常非常困难的,因为很难解决好数据一致性的问题。但实现分布式读就相对简单很多,我只需要增加一些只读的实例,只要能够把数据实时的同步到这些只读实例上,保证这这些只读实例上的数据都随时一样,这些只读的实例就可以分担大量的查询请求。

读写分离的另外一个好处是,它实施起来相对比较简单。把使用单机 MySQL 的系统升级为读写分离的多实例架构非常容易,一般不需要修改系统的业务逻辑,只需要简单修改 DAO 代码,把对数据库的读写请求分开,请求不同的 MySQL 实例就可以了。

这边有一个手写的例子,数据源配置了master、slave两个读写数据源,通过MyBatis拦截器,对sql判断是读sql还是写sql,进而选择对应的数据源。最后借助spring预留的扩展接口AbstractRoutingDataSource,其提供了动态数据源的功能,可以帮助我们实现读写分离,内部方法determineCurrentLookupKey() 可以决定最终使用哪个数据源。详细内容可参考文章

教你实现 MySQL 读写分离 + 故障转移

通过读写分离这样一个简单的存储架构升级,就可以让数据库支持的并发数量增加几倍到十几倍。所以,当你的系统用户数越来越多,读写分离应该是你首先要考虑的扩容方案。

注意:读写分离可能会带来数据不一致问题。

主库负责处理所有的更新操作,然后异步将数据变更实时同步到所有的从库中去,这个过程有一个微小的时间差,这个时间差叫主从同步延迟。正常情况下,主从延迟非常小,不超过1ms。但即使这个非常小的延迟,也会导致在某一个时刻,主库和从库上的数据是不一致的。应用程序需要能接受并克服这种主从不一致的情况,否则就会引发一些由于主从延迟导致的数据错误。

比如:用户在淘宝下了一笔订单,当支付成功后,按理说是应该跳到订单详情页。但此时,订单从库可能还没来及的同步订单主库的最新状态,有可能仍处于“待付款”,造成不好的用户体验。所以,细心的我们会发现,大部分的电商网站支付成功后,是不会自动跳回到订单详情页,它增加了一个无关紧要的“支付成功”页面。其实这个页面没有任何有效的信息,就是告诉你支付成功,然后再放一些广告什么的。你如果想再看刚刚支付完成的订单,需要手动点一下,这样就很好地规避了主从同步延迟的问题。

3、数据归档

既然数据库的容量影响性能,那么我们可以从数据量上做优化,将一些不用的数据清理并归档。

所谓归档,其实也是一种拆分数据的策略。以电商为例,就是把大量的历史订单移到另外一张历史订单表中。为什么这么做呢?因为像订单这类具有时间属性的数据,都存在热尾效应。大多数情况下访问的都是最近的数据,但订单表里面大量的数据都是不怎么常用的老数据。

画外音:不到万不得已,先不要着急分库分表,后者的技改成本还是很大的,同时还会引入分布式事务问题,需要引入额外框架来解决,维护成本也非常高。

清理数据方案改造成本非常小,由于相对独立,与业务解耦,不需要对原来的业务代码做改动,影响面及风险会比较低。

早年像淘宝、京东大型电商网站查看自己的订单时,都有一个”三个月前订单“选项,其实就是查的订单历史表。

清理过程中需要对原表的数据删除,但是删除了大量的数据后,如果你检查一下 MySQL 占用的磁盘空间,你会发现它占用的磁盘空间并没有变小,这是什么原因呢?其实和 InnoDB 的物理存储结构有关系。

虽然逻辑上每个表都有B+ 索引树,但是物理上,每条记录都是存放在磁盘文件中的,这些记录通过一些位置指针来组成一棵 B+ 树。当 MySQL 删除一条记录的时候,只能是找到记录所在的文件中位置,然后把文件的这块区域标记为空闲,然后再修改 B+ 树中相关的一些指针,完成删除。其实那条被删除的记录还是躺在那个文件的那个位置,所以并不会释放磁盘空间。

当然如果磁盘空间紧张,可以执行一次 OPTIMIZE TABLE 释放存储空间,对于 InnoDB 来说,执行 OPTIMIZE TABLE 实际上就是把这个表重建一遍,执行过程中会一直锁表,涉及到数据库的业务操作会被卡住,使用时要特别小心。重建表的过程中,索引也会重建,这样表数据和索引数据都会更紧凑,不仅占用磁盘空间更小,查询效率也会有提升。

4、分库分表

数据库的性能取决于两个因素:查找的时间复杂度、数据量大小。解决海量数据导致存储系统慢的问题,思想非常简单,就是一个“拆”字,把海量数据拆分成 N 个分片。拆开之后,每个分片里的数据就没那么多了,然后让查找尽量落在某一个分片上,这样来提升查找性能。

分库分表的核心特点:

  • 每个分表的结构都一样
  • 每个分表的数据都不一样,没有交集
  • 所有分表的并集是全量数据

分库分表可以解决两个问题:

  • 分片查询,减少了查询的数据量。有效解决查询慢问题
  • 应对高并发问题,一个数据库实例撑不住,就把并发请求分散到多个实例中去,所以,解决高并发的问题是需要分库的。

画外音:数据量大,就分表;并发高,就分库

分库分表最核心的就是选择分表键 Sharding Key,通过分表键按一定的路由分表算法(如:Hash取模分片、区间范围、查表法)指向对应的数据分片。

  • Hash取模分片。比如按订单id做分表键,分1024张表,则将订单id对1024取模,得到的便是分表编号,如果还要分库,则再对分库数取整。
  • 区间范围。这个比较容易理解,比如按日期分表,1年12个月,每个月的数据集中在一个表中。
  • 查表法。查表法其实就是没有分片算法,决定某个 Sharding Key 落在哪个分片上,全靠人为来分配,分配的结果记录在一张表里面。每次执行查询的时候,先去表里查一下要找的数据在哪个分片中。

以电商巨头淘宝的订单表设计为例,订单涉及双向查找,有买家视角,还有卖家视角。无论选择买家uid还是卖家uid都无法满足需求,参考淘宝的做法,规则最大化适用原则,订单号拆成两部分,前面部分为全局唯一自增id,后面部分为买家id的后六位,分表键按照买家uid的后6位来计算,未来支持最大扩展100万张逻辑分表,可以支持按订单id或买家uid来查询。至于卖家部分,采用数据异构方式,采用卖家uid做分表键,将卖家uid和订单id放入另一张数据表中,满足卖家的查询。当然复杂度也会提升不少。

关于分库分表的工具市面有很多,大家可以根据自己公司的实际需要选择最适合的框架。

  • sharding-sphere:jar,前身是 sharding-jdbc。
  • TDDL:jar,Taobao Distribute Data Layer。
  • Mycat:中间件。

关于分库分表要注意的问题,可以参考下面这篇文章:

数据库分库分表思路

本文分享自微信公众号 - 微观技术(weiguanjishu),作者:TomGE

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-06-27

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Dubbo框架常见问题

    在分布式架构时代,dubbo 作为RPC框架,以其高性能、易扩展、配置简单、易上手被越来越多的公司所青睐,在国内互联网公司中口碑一直很好。因为其高频使用,很多面...

    用户7676729
  • 聊聊分布式锁

    高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。

    用户7676729
  • 电商交易系统核心技术

    电商诞生已经有20多个年头了,从早期很多人的质疑、骗子、不接受、甚至肄业排斥、打压,到现在彻底融入我们生活的方方面面,并号称中国的 “新四大发明”,“认知教育”...

    用户7676729
  • 大咖 | 涂子沛:数据正在引领高清晰社会,重塑文明

    2018年11月1日下午由清华大学公共管理学院、中信出版集团和中国电子信息行业联合会数据与治理联盟联合举办的“从大数据到数文明前沿论坛暨《数文明》新书发布会”在...

    大数据文摘
  • 展现数据之美

    大数据文摘
  • 魔方大数据(7)| 聚道科技CEO李夏戎:如何从基因组数据流做到数据协作网络

    <数据猿导读> 目前基因组数据流究竟是什么样子?测序、传输、存储管理、分析计算、注释报告...其中,数据传输环节不仅工作量大,而且速度特别慢。聚道科技CEO李夏...

    数据猿
  • hduoj----1142A Walk Through the Forest(记忆化搜索+最短路)

    A Walk Through the Forest Time Limit: 2000/1000 MS (Java/Others)    Memory Limit...

    Gxjun
  • 奖学金

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。 ...

    喜欢ctrl的cxk
  • 手写一个迷你版的 Tomcat 喵

    Tomcat,这只3脚猫,大学的时候就认识了,直到现在工作中,也常会和它打交道。这是一只神奇的猫,今天让我来抽象你,实现你!

    芋道源码
  • 基因组数据分析步骤-基于R的计算基因组学

    本章的目的是为读者提供理解基因组学所需的一些基础知识。需要说明,这绝不是对这一学科的完整概述,而只是一个简单的总结,它将帮助非生物学相关专业的读者理解计算基因组...

    生信菜鸟团

扫码关注云+社区

领取腾讯云代金券