高并发下数据库优化方案

主从读写分离

基本概念:将一个数据库的数据拷贝为一份或者多份,并且写入到其它的数据库服务器中,原始的数据库我们称为主库,主要负责数据的写入,拷贝的目标数据库称为从库,主要负责支持数据查询。

主从读写分离有两个技术上的关键点

  • 一个是数据的拷贝,我们称为主从复制;
  • 在主从分离的情况下,我们如何屏蔽主从分离带来的访问数据库方式的变化,让开发同学像是在使用单一数据库一样。

主从复制

以MySQL为例:MySQL的主从复制是依赖于binlog的,也就是记录MySQL上的所有变化并以二进制形式保存在磁盘上二进制日志文件。主从复制就是将binlog中的数据从主库传输到从库上,一般这个过程是异步的,即主库上的操作不会等待binlog同步的完成。

主从复制的过程是这样的:首先从库在连接到主节点时会创建一个IO线程,用以请求主库更新的binlog,并且把接收到的binlog信息写入一个叫做relay log的日志文件中,而主库也会创建一个log dump线程来发送binlog给从库;同时,从库还会创建一个SQL线程读取relay log中的内容,并且在从库中做回放,最终实现主从的一致性。这是一种比较常见的主从复制方式。

在这个方案中,使用独立的log dump线程是一种异步的方式,可以避免对主库的主体更新流程产生影响,而从库在接收到信息后并不是写入从库的存储中,是写入一个relay log,是避免写入从库实际存储会比较耗时,最终造成从库和主库延迟变长。

主从复制缺点以及解决方案

主从同步是会带来一定的主从同步的延迟,这种延迟有时候会对业务产生一定的影响。问题解决的思路有很多,核心思想就是尽量不去从库中查询信息,存在以下三种解决方案

  1. 第一种方案是数据的冗余。向后端请求时,发送所有数据到后端,借此避免从数据库中重新查询数据。可以优先考虑这种方案,因为这种方式足够简单,不过可能造成单条消息比较大,从而增加了消息发送的带宽和时间。
  2. 第二种方案是使用缓存。在同步写数据库的同时写入缓存,查询会优先查询缓存,这样也可以保证数据的一致性。缓存的方案比较适合新增数据的场景,在更新数据的场景下,先更新缓存可能会造成数据的不一致,比方说两个线程同时更新数据,线程A把缓存中的数据更新为1,此时另一个线程B把缓存中的数据更新为2,然后线程B又更新数据库中的数据为2,此时线程A更新数据库中的数据为1,这样数据库中的值(1)和缓存中的值(2)就不一致了。
  3. 最后一种方案是查询主库。(尽量别用,违背了主从同步的原则,而且可能导致开发人员的滥用)

主从读写分离后如何访问数据库

为了降低实现的复杂度,业界涌现了很多数据库中间件来解决数据库的访问问题,这些中间件可以分为两类。

  • 第一类以淘宝的TDDL( Taobao Distributed Data Layer)为代表,以代码形式内嵌运行在应用程序内部。你可以把它看成是一种数据源的代理,它的配置管理着多个数据源,每个数据源对应一个数据库,可能是主库,可能是从库。当有一个数据库请求时,中间件将SQL语句发给某一个指定的数据源来处理,然后将处理结果返回。
  • 第二类中间件部署在独立的服务器上,业务代码如同在使用单一数据库一样使用它,实际上它内部管理着很多的数据源,当有数据库请求时,它会对SQL语句做必要的改写,然后发往指定的数据源。如早期阿里巴巴开源的Cobar,基于Cobar开发出来的Mycat,360开源的Atlas,美团开源的基于Atlas开发的DBProxy等等。

分库分表

水平拆分

水平拆分规则分为以下两种、

  1. 按照某一个字段的哈希值做拆分,这种拆分规则比较适用于实体表,比如说用户表,内容表,我们一般按照这些实体表的ID字段来拆分。比如说我们想把用户表拆分成16个库,每个库是64张表,那么可以先对用户ID做哈希,哈希的目的是将ID尽量打散,然后再对16取余,这样就得到了分库后的索引值;对64取余,就得到了分表后的索引值。
  2. 另一种比较常用的是按照某一个字段的区间来拆分,比较常用的是时间字段。你知道在内容表里面有“创建时间”的字段,而我们也是按照时间来查看一个人发布的内容。我们可能会要看昨天的内容,也可能会看一个月前发布的内容,这时就可以按照创建时间的区间来分库分表,比如说可以把一个月的数据放入一张表中,这样在查询时就可以根据创建时间先定位数据存储在哪个表里面,再按照查询条件来查询。

分库分表带来的问题

分库分表引入的一个最大的问题就是引入了分库分表键,也叫做分区键,也就是我们对数据库做分库分表所依据的字段。

从分库分表规则中你可以看到,无论是哈希拆分还是区间段的拆分,我们首先都需要选取一个数据库字段,这带来一个问题是:我们之后所有的查询都需要带上这个字段,才能找到数据所在的库和表,否则就只能向所有的数据库和数据表发送查询命令。如果像上面说的要拆分成16个库和64张表,那么一次数据的查询会变成16*64=1024次查询,查询的性能肯定是极差的。

针对这个问题,有一些相应的解决思路。比如,在用户库中我们使用ID作为分区键,这时如果需要按照昵称来查询用户时,你可以按照昵称作为分区键再做一次拆分,但是这样会极大地增加存储成本,如果以后我们还需要按照注册时间来查询时要怎么办呢,再做一次拆分吗?

所以最合适的思路是要建立一个昵称和ID的映射表,在查询的时候要先通过昵称查询到ID,再通过ID查询完整的数据,这个表也可以是分库分表的,也需要占用一定的存储空间,但是因为表中只有两个字段,所以相比重新做一次拆分还是会节省不少的空间的。

分库分表引入的另外一个问题是一些数据库的特性在实现时可能变得很困难。比如说多表的JOIN在单库时是可以通过一个SQL语句完成的,但是拆分到多个数据库之后就无法跨库执行SQL了,不过好在我们对于JOIN的需求不高,即使有也一般是把两个表的数据取出后在业务代码里面做筛选,复杂是有一些,不过是可以实现的。再比如说在未分库分表之前查询数据总数时只需要在SQL中执行count()即可,现在数据被分散到多个库表中,我们可能要考虑其他的方案,比方说将计数的数据单独存储在一张表中或者记录在Redis里面。