高并发场景下数据库的常见问题及解决方案 置顶! 有更新!

Published on with 4,912 views

    一、分库分表

      (1)为什么要分库分表

      随着系统访问量的增加,QPS越来越高,数据库磁盘容量不断增加,一般数据库服务器的QPS在800-1200的时候性能最佳,当超过2000的时候sql就会变得很慢并且很容易被请求打死,而单表数据量过大也会导致数据库执行sql很慢,为了应付这种场景产生了分库分表这种思想和技术。

      分表就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。可以按照某一个维度来进行分表(例如按照用户id来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用户就操作那个表就好了)。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定在200万以内。

      分库是什么?一般来说一个数据库服务器最多支撑到并发2000,一定要扩容了,而且一个健康的单库并发值你最好保持在每秒1000左右,不要太大。那么你可以将一个库的数据拆分到多个库中,访问的时候就访问一个库好了。

      (2)分库分表中间件

      常见的分布分表中间件有:cobar、TDDL、atlas、sharding-jdbc、mycat。它们可以分为client层和proxy方案。client方案的的优点在于不用部署,运维成本很低,但是如果要升级什么的得重新升级版本再发布,各个服务之间都需要耦合client依赖;proxy方案优点是对个各个服务都是透明的,如果需要升级什么的直接在中间件搞就可以了,但是得需要专门去部署运维。

      cobar:阿里b2b团队开发和开源的,属于proxy层方案。早些年还可以用,但是最近几年都没更新了,基本没啥人用,差不多算是被抛弃的状态吧。而且不支持读写分离、存储过程、跨库join和分页等操作。

      TDDL:淘宝团队开发的,属于client层方案。不支持join、多表查询等语法,就是基本的crud语法是ok,但是支持读写分离。目前使用的也不多,因为还依赖淘宝的diamond配置管理系统。

      atlas:360开源的,属于proxy层方案,以前是有一些公司在用的,但是确实有一个很大的问题就是社区最新的维护都在5年前了。所以,现在用的公司基本也很少了。

      sharding-jdbc:当当开源的,属于client层方案。确实之前用的还比较多一些,因为SQL语法支持也比较多,没有太多限制,而且目前推出到了2.0版本,支持分库分表、读写分离、分布式id生成、柔性事务(最大努力送达型事务、TCC事务)。而且确实之前使用的公司会比较多一些(这个在官网有登记使用的公司,可以看到从2017年一直到现在,是不少公司在用的),目前社区也还一直在开发和维护,还算是比较活跃,个人认为算是一个现在也可以选择的方案。

      mycat:基于cobar改造的,属于proxy层方案,支持的功能非常完善,而且目前应该是非常火的而且不断流行的数据库中间件,社区很活跃,也有一些公司开始在用了。但是确实相比于sharding jdbc来说,年轻一些,经历的锤炼少一些。

      (3)如何对数据库进行垂直拆分或者水平拆分?

      水平拆分就是把一个表的数据给弄到多个库的多个表里去,但是每个库的表结构都一样,只不过每个库表放的数据是不同的,所有库表的数据加起来就是全部数据。水平拆分的意义,就是将数据均匀放更多的库里,然后用多个库来抗更高的并发,还有就是用多个库的存储容量来进行扩容。

      垂直拆分就是把一个有很多字段的表给拆分成多个表,或者是多个库上去。每个库表的结构都不一样,每个库表都包含部分字段。一般来说,会将较少的访问频率很高的字段放到一个表里去,然后将较多的访问频率很低的字段放到另外一个表里去。因为数据库是有缓存的,你访问频率高的行字段越少,就可以在缓存里缓存更多的行,性能就越好。这个一般在表层面做的较多一些。

      有两种分库分表的方式,一种是按照range来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了;另外一种是按照某个字段hash一下均匀分散,这个较为常用。

      range来分,好处在于说,后面扩容的时候,就很容易,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用range,要看场景,你的用户不是仅仅访问最新的数据,而是均匀的访问现在的数据以及历史的数据。

      hash分法,好处在于说,可以平均分配没给库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的这么一个过程。

    二、如何让未分库分表的系统动态迁移到分库分表

      (1)停机迁移方案

      停机迁移是最简单的方案,思路很简单,通知用户系统需要升级维护时间,然后拒绝用户请求,

      首先建好新的分库分表后的数据库,然后写一个导数据的程序把原先数据库里面的数据入到新库里面,在导完数据后,系统添加分库分表的配置然后重启系统即可。

      这种方案现在不怎么用了,因为需要停机维护,况且数据量大了会导致停机时间很长,所以说很僵硬。

      (2)双写迁移方案

      双写迁移方案是现在常用的一种迁移方案,思路就是平滑上线一个新服务,把之前所有写库的地方,增删改操作,都除了对老库增删改,都加上对新库的增删改,这就是所谓双写,同时写俩库,老库和新库,读库还是读老库,然后在平滑的下线在之前直接走老库的老服务。然后新服务替换老服务后,由于新库数据差太远,用之前说的导数工具,跑起来读老库数据写新库,写的时候要根据updateTime这类字段判断这条数据最后修改的时间,除非是读出来的数据在新库里没有,或者是比新库的数据新才会写。在导完一轮数据后,有可能数据还是存在不一致,那么就程序自动做一轮校验,比对新老库每个表的每条数据,接着如果有不一样的,就针对那些不一样的,从老库读数据再次写。反复循环,直到两个库每个表的数据都完全一致为止。接着基于仅仅使用分库分表的最新代码,重新部署一次,不就仅仅基于分库分表在操作了么,还没有几个小时的停机时间,很稳。

    三、动态扩容缩容分库分表方案

      (1)停机扩容

      这个方案就跟停机迁移一样,步骤几乎一致,唯一的一点就是那个导数的工具,是把现有库表的数据抽出来慢慢倒入到新的库和表里去。但是最好别这么玩儿,有点不太靠谱,因为分库分表就说明数据量实在是太大了,可能多达几亿条,甚至几十亿,这么玩可能会出问题。

      (2)优化后的方案

      一开始上来就是32个库,每个库32个表,1024张表,这个分法,第一,基本上国内的互联网肯定都是够用了,第二,无论是并发支撑还是数据量支撑都没问题。开始的时候一个数据库服务器可以放多个数据库和表,在扩容或者缩容的时候直接迁移数据库到其他数据库服务器就可以了。例如使用如下步骤:

      1、设定好几台数据库服务器,每台服务器上几个库,每个库多少个表,推荐是32库 * 32表,对于大部分公司来说,可能几年都够了

      2、路由的规则,orderId 模 32 = 库,orderId / 32 模 32 = 表

      3、扩容的时候,申请增加更多的数据库服务器,装好mysql,倍数扩容,4台服务器,扩到8台服务器,16台服务器

      4、由dba负责将原先数据库服务器的库,迁移到新的数据库服务器上去,很多工具,库迁移,比较便捷

      5、我们这边就是修改一下配置,调整迁移的库所在数据库服务器的地址

      6、重新发布系统,上线,原先的路由规则变都不用变,直接可以基于2倍的数据库服务器的资源,继续进行线上系统的提供服务。

      每个库正常承载的写入并发量是1000,那么32个库就可以承载32 * 1000 = 32000的写并发,如果每个库承载1500的写并发,32 * 1500 = 48000的写并发,接近5万/s的写入并发,前面再加一个MQ,削峰,每秒写入MQ 8万条数据,每秒消费5万条数据。除非是国内排名非常靠前的这些公司,他们的最核心的系统的数据库,可能会出现几百台数据库的这么一个规模,128个库,256个库,512个库。

      1024张表,假设每个表放500万数据,在MySQL里可以放50亿条数据,每秒的5万写并发,总共50亿条数据,对于国内大部分的互联网公司来说,其实一般来说都够了。

      谈分库分表的扩容,第一次分库分表,就一次性给他分个够,32个库,1024张表,可能对大部分的中小型互联网公司来说,已经可以支撑好几年了,刚开始就是一个mysql服务器可能建了n个库,比如16个库。后面如果要拆分,就是不断在库和mysql服务器之间做迁移就可以了。然后系统配合改一下配置即可。

      这么搞,是不用开发人员写代码做数据迁移的,都交给dba来搞好了,但是dba确实是需要做一些库表迁移的工作,但是总比开发人员写代码,抽数据导数据来的效率高得多了。

      哪怕是要减少库的数量,也很简单,其实说白了就是按倍数缩容就可以了,然后修改一下路由规则。

    四、分库分表后,id主键如何生成?

      在我们原先单库的时候可以基于数据库自增来生成主键id,但是在分库分表后,这样肯定是不行的,所以需要一个全局唯一的id来生成主键,以下是几种常见的解决方案。

      (1)数据库自增id

      这个方案就是每次要获取一个全局id,要向一个单库单表中插入一条无意义的数据来获取他的自增id,在拿到这个id后再在分库分表中使用。

      这个方案用的人几乎没有,因为分库分表就是因为并法量或者数据量问题,用单库单表去生成主键id,效率很低,不能支持高并发。这个方案还有一个弊端就是生成的是连续的id,如果用于订单什么的可能会暴露商业机密,这就很尴尬。

      (2)redis的INCR指令

      Redis Incr 命令将 key 中储存的数字值增一,然后返回执行完命令的值,这个方案规避了基于数据库的并发问题,因为redis天然吞吐量高,而且纯内存操作,速度非常快,但是就是有一个弊端,生成的是有顺序的id,同上描述的数据库自增id一样在一些场景下不可用。

      (3)UUID

      uuid好处就是本地生成,不要基于数据库来了;不好之处就是,uuid太长了,作为主键性能太差了,不适合用于主键。

      uuid适合的场景一般是如果你是要随机生成个什么文件名了,编号之类的,你可以用uuid,但是作为主键是不能用uuid的。

      (4)snowflake算法

      twitter开源的分布式id生成算法,就是把一个64位的long型的id,1个bit是不用的,用其中的41 bit作为毫秒数,用10 bit作为工作机器id,12 bit作为序列号,其中具体含义如下:

      1 bit:不用,为啥呢?因为二进制里第一个bit为如果是1,那么都是负数,但是我们生成的id都是正数,所以第一个bit统一都是0

      41 bit:表示的是时间戳,单位是毫秒。41 bit可以表示的数字多达241 - 1,也就是可以标识2 41 - 1个毫秒值,换算成年就是表示69年的时间。

      10 bit:记录工作机器id,代表的是这个服务最多可以部署在210台机器上哪,也就是1024台机器。但是10 bit里5个bit代表机房id,5个bit代表机器id。意思就是最多代表2 5个机房(32个机房),每个机房里可以代表2 ^ 5个机器(32台机器)。

      12 bit:这个是用来记录同一个毫秒内产生的不同id,12 bit可以代表的最大正整数是2 ^ 12 - 1 = 4096,也就是说可以用这个12bit代表的数字来区分同一个毫秒内的4096个不同的id。

      这个方案属于现在开发中最常用的方案,各方面来说都不错。

    五、mysql的读写分离

      (1)如何实现mysql的读写分离?

      mysql的读写分离基于主从复制架构,我们可以搞一个主库,然后主库挂多个从库,然后我们单单写主库,然后主库会自动把数据同步到从库,然后读从库来访问数据。

      (2)mysql主从复制的原理

      主库将变更写binlog日志,然后从库连接到主库之后,从库有一个IO线程,将主库的binlog日志拷贝到自己本地,写入一个中继日志中。接着从库中有一个SQL线程会从中继日志读取binlog,然后执行binlog日志中的内容,也就是在自己本地再次执行一遍SQL,这样就可以保证自己跟主库的数据是一样的。

      在这个过程中从库同步主库数据的过程是串行化的,也就是说主库上并行的操作,在从库上会串行执行。所以这就是一个非常重要的点了,由于从库从主库拷贝日志以及串行执行SQL的特点,在高并发场景下,从库的数据一定会比主库慢一些,是有延时的。所以经常出现,刚写入主库的数据可能是读不到的,要过几十毫秒,甚至几百毫秒才能读取到。

      而且会存在一个问题,就是如果主库突然宕机,然后恰好数据还没同步到从库,那么有些数据可能在从库上是没有的,有些数据可能就丢失了。

      所以mysql实际上在这一块有两个机制,一个是半同步复制,用来解决主库数据丢失问题;一个是并行复制,用来解决主从同步延时问题。

      这个所谓半同步复制,semi-sync复制,指的就是主库写入binlog日志之后,就会将强制此时立即将数据同步到从库,从库将日志写入自己本地的relay log之后,接着会返回一个ack给主库,主库接收到至少一个从库的ack之后才会认为写操作完成了。

      所谓并行复制,指的是从库开启多个线程,并行读取relay log中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。

      (3)mysql的主从同步延时问题

      可以通过配置一些参数来降低延迟,但不是根本解决主从同步的方法,这个延迟时间一般在写并发在1000的时候延迟时间一般是几毫秒,在2000的时候会有几十毫秒,在4000、6000、8000并发时一般会达到几秒。

      所以一般说,可以采用如下方案:

      1.分库,将一个主库拆分为4个主库,每个主库的写并发就500/s,此时主从延迟可以忽略不计

      2.打开mysql支持的并行复制,多个库并行复制,如果说某个库的写入并发就是特别高,单库写 并发达到了2000/s,并行复制还是没意义。

      3.重写代码,写代码的同学,要慎重,插入数据之后,直接就更新,不要查询。

      4.如果确实是存在必须先插入,立马要求就查询到,然后立马就要反过来执行一些操作,对这个查询设置直连主库。不推荐这种方法,这么搞导致读写分离的意义就丧失了。

    CSDN:https://www.jmhat.com/qq_36236890

    微信订阅号:
    0c2ae39501374a4199104d9c561b2849-weixin.png

    Responses