租车网站 模板,投资网站php源码,asp.net 网站访问量,wordpress什么主题好从公众号转载#xff0c;关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、高性能数据库简介
1.高性能数据库方式
读写分离#xff1a;将访问压力分散到集群中的多个节点#xff0c;没有分散存储压力
分库分表… 从公众号转载关注微信公众号掌握更多技术动态 --------------------------------------------------------------- 一、高性能数据库简介
1.高性能数据库方式
读写分离将访问压力分散到集群中的多个节点没有分散存储压力
分库分表既可以分散访问压力又可以分散存储压力
2.为啥不用表分区 如果SQL不走分区键很容易出现全表锁 在分区表实施关联查询就是一个灾难 分库分表自己掌控业务场景与访问模式可控分区表工程师写了一个SQL自己无法确定MySQL是怎么玩的不可控 二、读写分离——提升数据库读性能
可以缓解订单系统、账户系统、购物车系统等等功能mysql的并发压力
读写分离的基本原理是将数据库的读写操作分散到不同的节点 1.读写分离的基本实现 数据库服务器搭建主从集群一主一从、或者一主多从数据库主机负责读写操作从机只负责读操作。数据库主机通过复制将数据同步到从机每台数据库服务器都存储了所有的业务数据。业务服务器将写操作发给数据库主机将读操作发给数据库从机。 需要注意的是这里用的是“主从集群”而不是“主备集群”。“从机”的“从”可以理解为“仆从”仆从是要帮主人干活的“从机”是需要提供读数据的功能的而“备机”一般被认为仅仅提供备份功能不提供访问功能。
2.读写分离引起的复杂性
(1)复制延迟 一般会把从库落后的时间作为一个重点的数据库指标做监控和报警正 常的时间是在毫秒级别一旦落后的时间达到了秒级别就需要告警了。 以 MySQL 为例主从复制延迟可能达到 1 秒如果有大量数据同步延迟 1 分钟也是有可能的。主从复制延迟会带来一个问题如果业务服务器将数据写入到数据库主服务器后立刻1 秒内进行读取此时读操作访问的是从机主机还没有将数据复制过来到从机读取数据是读不到最新数据的业务上就可能出现问题。解决主从复制延迟有几种常见的方法 写操作后的读操作指定发给数据库主服务器(缓存标记法)。例如注册账号完成后登录时读取账号的读操作也发给数据库主服务器。这种方式和业务强绑定对业务的侵入和影响较大如果哪个新来的程序员不知道这样写代码就会导致一个 bug。可以利用一个缓存记录必须读主的数据。当写请求发生时 写主库 将哪个库哪个表哪个主键三个信息拼装一个key设置到cache里这条记录的超时时间设置为“主从同步时延” 查询时 cache里有这个key说明1s内刚发生过写请求数据库主从同步可能还没有完成此时就应该去主库查询 cache里没有这个key说明最近没有发生过写请求此时就可以去从库查询 读从机失败后再读一次主机。这就是通常所说的“二次读取”二次读取和业务无绑定只需要对底层数据库访问的 API 进行封装即可实现代价较小不足之处在于如果有很多二次读取将大大增加主机的读操作压力。 关键业务读写操作全部指向主机非关键业务采用读写分离。例如对于一个用户管理系统来说注册 登录的业务读写操作全部访问主机用户的介绍、爱好、等级等业务可以采用读写分离因为即使用户改了自己的自我介绍在查询时却看到了自我介绍还是旧的业务影响与不能登录相比就小很多还可以忍受。 写操作完成后跳转到无关页面类似订单支付的“支付完成”页面其实这个页面没有任何有效的信息就是告诉你支付成功然后再放一些广告什么的。你如果想再看刚刚支付完成的订单需要手动点一下这样就很好地规避了主从同步延迟的问题。 (2)分配机制 将读写操作区分开来然后访问不同的数据库服务器一般有两种方式程序代码封装和中间件封装。由于数据库中间件的复杂度要比程序代码封装高出一个数量级一般情况下建议采用程序语言封装的方式或者使用成熟的开源数据库中间件。 程序代码封装。程序代码封装指在代码中抽象一个数据访问层实现读写操作分离和数据库服务器连接的管理。 中间件封装。中间件封装指的是独立一套系统出来实现读写操作分离和数据库服务器连接的管理。对于业务服务器来说访问中间件和访问数据库没有区别。中间件需要支持多种编程语言因为数据库中间件对业务服务器提供的是标准 SQL 接口。数据库中间件要支持完整的 SQL 语法和数据库服务器的协议例如MySQL 客户端和服务器的连接协议实现比较复杂细节特别多很容易出现 bug需要较长的时间才能稳定。数据库中间件自己不执行真正的读写操作但所有的数据库操作请求都要经过中间件中间件的性能要求也很高。 (3)区分连接池 数据库连接池需要区分读连接池写连接池 如果要保证读高可用读连接池要实现故障自动转移 3.从库的数量 是不是无限制地增加从库的数量就可以抵抗大量的并发呢实际上并不 是的。因为随着从库数量增加从库连接上来的 IO 线程比较多主库也需要创建同样多的 log dump 线程来处理复制的请求对于主库资源消耗比较高同时受限于主库的网络带 宽所以在实际使用中一般一个主库最多挂 35 个从库。 三、分库分表(最后选择的优化方案)——数据库数据量大
分表分库规则在设计时需要考虑数据分布均匀避免单库或者单表数据倾斜。
1.分库分表简介 如果预计三年后的数据量根本达不到这个级别请不要在创建表时就分库分表。单表行数超过1000万行或者单表容量超过4GB才推荐分库分表。
(1)分库分表的原因 当数据量达到千万甚至上亿条的时候单台数据库服务器的存储能力会成为系统的瓶颈主要体现在这几个方面 数据量太大读写的性能会下降即使有索引索引也会变得很大性能同样会下降。 数据文件会变得很大数据库备份和恢复需要耗费很长时间。 数据文件越大极端情况下丢失数据的风险越高例如机房火灾导致数据库主备机都发生故障。 基于上述原因单个数据库服务器存储的数据量不能太大需要控制在一定的范围内。为了满足业务数据存储的需求就需要将存储分散到多台数据库服务器上。
(2)分库分表的选择 数据量大就分表并发高就分库。查询慢只要减少每次查询的数据总量就可以了分表就可以解决问题应对高并发的问题一个数据库实例撑不住就把并发请求分散到多个实例中去。 IO瓶颈 第一种磁盘读IO瓶颈热点数据太多数据库缓存放不下每次查询会产生大量的IO降低查询速度-分库和垂直分表 第二种网络IO瓶颈请求的数据太多网络带宽不够 -分库 CPU瓶颈 第一种SQl问题如SQL中包含join,group by, order by非索引字段条件查询等增加CPU运算的操作-SQL优化建立合适的索引在业务Service层进行业务计算。 第二种单表数据量太大查询时扫描的行太多SQl效率低增加CPU运算的操作。-水平分表。
(3)分库分表步骤 根据容量当前容量和增长量评估分库或分表个数 - 选key均匀- 分表规则hash或range等- 执行一般双写- 扩容问题尽量减少数据的移动。 2.业务分库 业务分库也叫垂直分库指的是按照业务模块将数据分散到不同的数据库服务器。例如一个简单的电商网站包括用户、商品、订单三个业务模块可以将用户数据、商品数据、订单数据分开放到三台不同的数据库服务器上。这种方式在微服务架构中非常常用。微服务架构的核心思想是将一个完整的应用按照业务功能拆分成多个可独立运行的子系统这些子系统称为“微服务”。垂直分库的理念与微服务的理念不谋而合可以将原本完整的数据按照微服务拆分系统的方式拆分成多个独立的数据库使得每个微服务系统都有各自独立的数据库从而可以避免单个数据库节点压力过大影响系统的整体性能如下图所示。 以字段为依据按照一定策略hash、range等将一个库中的数据拆分到多个库中。系统绝对并发量上来了分表难以根本上解决问题并且还没有明显的业务归属来垂直分库的情况下。
(1)join 操作问题 业务分库后原本在同一个数据库中的表分散到不同数据库中导致无法使用 SQL 的 join 查询。 例如“查询购买了化妆品的用户中女性用户的列表”这个功能虽然订单数据中有用户的 ID 信息但是用户的性别数据在用户数据库中如果在同一个库中简单的 join 查询就能完成但现在数据分散在两个不同的数据库中无法做 join 查询只能采取先从订单数据库中查询购买了化妆品的用户 ID 列表然后再到用户数据库中查询这批用户 ID 中的女性用户列表这样实现就比简单的 join 查询要复杂一些。 系统中所有模块都可能依赖的一些表为了避免库join查询可以将这类表在每个数据库中都保存一份。这些数据通常很少修改所以不必担心一致性的问题。
(2)事务问题 原本在同一个数据库中不同的表可以在同一个事务中修改业务分库后表分散到不同的数据库中无法通过事务统一修改。虽然数据库厂商提供了一些分布式事务的解决方案例如MySQL 的 XA但性能实在太低与高性能存储的目标是相违背的。 例如用户下订单的时候需要扣商品库存如果订单数据和商品数据在同一个数据库中我们可以使用事务来保证扣减商品库存和生成订单的操作要么都成功要么都失败但分库后就无法使用数据库事务了需要业务程序自己来模拟实现事务的功能。例如先扣商品库存扣成功后生成订单如果因为订单数据库异常导致生成订单失败业务程序又需要将商品库存加上而如果因为业务程序自己异常导致生成订单失败则商品库存就无法恢复了需要人工通过日志等方式来手工修复库存异常。
(3)成本问题 业务分库同时也带来了成本的代价本来 1 台服务器搞定的事情现在要 3 台如果考虑备份那就是 2 台变成了 6 台。基于上述原因对于小公司初创业务并不建议一开始就这样拆分主要有几个原因 初创业务并没有真正的存储和访问压力业务分库并不能为业务带来价值。业务分库后表之间的 join 查询、数据库事务无法简单实现了。 业务分库后因为不同的数据要读写不同的数据库代码中需要增加根据数据类型映射到不同数据库的逻辑增加了工作量。 2.分表 将不同业务数据分散存储到不同的数据库服务器能够支撑百万甚至千万用户规模的业务但如果业务继续发展同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如淘宝的几亿用户数据如果全部存放在一台数据库服务器的一张表中肯定是无法满足性能要求的此时就需要对单表数据进行拆分。单表数据拆分有两种方式垂直分表和水平分表(表的数量一般是2的n次幂)。示意图如下 单表进行切分后是否要将切分后的多个表分散在不同的数据库服务器中可以根据实际的切分效果来确定。因为单表切分为多表后新的表即使在同一个数据库服务器中也可能带来可观的性能提升如果性能能够满足业务要求是可以不拆分到多台数据库服务器的毕竟业务分库也会引入很多复杂性的问题如果单表拆分为多表后单台服务器依然无法满足性能要求那就不得不再次进行业务分库的设计了。
(1)垂直分表 垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如前面示意图中的 nickname 和 description 字段假设我们是一个婚恋网站用户在筛选其他用户的时候主要是用 age 和 sex 两个字段进行查询而 nickname 和 description 两个字段主要用于展示一般不会在业务查询中用到。description 本身又比较长因此我们可以将这两个字段独立到另外一张表中这样在查询 age 和 sex 时就能带来一定的性能提升。如果属性过多可以有多个扩展表。 将长度较短访问频率较高的属性尽量放在一个表里这个表暂且称为主表 将字段较长访问频率较低的属性尽量放在一个表里这个表暂且称为扩展表 经常一起访问的属性也可以放在一个表里
优点 可以使得行数据变小一个数据块( Block )就能存放更多的数据在查询时就会减少 I/O 次数(每次查询时读取的 Block 就少) 可以达到最大化利用 Cache 的目的具体在垂直拆分的时候可以将不常变的字段放一起将经常改变的放一起
缺点 主键出现冗余需要管理冗余列 会引起表连接 JOIN 操作增加 CPU 开销可以通过在业务服务器上进行 join 来减少数据库压力 依然存在单表数据量过大的问题需要水平拆分 事务处理复杂
垂直分表引入的复杂性主要体现在表操作的数量要增加。例如原来只要一次查询就可以获取 name、age、sex、nickname、description现在需要两次查询一次查询获取 name、age、sex另外一次查询获取 nickname、description。 (2)水平分表 水平分表适合表行数特别大的表有的公司要求单表行数超过 5000 万就必须进行分表这个数字可以作为参考但并不是绝对标准关键还是要看表的访问性能。对于一些比较复杂的表可能超过 1000 万就要分表了而对于一些简单的表即使存储数据超过 1 亿行也可以不分表。(预计三年内可达到单表最大容量2GB) 当然如果拆分出来的表都存储在同一个数据库节点上那么当请求量过大的时候毕竟单台服务器的处理能力是有限的数据库仍然会成为系统的瓶颈所以为了解决这个问题就出现了水平数据分片的解决方案。 水平拆分的优点是: 不存在单库大数据和高并发的性能瓶颈 应用端改造较少 提高了系统的稳定性和负载能力
缺点是 分片事务一致性难以解决 跨节点 Join 性能差逻辑复杂 数据多次扩展难度跟维护量极大
核心概念 数据节点:数据节点是数据分片中一个不可再分的最小单元表它由数据源名称和数据表组成例如DB_1.t_order_1、DB_2.t_order_2 就表示一个数据节点。 逻辑表逻辑表是指具有相同结构的水平拆分表的逻辑名称。比如我们将订单表t_order 分表拆分成 t_order_0 ··· t_order_9等10张表这时我们的数据库中已经不存在 t_order这张表取而代之的是若干的t_order_n表。在代码中SQL依然按 t_order来写而在执行逻辑SQL前将其解析成对应的数据库真实执行的SQL。此时 t_order 就是这些拆分表的逻辑表。 广播表:广播表是一类特殊的表其表结构和数据在所有分片数据源中均完全一致。与拆分表相比广播表的数据量较小、更新频率较低通常用于字典表或配置表等场景。由于其在所有节点上都有副本因此可以大大降低JOIN关联查询的网络开销提高查询效率。需要注意的是对于广播表的修改操作需要保证同步性以确保所有节点上的数据保持一致。比如和订单表关联的城市表 绑定表绑定表是那些具有相同分片规则的一组分片表由于分片规则一致所产生的的数据落地位置相同在JOIN联合查询时能有效避免跨库操作。比如t_order 订单表和 t_order_item 订单项目表都以 order_no 字段作为分片键并且使用 order_no 进行关联因此两张表互为绑定表关系。
①Sharding Key选择 分库分表还有一个重要的问题是选择一个合适的列或者说是属性作为分表的依据这个属性一般称为 Sharding Key。
比如归档历史订单的方法它的 ShardingKey 就是订单完成时间。每次查询的时候查询条件中必须带上这个时间我们的程序就知道三个月以前的数据查订单历史表三个月内的数据查订单表这就是一个简单的按照时间范围来分片的算法。
选择这个 Sharding Key 最重要的参考因素是业务是如何访问数据的。
②路由算法 水平分表后某条数据具体属于哪个切分后的子表需要增加路由算法进行计算这个算法会引入一定的复杂性。常见的路由算法有
范围路由选取有序的数据列例如整形、时间戳等作为路由的条件不同分段分散到不同的数据库表中。以最常见的用户 ID 为例路由算法可以按照 1000000 的范围大小进行分段1 ~ 999999 放到数据库 1 的表中1000000 ~ 1999999 放到数据库 2 的表中以此类推。
范围路由设计的复杂点主要体现在分段大小的选取上分段太小会导致切分后子表数量过多增加维护复杂度分段太大可能会导致单表依然存在性能问题一般建议分段大小在 100 万至 2000 万之间具体需要根据业务选取合适的分段大小。
范围路由的优点是可以随着数据的增加平滑地扩充新的表。例如现在的用户是 100 万如果增加到 1000 万只需要增加新的表就可以了原有的数据不需要动。
范围路由的一个比较隐含的缺点是分布不均匀假如按照 1000 万来进行分表有可能某个分段实际存储的数据量只有 1000 条而另外一个分段实际存储的数据量有 900 万条。
Hash 路由选取某个列或者某几个列组合也可以的值进行 Hash 运算然后根据 Hash 结果分散到不同的数据库表中。同样以用户 ID 为例假如我们一开始就规划了 10 个数据库表路由算法可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号ID 为 985 的用户放到编号为 5 的子表中ID 为 10086 的用户放到编号为 6 的字表中。
Hash 路由设计的复杂点主要体现在初始表数量的选取上表数量太多维护比较麻烦表数量太少又可能导致单表性能存在问题。而用了 Hash 路由后增加字表数量是非常麻烦的所有数据都要重分布。
Hash 路由的优缺点和范围路由基本相反Hash 路由的优点是表分布比较均匀缺点是扩充新的表很麻烦所有数据都要重分布。
配置路由配置路由就是路由表用一张独立的表来记录路由信息。同样以用户 ID 为例我们新增一张 user_router 表这个表包含 user_id 和 table_id 两列根据 user_id 就可以查询对应的 table_id。
配置路由设计简单使用起来非常灵活尤其是在扩充表的时候只需要迁移指定的数据然后修改路由表就可以了。
配置路由的缺点就是必须多查询一次会影响整体性能而且路由表本身如果太大例如几亿条数据性能同样可能成为瓶颈如果我们再次将路由表分库分表则又面临一个死循环式的路由算法选择问题。
(3)带来的问题
①join 操作
水平分表后数据分散在多个表中如果需要与其他表进行 join 查询需要在业务代码或者数据库中间件中进行多次 join 查询然后将结果合并。
②count() 操作
水平分表后虽然物理上数据分散到多个表中但某些业务逻辑上还是会将这些表当作一个表来处理。例如获取记录总数用于分页或者展示水平分表前用一个 count() 就能完成的操作在分表后就没那么简单了。常见的处理方式有下面两种 count() 相加具体做法是在业务代码或者数据库中间件中对每个表进行 count() 操作然后将结果相加。这种方式实现简单缺点就是性能比较低。例如水平分表后切分为 20 张表则要进行 20 次 count(*) 操作如果串行的话可能需要几秒钟才能得到结果。 记录数表具体做法是新建一张表假如表名为“记录数表”包含 table_name、row_count 两个字段每次插入或者删除子表数据成功后都更新“记录数表”。这种方式获取表记录数的性能要大大优于 count() 相加的方式因为只需要一次简单查询就可以获取数据。缺点是复杂度增加不少对子表的操作要同步操作“记录数表”如果有一个业务逻辑遗漏了数据就会不一致且针对“记录数表”的操作和针对子表的操作无法放在同一事务中进行处理异常的情况下会出现操作子表成功了而操作记录数表失败同样会导致数据不一致。此外记录数表的方式也增加了数据库的写压力因为每次针对子表的 insert 和 delete 操作都要 update 记录数表所以对于一些不要求记录数实时保持精确的业务也可以通过后台定时更新记录数表。定时更新实际上就是“count() 相加”和“记录数表”的结合即定时通过 count() 相加计算表的记录数然后更新记录数表中的数据。
③order by 操作
水平分表后数据分散到多个子表中排序操作无法在数据库中完成只能由业务代码或者数据库中间件分别查询每个子表中的数据然后汇总进行排序。
④分页处理
水平分库后分页查询的问题比较突出因为有些分页查询需要遍历所有库。举个例子假设要按时间顺序展示某个商家的订单每页有 100 条记录由于是按商家查询需要遍历所有数据库。假设库数量是 8我们来看下水平分库后的分页逻辑
如果是取第 1 页数据需要从每个库里按时间顺序取前 100 条记录8 个库汇总后 共有 800 条然后对这 800 条记录在应用里进行二次排序最后取前 100 条如果取第 10 页数据则需要从每个库里取前 1000100*10条记录汇总后共有 8000 条记录然后对这 8000 条记录进行二次排序后取第 900 到 1000 之间的 记录。
在分库情况下对于每个数据库要取更多的记录并且汇总后还要在 应用里做二次排序越是靠后的分页系统要耗费更多的内存和执行时间。而在不分库的情 况下无论取哪一页只要从单个 DB 里取 100 条记录即可也无需在应用内部做二 次排序非常简单。那么如何解决分库情况下的分页问题呢这需要具体情况具体分析
如果是为前台应用提供分页可以限定用户只能看到前面 n 页这个限制在业务上 也是合理的一般看后面的分页意义不大如果一定要看可以要求用户缩小范围重新 查询
如果是后台批处理任务要求分批获取数据可以加大分页的大小比如设定每次获 取 5000 条记录这样可以有效减少分页的访问次数分库设计时一般还有配套的大数据平台负责汇总所有分库的记录所以有些分页查询可以考虑走大数据平台。
场景1前提取余法
原序列(12345678)需要取出limit 2,2 即(34)
第1次查询
(1357) - limit 2,2 - 改写成 limit 1,2 - (35)
(2468) - limit 2,2 - 改写成 limit 1,2 - (46)
最小值为3
第2次查询
(1357) - between 3 and 5 - (35)
(2468) - between 3 and 6 - (46)
将第2次查询的结果合并
(3456) -取头开始取pageSize即2个元素 - (34) 正确
场景2前提取余法
原序列(12345678)需要取出limit 1,2 即(23)
第1次查询
(1357) - limit 1,2 - 改写成 limit 0,2 - (13) --注因为1/2除不尽这里向下取整了
(2468) - limit 1,2 - 改写成 limit 0,2 - (24)
最小值为1
第2次查询
(1357) - between 1 and 3 - (13)
(2468) - between 1 and 4 - (24)
将上面的结果合并
(1234) - (23) (注起始点第1次查询改写时向下取整了所以这里要向移1位从第2个数字开始取pagesize条数据)
⑤非partition key的查询问题
端上除了partition key只有一个非partition key作为条件查询 端上除了partition key不止一个非partition key作为条件查询 (4)数据冗余
①如何进行数据冗余
方案一服务同步双写 由服务层同步写冗余数据 业务方调用服务新增数据 服务先插入T1数据 服务再插入T2数据 服务返回业务方新增数据成功
优点 不复杂服务层由单次写变两次写 数据一致性相对较高因为双写成功才返回
缺点 请求的处理时间增加要插入两次时间加倍 数据仍可能不一致例如第二步写入T1完成后服务重启则数据不会写入T2
方案二服务异步双写
系统对处理时间比较敏感 数据的双写并不再由服务来完成服务层异步发出一个消息通过消息总线发送给一个专门的数据复制服务来写入冗余数据 业务方调用服务新增数据 服务先插入T1数据 服务向消息总线发送一个异步消息发出即可不用等返回通常很快就能完成 服务返回业务方新增数据成功 消息总线将消息投递给数据同步中心 数据同步中心插入T2数据
优点 请求处理时间短只插入1次
缺点 系统的复杂性增加了多引入了一个组件消息总线和一个服务专用的数据复制服务 因为返回业务线数据插入成功时数据还不一定插入到T2中因此数据有一个不一致时间窗口这个窗口很短最终是一致的 在消息总线丢失消息时冗余表数据会不一致
不管是服务同步双写还是服务异步双写服务都需要关注“冗余数据”带来的复杂性。如果想解除“数据冗余”对系统的耦合引出常用的第三种方案。
方案三线下异步双写 为了屏蔽“冗余数据”对服务带来的复杂性数据的双写不再由服务层来完成而是由线下的一个服务或者任务来完成 业务方调用服务新增数据 服务先插入T1数据 服务返回业务方新增数据成功 数据会被写入到数据库的log中 线下服务或者任务读取数据库的log 线下服务或者任务插入T2数据
优点 数据双写与业务完全解耦 请求处理时间短只插入1次
缺点 返回业务线数据插入成功时数据还不一定插入到T2中因此数据有一个不一致时间窗口这个窗口很短最终是一致的 数据的一致性依赖于线下服务或者任务的可靠性
②如何进行数据冗余
高并发的情况下实时一致性很难方法论是最终一致性。实现方式是异步检测异步修复。
方案一线下扫描全量数据法 线下启动一个离线的扫描工具不停的比对正表T1和反表T2如果发现数据不一致就进行补偿修复。
优点 比较简单开发代价小 线上服务无需修改修复工具与线上服务解耦
缺点 扫描效率低会扫描大量的“已经能够保证一致”的数据 由于扫描的数据量大扫描一轮的时间比较长即数据如果不一致不一致的时间窗口比较长 方案二线下扫描增量数据法
只扫描“可能存在不一致可能性”的数据而不是每次扫描全部数据以提高效率 每次只扫描增量的日志数据就能够极大提高效率缩短数据不一致的时间窗口如 写入正表T1 第一步成功后写入日志log1 写入反表T2 第二步成功后写入日志log2
当然我们还是需要一个离线的扫描工具不停的比对日志log1和日志log2如果发现数据不一致就进行补偿修复
优点 虽比方法一复杂但仍然是比较简单的 数据扫描效率高只扫描增量数据
缺点 线上服务略有修改代价不高多写了2条日志 虽然比方法一更实时但时效性还是不高不一致窗口取决于扫描的周期 方案三线上实时检测“消息对”法
实时检测一致性并进行修复 这次不是写日志了而是向消息总线发送消息 写入正表T1 第一步成功后发送消息msg1 写入反表T2 第二步成功后发送消息msg2
这次不是需要一个周期扫描的离线工具了而是一个实时订阅消息的服务不停的收消息。
假设正常情况下msg1和msg2的接收时间应该在3s以内如果检测服务在收到msg1后没有收到msg2就尝试检测数据的一致性不一致时进行补偿修复。
优点 效率高 实时性高
缺点 方案比较复杂上线引入了消息总线这个组件 线下多了一个订阅总线的检测服务 (5) SQL解析
分库分表后在应用层面执行一条 SQL 语句时通常需要经过以下六个步骤
SQL 解析 - 执⾏器优化 - SQL 路由 - SQL 改写 - SQL 执⾏ - 结果归并 。 ①SQL解析
SQL解析过程分为词法解析和语法解析两步比如下边查询用户订单的SQL先用词法解析将这条SQL拆解成不可再分的原子单元。在根据不同数据库方言所提供的字典将这些单元归类为关键字表达式变量或者操作符等类型。
SELECT order_no FROM t_order where order_status 0 and user_id 10086
接着语法解析会将拆分后的SQL关键字转换为抽象语法树通过对抽象语法树遍历提炼出分片所需的上下文上下文包含查询字段信息Field、表信息Table、查询条件Condition、排序信息Order By、分组信息Group By以及分页信息Limit等并标记出 SQL中有可能需要改写的位置。 ②SQL路由
通过上边的SQL解析得到了分片上下文数据在匹配用户配置的分片策略和算法就可以运算生成路由路径将 SQL 语句路由到相应的数据节点上。
简单点理解就是拿到分片策略中配置的分片键等信息在从SQL解析结果中找到对应分片键字段的值计算出 SQL该在哪个库的哪个表中执行SQL路由又根据有无分片健分为 分片路由 和 广播路由。 有分⽚键的路由叫分片路由细分为直接路由、标准路由和笛卡尔积路由这3种类型。
标准路由
标准路由是最推荐也是最为常⽤的分⽚⽅式它的适⽤范围是不包含关联查询或仅包含绑定表之间关联查询的SQL。
当 SQL分片健的运算符为 时路由结果将落⼊单库表当分⽚运算符是 BETWEEN 或 IN 等范围时路由结果则不⼀定落⼊唯⼀的库表因此⼀条逻辑SQL最终可能被拆分为多条⽤于执⾏的真实SQL。
SELECT * FROM t_order where t_order_id in (1,2)
SQL路由处理后
SELECT * FROM t_order_0 where t_order_id in (1,2) SELECT * FROM t_order_1 where t_order_id in (1,2)
直接路由
直接路由是直接将SQL路由到指定⾄库、表的一种分⽚方式而且直接路由可以⽤于分⽚键不在SQL中的场景还可以执⾏包括⼦查询、⾃定义函数等复杂情况的任意SQL。
笛卡尔积路由
笛卡尔路由是由⾮绑定表之间的关联查询产生的比如订单表t_order 分片键是t_order_id 和用户表t_user分片键是t_order_id 两个表的分片键不同要做联表查询会执行笛卡尔积路由查询性能较低尽量避免走此路由模式。 SELECT * FROM t_order_0 t LEFT JOIN t_user_0 u ON u.user_id t.user_id WHERE t.user_id 1SELECT * FROM t_order_0 t LEFT JOIN t_user_1 u ON u.user_id t.user_id WHERE t.user_id 1SELECT * FROM t_order_1 t LEFT JOIN t_user_0 u ON u.user_id t.user_id WHERE t.user_id 1SELECT * FROM t_order_1 t LEFT JOIN t_user_1 u ON u.user_id t.user_id WHERE t.user_id 1
无分⽚键的路由又叫做广播路由可以划分为全库表路由、全库路由、 全实例路由、单播路由和阻断路由这 5种类型。
③SQL改写
SQL经过解析、优化、路由后已经明确分片具体的落地执行的位置接着就要将基于逻辑表开发的SQL改写成可以在真实数据库中可以正确执行的语句。比如查询 t_order 订单表我们实际开发中 SQL是按逻辑表 t_order 写的。
SELECT * FROM t_order
这时需要将分表配置中的逻辑表名称改写为路由之后所获取的真实表名称。 SELECT * FROM t_order_n
④结果归并
将从各个数据节点获取的多数据结果集合并成一个大的结果集并正确的返回至请求客户端称为结果归并。而我们SQL中的排序、分组、分页和聚合等语法均是在归并后的结果集上进行操作的。
3.水平分库分表
以字段为依据按照一定策略hash、range等将一个库中的数据拆分到多个库中。场景系统绝对并发量上来了分表难以根本上解决问题并且还没有明显的业务归属来垂直分库。
水平数据分片与数据分片区别在于水平数据分片首先将数据表进行水平拆分然后按照某一分片规则存储在多台数据库服务器上。从而将单库的压力分摊到了多库上从而避免因为数据库硬件资源有限导致的数据库性能瓶颈。 每个库的结构都一样; 每个库的数据都不一样没有交集; 所有库的并集是全量数据; 4.DBLink
随着业务复杂程度的提高、数据规模的增长越来越多的公司选择对其在线业务数据库进行垂直或水平拆分甚至选择不同的数据库类型以满足其业务需求。与此同时业务的数据被“散落”在各个数据库实例中。如何方便地对这些数据进行汇总查询已经成为困扰用户的一大问题。
例如一家电商创业公司最初的会员、商品、订单数据全部都存放在一个SQLServer实例中。但随着会员数量和交易规模的不断增长单个SQLServer实例已经支撑不了巨大的业务压力同时基于成本考虑将商品和订单表从原来的SQLServer中拆分出来分别存放到两个不同的MySQL实例中。原先用户连接到一个实例上即可执行一条SQL来关联汇总查询这三张表的数据但现在由于数据库拆分无法简易实现这一操作。针对这类问题提供了一套基于DBLink的解决方案用户通过一条SQL就能实现跨越多个数据库实例的查询。 可以在当前登录的Oracle上建立一个DBLink指向另一个远程的Oracle数据库表。 跨数据库查询中的DBLink是一个指向用户的任意数据库实例的虚拟连接是数据库实例的别名 DBLink和数据库实例一一对应对于MySQL来说对应的就是MySQL数据库所在的ip:port DBLink可以指向MySQL、SQLServer、PostgreSQL、Oracle、Redis等
用户需在SQL语句的库表名前加上DBLink前缀DBLink.库.表即可实现跨数据库查询。DBLink的名字由英文字母、数字和下划线组成
DBLink/database/table对应关系数据库系统通常把数据组织成层次结构如database、schema、table等以方便命名空间隔离和权限管理。跨库查询也是类似它以dblink、database和table这三层结构来组织。
在跨库查询中用户访问一个表需要指定全名称即dblink.database.table。然而不同数据库类型具有不同的层次组织为了实现统一查询需要将这些不同层次结构统一起来形成DBLink、database和table三层结构。 四、分库分表案例演示
1.分库维度怎么定
这个字段选择的标准是尽量避免应用代码和 SQL 性能受到影响。具体地说就是现有的 SQL 在分库后它的访问尽量落在单个数据库里否则原来的单库访问就变成了多库扫 描不但 SQL 的性能会受到影响而且相应的代码也需要进行改造。
具体到订单数据库的拆分首先会想到按照用户 ID 来进行拆分。这个结论是没错 但最好还是要有量化的数据支持不能拍脑袋。这里最好的做法是先收集所有 SQL挑选出 WHERE 语句中最常出现的过滤字段比 如说这里有三个候选对象分别是用户 ID、订单 ID 和商家 ID每个字段在 SQL 中都会出 现三种情况 单 ID 过滤比如说“用户 ID” 多 ID 过滤比如“用户 ID IN(?,?,?)” 该 ID 不出现。
最后分别统计这三个字段的使用情况假设共有 500 个 SQL 访问订单库3 个候选 字 段出现的情况如下 从这张表来看结论非常明显应该选择用户 ID 来进行分库。不过这只是静态分析。每个 SQL 访问的频率是不一样的所以还要分析每个 SQL 的实际访问量。在项目中分析了 Top15 执行次数最多的 SQL 它们占总执行次数 85%具有足够 代表性按照执行的次数如果使用用户 ID 进行分库这些 SQL 85% 会落到单个数据 库13% 落到多个数据库只有 2% 需要遍历所有的数据库。所以说从 SQL 动态执行次 数的角度来看用户 ID 分库也明显优于使用其他两个 ID 进行分库。
2.数据怎么分 根据 ID 范围进行分库比如把用户 ID 为 1 ~ 999 的记录分到第一个库1000 ~ 1999 的分到第二个库以此类推。 根据 ID 取模进行分库比如把用户 ID mod 10余数为 0 的记录放到第一个库余数 为 1 的放到第二个库以此类推。 在实践中为了运维方便选择 ID 取模进行分库的做法比较多。同时为了数据迁移方便 一般分库的数量是按照倍数增加的比如说一开始是 4 个库二次分裂为 8 个再分成 16 个。这样对于某个库的数据在分裂的时候一半数据会移到新库剩余的可以不用 动。
与此相反如果每次只增加一个库所有记录都要按照新的模数做调整。在这个项目中结合订单数据的实际情况最后采用的是取模的方式来拆分记录。补充说明按照取模进行分库每个库记录数一般比较均匀但也有些数据库存在超 级 ID这些 ID 的记录远远超过其他 ID。比如在广告场景下某个大广告主的广告数可 能占很大比例。如果按照广告主 ID 取模进行分库某些库的记录数会特别多对于这些 超级 ID需要提供单独库来存储记录。
3.分几个库
分库数量首先和单库能处理的记录数有关。一般来说MySQL 单库超过了 5000 万条记录Oracle 单库超过了 1 亿条记录DB 的压力就很大当然这也和字段数量、字段长度 和查询模式有关系。
在满足前面记录数量限制的前提下如果分库的数量太少达不到分散存储和减轻 DB 性能压力的目的如果分库的数量太多好处是单库访问性能好但对于跨多个库的访问 应用程序需要同时访问多个库如果我们并发地访问所有数据库就意味着要消耗更多的线 程资源如果是串行的访问模式执行的时间会大大地增加。另外分库数量还直接影响了硬件的投入多一个库就意味着要多投入硬件设备。所以 具体分多少个库需要做一个综合评估一般初次分库建议你分成 4~8 个库。在项目中拆分为了 6 个数据库这样可以满足较长一段时间的订单业务需求。
4.分库路由
分库从某种意义上来说意味着 DB Schema 改变了必然会影响应用但这种改变和业 务无关所以我们要尽量保证分库相关的逻辑都在数据访问层进行处理对上层的订单服务 透明服务代码无需改造。当然要完全做到这一点会很困难。那么具体哪些改动应该由 DAL数据访问层负责 哪些由订单服务负责这里我给你一些可行的建议对于单库访问比如查询条件已经指定了用户 ID那么该 SQL 只需访问特定库即可。此时应该由 DAL 层自动路由到特定库当库二次分裂时我们也只需要修改取模因子就 可以了应用代码不会受到影响。对于简单的多库查询DAL 层负责汇总各个分库返回的记录此时它仍对上层应用透 明。对于带聚合运算的多库查询比如说带 groupby、orderby、min、max、avg 等关键 字建议可以让 DAL 层汇总单个库返回的结果然后由上层应用做进一步的处理。
5.最终架构 上层应用通过订单服务访问数据库 分库代理实现了分库相关的功能包括聚合运算、订单 ID 到用户 ID 的映射做到分库 逻辑对订单服务透明 Lookup 表用于订单 ID 和用户 ID 的映射保证订单服务按订单 ID 访问时可以直接 落到单个库Cache 是 Lookup 表数据的缓存 DDAL 提供库的路由可以根据用户 ID 定位到某个库对于多库访问DDAL 支持可 选的多线程并发访问模式并支持简单的记录汇总 Lookup 表初始化数据来自于现有的分库数据当新增订单记录时由分库代理异步写 入。 6.如何安全落地
订单表是系统的核心业务表它的水平拆分会影响到很多业务订单服务本身的代码改造也 很大很容易导致依赖订单服务的应用出现问题。在上线时必须谨慎考虑。所以为了保证订单水平分库的总体改造可以安全落地整个方案的实施过程如下 首先实现 Oracle 和 MySQL 两套库并行所有数据读写指向 Oracle 库通过数 据同步程序把数据从 Oracle 拆分到多个 MySQL 库比如说 3 分钟增量同步一次。 其次选择几个对数据实时性要求不高的访问场景比如访问历史订单把订单 服务转向访问 MySQL 数据库以检验整套方案的可行性。 最后经过大量测试如果性能和功能都没有问题我们再一次性把所有实时读写访问 转向 MySQL废弃 Oracle。
这里把上线分成了两个阶段第一阶段把部分非实时的功能切换到 MySQL这个 阶段主要是为了验证技术它包括了分库代理、DDAL、Lookup 表等基础设施的改造第 二阶段主要是验证业务功能我们把所有订单场景全面接入 MySQL。1 号店两个阶段的 上线都是一次性成功的特别是第二阶段的上线100 多个依赖订单服务的应用通过简 单的重启就完成了系统的升级中间没有出现一例较大的问题。