天天看点

大厂原来都这么对MySQL分库分表!(下)选择分片键5 分库分表后的难题总结

选择分片键

分库分表的问题就是引入了分库分表键,即分片键,也就是我们对数据库做分库分表所依据的字段。

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

解决方案

比如在用户库中使用 ID 作为分区键,这时若需按昵称查询用户,可按昵称作为分区键再做次拆分,但这样会极大增加存储成本,若以后还需要按注册时间查询,怎么办呢,再做次拆分?

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

尽量避免跨分区查询的发生(无法完全避免)

尽量使各个分片中的数据平均

如何存储无需分片的表

  • 每个分片中存储一份相同的数据

    对于数据量不大且并不经常被更新的字典类表,经常需要和分区表一起关联查询,每个分片中存储一份冗余的数据可以更好提高查询效率,维护其一致性就很重要了。

  • 使用额外的节点统一存储

    没有冗余问题,但是查询效率较差,需要汇总

在节点上部署分片

  • 每个分片使用单一数据库,并且数据库名也相同

    结构也保持相同,和单一节点时的一致

  • 将多个分片表存储在一个数据库中,并在表名上加入分片号后缀
  • 在一个节点中部署多个数据库,每个数据库包含一个切片

5 分库分表后的难题

5.1 全局唯一ID生成方案

数据库自增ID

使用

auto_increment_increment

auto_increment_offset

系统变量让MySQL以期望的值和偏移量来增加auto_increment列的值。

优点

最简单,不依赖于某节点,较普遍采用,但需要非常仔细的配置服务器哦!

缺点

单点风险、单机性能瓶颈。不适用于一个节点包含多个分区表的场景。

数据库集群并设置相应步长(Flickr方案)

在一个全局数据库节点中创建一个包含

auto_increment

列的表,应用通过该表生成唯一数字。

  • 高可用、ID较简洁。
  • 需要单独的数据库集群。

Redis缓存

避免了MySQL性能低的问题。

Snowflake(雪花算法)

  • 高性能高可用、易拓展
  • 需要独立的集群以及ZK

各种GUID、Random算法

  • 简单
  • 生成ID较长,且有重复几率

业务字段

为减少运营成本并减少额外风险,排除所有需要独立集群的方案,采用了带有业务属性的方案:

时间戳+用户标识码+随机数

  • 方便、成本低
  • 基本无重复的可能
  • 自带分库规则,这里的用户标识码即为

    userID的后四位

    ,在查询场景,只需订单号即可匹配到相应库表而无需用户ID,只取四位是希望订单号尽可能短,评估后四位已足。
  • 可排序,因为时间戳在最前

  • 长度稍长,性能要比int/bigint的稍差。

事务

分库分表后,由于数据存到了不同库,数据库事务管理出现困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。

比如美团,是将整个订单领域聚合体切分,维度一致,所以对聚合体的事务是支持的。

数据库特性

多表的 join 在单库时可通过一个 SQL 完成,但拆分到多个数据库后就无法跨库执行 SQL,好在 join 语法一般都被禁止使用,都是把两个表的数据取出后在业务代码里做筛选。

分库分表后,难免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上,这时,表的关联操作将受到限制,我们无法join位于不同分库的表,也无法join分表粒度不同的表,结果原本一次询能够完成的业务,可能需要多次查询才能完成。

垂直切分后,就跟join说拜拜了;水平切分后,查询的条件一定要在切分的维度内。

比如查询具体某个用户下的订单等;

禁止不带切分的维度的查询,即使中间件可以支持这种查询,可以在内存中组装,但是这种需求往往不应该在在线库查询,或者可以通过其他方法转换到切分的维度来实现。

在未分库分表前,查询数据总数时只需 SQL 执行 count(),现在数据被分散到多个库表,就要考虑其他方案,比方说将计数的数据单独存储在一张表或记录在 Redis。

额外的数据管理和运算压力

额外的数据管理负担,最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。

例如,对于一个记录用户成绩的用户数据表userTable,业务要求查出成绩最好的100位,在进行分表前,只需一个order by即可。但分表后,将需要n个order by语句,分别查出每一个分表前100名用户数据,然后再对这些数据进行合并计算,才能得出结果。

总结

并非所有表都需要水平拆分,要看增长的类型、速度,水平拆分是大招,拆分后会增加开发复杂度,不到万不得已不用。

拆分维度的选择很重要,要尽可能在解决拆分前问题的基础上,便于开发。

参考

https://tech.meituan.com/2016/11/18/dianping-order-db-sharding.html

《Java工程师面试突击第1季》