千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

随着互联网的高速发展,带来了海量数据存储的问题,比如像物联网行业,每个智能终端每天进行数据采集和上报,每天能够产几千万甚至上亿的数据。在互联网电商行业,或者一些O2O平台,每天也能产生上千万的订单数据,这些量级的数据在传统的关系型数据库中已经无法支撑了,那么如何解决海量数据存储和计算等问题,在业内引入了分布式存储和分布式计算等解决方案,特别是NoSql的生态,我在前面讲过的k-v数据库、文档数据库、图形数据库等,都是比较主流的分布式数据库解决方案。

即便如此,关系型数据库仍然有它不可替代的特性,所以关系型数据库仍然是核心业务的基础数据平台,因此关系型数据库必然会面临数据量日益增长带来的海量数据处理问题。

Mysql数据库海量数据带来的性能问题

目前几乎所有的互联网公司都是采用mysql这个开源数据库,根据阿里巴巴的《Java开发手册》上提到的,当单表行数超过500W行或者单表数据容量超过2G时,就会对查询性能产生较大影响,这个时候建议对表进行优化。

其实500W数据只是一个折中的值,具体的数据量和数据库服务器配置以及mysql配置有关,因为Mysql为了提升性能,会把表的索引装载到内存,innodb_buffer_pool_size 足够的情况下,mysql能把全部数据加载进内存,查询不会有问题。

但是,当单表数据库到达某个量级的上限时,导致内存无法存储其索引,使得之后的 SQL 查询会产生磁盘 IO,从而导致性能下降。当然,这个还有具体的表结构的设计有关,最终导致的问题都是内存限制,这里,增加硬件配置,可能会带来立竿见影的性能提升。

innodb_buffer_pool_size 包含数据缓存、索引缓存等。

Mysql常见的优化手段

当然,我们首先要进行的优化是基于Mysql本身的优化,常见的优化手段有:

这些常见的优化手段,在数据量较小的情况下效果非常好,但是数据量到达一定瓶颈时,常规的优化手段已经解决不了实际问题,那怎么办呢?

大数据表优化方案

对于大数据表的优化最直观的方式就是减少单表数据量,所以常见的解决方案是:

其实这些解决方案都是属于偏业务类的方案,并不完全是技术上的方案,所以在实施的时候,需要根据业务的特性来选择合适的方式。

详解分库分表

分库分表是非常常见针对单个数据表数据量过大的优化方式,它的核心思想是把一个大的数据表拆分成多个小的数据表,这个过程也叫(数据分片),它的本质其实有点类似于传统数据库中的分区表,比如mysql和oracle都支持分区表机制。

分库分表是一种水平扩展手段,每个分片上包含原来总的数据集的一个子集。这种分而治之的思想在技术中很常见,比如多CPU、分布式架构、分布式缓存等等,像前面我们讲redis cluster集群时,slot槽的分配就是一种数据分片的思想。

如图6-1所示,数据库分库分表一般有两种实现方式:

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-1

垂直拆分

垂直拆分有两种,一种是单库的垂直拆分,另一种是多个数据库的垂直拆分。

单库垂直分表

单个表的字段数量建议控制在20~50个之间,之所以建议做这个限制,是因为如果字段加上数据累计的长度超过一个阈值后,数据就不是存储在一个页上,就会产生分页的问题,而这个问题会导致查询性能下降。

所以如果当某些业务表的字段过多时,我们一般会拆去垂直拆分的方式,把一个表的字段拆分成多个表,如图6-2所示,把一个订单表垂直拆分成一个订单主表和一个订单明细表。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-2

在Innodb引擎中,单表字段最大限制为1017

参考: https://dev.mysql.com/doc/mysql-reslimits-excerpt/5.6/en/column-count-limit.html

多库垂直分表

多库垂直拆分实际上就是把存在于一个库中的多个表,按照一定的纬度拆分到多个库中,如图6-3所示。这种拆分方式在微服务架构中也是很常见,基本上会按照业务纬度拆分数据库,同样该纬度也会影响到微服务的拆分,基本上服务和数据库是独立的。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-3

多库垂直拆分最大的好处就是实现了业务数据的隔离。其次就是缓解了请求的压力,原本所有的表在一个库的时候,所有请求都会打到一个数据库服务器上,通过数据库的拆分,可以分摊掉请求,在这个层面上提升了数据库的吞吐能力。

水平拆分

垂直拆分的方式并没有解决单表数据量过大的问题,所以我们还需要通过水平拆分的方式把大表数据做数据分片。

水平切分也可以分成两种,一种是单库的,一种是多库的。

单库水平分表

如图6-4所示,表示把一张有10000条数据的用户表,按照某种规则拆分成了4张表,每张表的数据量是2500条。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-4

两个案例:

银行的交易流水表,所有进出的交易都需要登记这张表,因为绝大部分时候客户都是查询当天的交易和一个月以内的交易数据,所以我们根据使用频率把这张表拆分成三张表:

当天表:只存储当天的数据。

当月表:我们在夜间运行一个定时任务,前一天的数据,全部迁移到当月表。用的是insert into select,然后delete。

历史表:同样是通过定时任务,把登记时间超过30天的数据,迁移到history历史表(历史表的数据非常大,我们按照月度,每个月建立分区)。

费用表:消费金融公司跟线下商户合作,给客户办理了贷款以后,消费金融公司要给商户返费用,或者叫提成,每天都会产生很多的费用的数据。为了方便管理,我们每个月建立一张费用表,例如fee_detail_201901……fee_detail_201912。

但是注意,跟分区一样,这种方式虽然可以一定程度解决单表查询性能的问题,但是并不能解决单机存储瓶颈的问题。

多库水平分表

多库水平分表,其实有点类似于分库分表的综合实现方案,从分表来说是减少了单表的数据量,从分库层面来说,降低了单个数据库访问的性能瓶颈,如图6-5所示。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-5

常见的水平分表策略

分库更多的是关注业务的耦合度,也就是每个库应该放那些表,是由业务耦合度来决定的,这个在前期做领域建模的时候都会先考虑好,所以问题不大,只是分库之后带来的其他问题,我们在后续内容中来分析。

而分表这块,需要考虑的问题会更多一些,也就是我们应该根据什么样的策略来水平分表?这里就需要涉及到分表策略了,下面简单介绍几种最常见的分片策略。

哈希取模分片

哈希分片,其实就是通过表中的某一个字段进行hash算法得到一个哈希值,然后通过取模运算确定数据应该放在哪个分片中,如图6-6所示。这种方式非常适合随机读写的场景中,它能够很好的将一个大表的数据随机分散到多个小表。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-6

hash取模的问题

hash取模运算有个比较严重的问题,假设根据当前数据表的量以及增长情况,我们把一个大表拆分成了4个小表,看起来满足目前的需求,但是经过一段时间的运行后,发现四个表不够,需要再增加4个表来存储,这种情况下,就需要对原来的数据进行整体迁移,这个过程非常麻烦。

一般为了减少这种方式带来的数据迁移的影响,我们会采用一致性hash算法。

一致性hash算法

在前面我们讲的hash取模算法,实际上对目标表或者目标数据库进行hash取模,一旦目标表或者数据库发生数量上的变化,就会导致所有数据都需要进行迁移,为了减少这种大规模的数据影响,才引入了一致性hash算法。

如图6-7所示,简单来说,一致性哈希将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-232-1(即哈希值是一个32位无符号整形),什么意思呢?

就是我们通过0-232-1的数字组成一个虚拟的圆环,圆环的正上方的点代表0,0点右侧的第一个点代表1,以此类推,2、3、4、5、6……直到232-1,也就是说0点左侧的第一个点代表232-1。我们把这个由2的32次方个点组成的圆环称为hash环。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-7

那一致性hash算法和上面的虚拟环有什么关系呢?继续回到前面我们讲解hash取模的例子,假设现在有四个表,table_1、table_2、table_3、table_4,在一致性hash算法中,取模运算不是直接对这四个表来完成,而是对232来实现。

hash(table编号)%232

通过上述公式算出的结果一定是一个0到232-1之间的一个整数,然后在这个数对应的位置标注目标表,如图6-8所示,四个表通过hash取模之后分别落在hash环的某个位置上。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-8

好了,到目前为止,我们已经把目标表与hash环联系在了一起,那么接下来我们需要把一条数据保存到某个目标表中,怎么做呢?如图6-9所示,当添加一条数据时,同样通过hash和hash环取模运算得到一个目标值,然后根据目标值所在的hash环的位置顺时针查找最近的一个目标表,把数据存储到这个目标表中即可。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-9

不知道大家是否发现了一致性hash的好处,就是hash运算不是直接面向目标表,而是面向hash环,这样的好处就是当需要删除某张表或者增加表的时候,对于整个数据变化的影响是局部的,而不是全局。举个例子,假设我们发现需要增加一张表table_04,如图6-10所示,增加一个表,并不会对其他四个已经产生了数据的表造成影响,原来已经分片的数据完全不需要做任何改动。

如果需要删除一个节点,同样只会影响删除节点本身的数据,前后表的数据完全不受影响。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-10

hash环偏斜

上述设计有一个问题,理论情况下我们目标表是能够均衡的分布在整个hash环中,但实际情况有可能是图6-11所示的样子。也就是产生了hash环偏斜的现象,这种现象导致的问题就是大量的数据都会保存到同一个表中,倒是数据分配极度不均匀。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-11

为了解决这个问题,必须要保证目标节点要均匀的分布在整个hash环中,但是真实的节点就只有4个,如何均匀分布呢?最简单的方法就是,把这四个节点分别复制一份出来分散到这个hash环中,这个复制出来的节点叫虚拟节点,根据实际需要可以虚拟出多个节点出来,如图6-12所示。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-12

按照范围分片

按范围分片,其实就是基于数据表的业务特性,按照某种范围拆分,这个范围的有很多含义,比如:

如图6-7所示,表示按照数据范围进行拆分。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-7

范围分片最终要的是选择一个合适的分片键,这个是否合适来自于业务需求,比如之前有个学员是在做智能家居的,他们卖的是硬件设备,这些设备会采集数据上报到服务器上,当来自全国范围的数据统一保存在一个表中后,数据量达到了亿级别,所以这种场景比较适合按照城市和地域来拆分。

分库分表实战

为了让大家理解分库分表以及实操,我们通过一个简单的案例来演示一下。代码详见:springboot-split-table-example项目

假设存在一个用户表,用户表的字段如下。

该表主要提供注册、登录、查询、修改等功能。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-8

该表的具体的业务情况如下(需要注意,在进行分表之前,需要了解业务层面对这个表的使用情况,然后再决定使用什么样的方案,否则脱离业务去设计技术方案是耍流氓)

用户端: 前台访问量较大,主要涉及两类请求:

运营端: 主要是运营后台的信息访问,需要支持根据性别、手机号、注册时间、用户昵称等进行分页查询,由于是内部系统,访问量较低,对可用性一致性要求不高。

根据uid进行水平分表

由于99%的请求是基于uid进行用户信息查询,所以毫无疑问我们选择使用uid进行水平分表。那么这里我们采用uid的hash取模方法来进行分表,具体的实施如图6-9所示,根据uid进行一致性hash取模运算得到目标表进行存储。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-9

按照图6-9的结构,分别复制user_info表,重新命名为01~04,如图6-10所示。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-10

如何生成全局唯一id

当完成上述动作后,就需要开始开始落地实施,这里需要考虑在数据添加、修改、删除时,要正确路由到目标数据表,其次是老数据的迁移。

老数据迁移,一般我们是写一个脚本或者一个程序,把旧表中的数据查询出来,然后根据分表规则重新路由分发到新的表中,这里不是很复杂,就不做展开说明,我们重点说一下数据添加/修改/删除的路由。

在实施之前,我们需要先考虑一个非常重要的问题,就是在单个表中,我们使用递增主键来保证数据的唯一性,但是如果把数据拆分到了四个表,每个表都采用自己的递增主键规则,就会存在重复id的问题,也就是说递增主键不是全局唯一的。

我们需要知道一个点是,user_info虽然拆分成了多张表,但是本质上它应该还是一个完整的数据整体,当id存在重复的时候,就失去了数据的唯一性,因此我们需要考虑如何生成一个全局唯一ID。

如何实现全局唯一ID

全局唯一ID的特性就是能够保证ID的唯一性,那么基于这个特性,我们可以轻松找到很多的解决方案。

分布式ID的特性

数据库自增方案

在数据库中专门创建一张序列表,利用数据库表中的自增ID来为其他业务的数据生成一个全局ID,那么每次要用ID的时候,直接从这个表中获取即可。

CREATE TABLE `uid_table`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `business_id` int(11)  NOT NULL,
  PRIMARY KEY (`id`) USING BTREE,
	UNIQUE (business_type) 
) 

在应用程序中,每次调用下面这段代码,就可以持续获得一个递增的ID。

begin;
REPLACE INTO uid_table (business_id) VALUES (2);
SELECT LAST_INSERT_ID();
commit;

其中,replace into是每次删除原来相同的数据,同时加1条,就能保证我们每次得到的就是一个自增的ID

这个方案的优点是非常简单,它也有缺点,就是对于数据库的压力比较大,而且最好是独立部署一个DB,而独立部署又会增加整体的成本,这个在美团的leaf里面设计了一个很巧妙的设计方案,后面再讲

优点:

缺点:

UUID

UUID的格式是: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx 8-4-4-4-12共36个字符,它是一个128bit的二进制转化为16进制的32个字符,然后用4个-连接起来的字符串。

UUID的五种生成方式

在Java中,提供了基于MD5算法的UUID、以及基于随机数的UUID。

优点:

缺点:

雪花算法

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。雪花算法比较常见,在百度的UidGenerator、美团的Leaf中,都有用到雪花算法的实现。

如图6-11所示,表示雪花算法的组成,一共64bit,这64个bit位由四个部分组成。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-11

分库分表之后的数据DML操作

有序需要用到全局id,所以在user_info表需要添加一个唯一id的字段。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-12

配置完成之后,在如下代码中引入signal方法。

@Slf4j
@RestController
@RequestMapping("/users")
public class UserInfoController {

    @Autowired
    IUserInfoService userInfoService;
    SnowFlakeGenerator snowFlakeGenerator=new SnowFlakeGenerator(1,1,1);
    @PostMapping("/batch")
    public void user(@RequestBody List userInfos){
        log.info("begin UserInfoController.user");
        userInfoService.saveBatch(userInfos);
    }

    @PostMapping
    public void signal(@RequestBody UserInfo userInfo){
        Long bizId=snowFlakeGenerator.nextId();
        userInfo.setBizId(bizId);
        String table=ConsistentHashing.getServer(bizId.toString());
        log.info("UserInfoController.signal:{}",table);
        MybatisPlusConfig.TABLE_NAME.set(table);
        userInfoService.save(userInfo);
    }
}

并且,需要增加一个mybatis拦截器,针对user_info表进行拦截和替换,从而实现动态表的路由。

@Configuration
public class MybatisPlusConfig {
    public static ThreadLocal TABLE_NAME = new ThreadLocal<>();

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        interceptor.addInnerInterceptor(paginationInnerInterceptor);
        DynamicTableNameInnerInterceptor dynamicTableNameInnerInterceptor = new DynamicTableNameInnerInterceptor();
        Map tableNameHandlerMap = new HashMap<>();
        tableNameHandlerMap.put("user_info", (sql, tableName) -> TABLE_NAME.get());
        dynamicTableNameInnerInterceptor.setTableNameHandlerMap(tableNameHandlerMap);
        interceptor.addInnerInterceptor(dynamicTableNameInnerInterceptor);
        return interceptor;
    }
}

至此,一个基础的分库分表的演练就完成了,但问题仍然还未完全解决。

非分片键查询

我们对user_info表的分片,是基于biz_id来实现的,也就是意味着如果我们想查询某张表的数据,必须先要使用biz_id路由找到对应的表才能查询到。

那么问题来了,如果查询的字段不是分片键(也就是不是biz_id),比如本次分库分表实战案例中,运营端查询就有根据名字、手机号、性别等字段来查,这时候我们并不知道去哪张表查询这些信息。

非分片键和分片键建立映射关系

第一种解决办法就是,把非分片键和分片键建立映射关系,比如login_name -> biz_id 建立映射,相当于建立一个简单的索引,当基于login_name查询数据时,先通过映射表查询出login_name对应的biz_id,再通过biz_id定位到目标表。

映射表的只有两列,可以成再很多的数据,当数据量过大时,也可以对映射表做水平拆分。 同时这种映射关系其实就是k-v键值对的关系,所以我们可以使用k-v缓存来存储提升性能。

同时因为这种映射关系的变更频率很低,所以缓存命中率很高,性能也很好。

用户端数据库和运营端数据库进行分离

运营端的查询可能不止于单个字段的映射来查询,可能更多的会涉及到一些复杂查询,以及分页查询等,这种查询本身对数据库性能影响较大,很可能影响到用户端对于用户表的操作,所以一般主流的解决方案就是把两个库进行分离。

由于运营端对于数据的一致性和可用性要求不是很高,也不需要实时访问数据库,所以我们可以把C端用户表的数据同步到运营端的用户表,而且用户表可以不需要做分表操作,直接全量查表即可。

当然,如果运营端的操作性能实在是太慢了,我们还可以采用ElasticSearch搜索引擎来满足后台复杂查询的需求。

实际应用中会遇到的问题

在实际应用中,并不是一开始就会想到未来会对这个表做拆分,因此很多时候我们面临的问题是在数据量已经达到一定瓶颈的时候,才开始去考虑这个问题。

所以分库分表最大的难点不是在于拆分的方法论,而是在运行了很长时间的数据库中,如何根据实际业务情况选择合适的拆分方式,以及在拆分之前对于数据的迁移方案的思考。而且,在整个数据迁移和拆分过程中,系统仍然需要保持可用。

对于运行中的表的分表,一般会分为三个阶段。

阶段一,新老库双写

由于老的数据表肯定没有考虑到未来分表的设计,同时随着业务的迭代,可能有些模型也需要优化,因此会设计一个新的表来承载老的数据,而这个过程中,需要做几件事情

阶段二,以新的模型为准

到了第二个阶段,历史数据已经导完了,并且校验数据没有问题。

阶段三,结束双写

到了第三个阶段,说明数据已经完全迁移好了,因此。

分库分表后带来的问题

分库分表带来性能提升的好处的同时,也带来了很多的麻烦,

分布式事务问题

分库分表之后,原本在一个库中的事务,变成了跨越多个库,如何保证跨库数据的一致性问题,也是一个常见的难题。如图6-13所示,用户创建订单时,需要在订单库中保存一条订单记录,并且修改库存库中的商品库存,这里就涉及到跨库事务的一致性问题。也就是说我怎么保证当前两个事务操作要么同时成功,要么同时失败。

千万级并发架构下,关系型数据库如何优化?大厂如何做分库分表的

图6-13

跨库查询

比如查询在合同信息的时候要关联客户数据,由于是合同数据和客户数据是在不同的数据库,那么我们肯定不能直接使用join的这种方式去做关联查询。

我们有几种主要的解决方案:

上面的思路都是通过合理的数据分布避免跨库关联查询,实际上在我们的业务中,也是尽量不要用跨库关联查询,如果出现了这种情况,就要分析一下业务或者数据拆分是不是合理。如果还是出现了需要跨库关联的情况,那我们就只能用最后一种办法。

在不同的数据库节点把符合条件数据的数据查询出来,然后重新组装,返回给客户端。

排序、翻页、函数计算等问题

跨节点多库进行查询时,会出现limit分页,order by排序的问题。比如有两个节点,节点1存的是奇数id=1,3,5,7,9……;节点2存的是偶数id=2,4,6,8,10……

执行select * from user_info order by id limit 0,10

需要在两个节点上各取出10条,然后合并数据,重新排序。

max、min、sum、count之类的函数在进行计算的时候,也需要先在每个分片上执行相应的函数,然后将各个分片的结果集进行汇总和再次计算,最终将结果返回。

全局唯一ID

全局唯一id的问题,前面已经说了,水平分表之后,需要考虑全局唯一id设计问题。

多数据源的问题

分库分表之后,难免会存在一个应用配置多个数据源。

另外,数据库层面有可能会设计读写分离的方案,也使得一个应用会访问多个数据源,并且还需要实现读写分离的动态路由。

而这些问题在每个应用系统中都会存在并且需要解决,所以为了提供统一的分库分表相关问题的解决方案,引入了很多的开源技术。

分库分表解决方案

目前市面上分库分表的中间件相对来说说比较多,比如

目前很多公司选择较多的是Mycat或者Sharding-Sphere,所以我重点介绍Sharding-Sphere的使用和原理。

对于同类技术的选择,无非就是看社区活跃度、技术的成熟度、以及功能和当前需求是否匹配。

原文链接:https://www.cnblogs.com/mic112/p/15449172.html

原作者:跟着Mic学架构

展开阅读全文

页面更新:2024-06-17

标签:关系   数据库   分布式   节点   字段   全局   算法   架构   性能   水平   目标   方式   业务   时间   数据   系统   科技

1 2 3 4 5

上滑加载更多 ↓
推荐阅读:
友情链接:
更多:

本站资料均由网友自行发布提供,仅用于学习交流。如有版权问题,请与我联系,QQ:4156828  

© CopyRight 2020-2024 All Rights Reserved. Powered By 71396.com 闽ICP备11008920号-4
闽公网安备35020302034903号

Top