导读
让人高兴的是,我需要为公司业务量正在(或者面临)告诉增长做出方案,技术方面也会面临一些挑战。
让人担忧的是,我们系统真的就需要“分表分库”了吗?“分表分库”如何去实践?
经过对分库分表的熟悉,做个初版的方案总结,从以下几个方面说起:
-
实际将会面临的问题
-
有哪几种切入方式(垂直和水平的适用面)
-
针对开源技术产品,优缺点是什么
-
建议和采纳
目的
-
请求过大
- 因为单机TPS、Menmory、IO等都是有限的,所以将求取分散到多台服务器上。(用户的请求和执行一个sql查询本质是一样的,都是一个请求资源)
-
单库过大
- 单机数据库的处理力、磁盘空间、IO瓶颈都是有限的,所以将一个大库切分成更多更小的库。
-
单表过大
- CRUD遇到瓶颈、索引膨胀、查询超时,所以将大表切分成更多更小的表。
拆分问题
-
跨库关联问题
在拆分的时候,很多后端程序数据都是通过sql join等关联查询来完成,而拆分之后,数据库可能是粉丝不是在不同实例和不同的主机上,join将非常麻烦,基于架构规范、性能、安全等综合方面考虑,一般禁止join的,那怎么处理呢?首先考虑垂直分库的设计问题,如果可以调整,优先调整,如果无法调整的情况下,将可以根据实际场景来看下常用的解决思路,分析适合的思路并应用。
-
全局表
- 所谓全局表,就是有可能系统所有模块都可能会依赖一些表,比较类似我们理解的“数据词典”,为了避免垮库join查询,我们可以将这类表在其他每个数据库中保存一份,同时这类数据通常不会发生改变(甚至几乎不会),所以不用担心“一致性”的问题。
-
字段冗余
-
一种典型的反范式设计,比较常用,通常为了避免join查询。(比如:电商中有个这样的业务场景,“订单表”中保存“卖家ID”的同时,将卖家的“name”字段也冗余,这样查询订单详情的时候就不需要再去查询“卖家用户表”)
字段冗余能带来遍历,是一种“空间换时间”的体现,使用场景有限,比较适合依赖字段少的情况,最复杂的是数据一致性的问题,这点很难保证,可以借助数据库触发器或者在业务层去保证,当然,结合业务一致性的要求(如果卖家更新了Name,订单中是否要更新),根据不同业务要求去做。
-
-
-
数据同步问题
- A库中的t_user和B库中的t_device有关联的话,可以定时将指定的表做同步,当然,同步本身会对数据库带来一定的影响,需要性能和数据时效性中取一个平衡,这样避免复杂的垮库查询,项目中可以通过ETL(Camel、Scriptella、Apatar等)工具实施。
-
系统层组装
-
在系统层面,调用不通模块的组件或者服务,获取倒得数据并进行字段封装,看起来容易,但时间是飞航复杂,尤其数据库设计上存在问题但又无法轻易调整的时候。下面结合伪代码来描述:
-
简单列查询的情况,要求是(1.查询用户数据,返回结果为List,T中包含UserId, 2.通过UserId进行数据结果组装)
public List<T> allMessage(){ //获取所有用户信息 List<message> msg = messageMapper.getUserMessage(); //组装用户相关信息(名字、性别等) for (message iterm : msg) { Users user = userMapper.getUserByPrimary(iterm.getUserId); iterm.setUserName(user.getUserName); //设置姓名 iterm.setUserName(user.sex); //设置性别 } return msg; }
伪代码如上,先获取信息列表,再循环调用用户服务获取name,拼装结果。
上述代码一眼就可以看出有效率问题,循环调用服务,可能有循环RPC,循环数据库,不推荐使用,看下改进后的伪代码:
public List<T> allMessage(){ //获取所有用户信息 List<message> msg = messageMapper.getUserMessage(); List<Long> UserIdList = new List<Long> for (message iterm : msg) { UserIdList.add(iterm.getUserId); } //传入UserId 获取用户信息 List<Users> users = userMapper.getUserByUserIds(UserIdList); //组装用户相关信息(名字、性别等) for (message iterm : msg) { iterm.setUserName(users.where(x -> x.UserId == iterm.getUserId).FirstOrDefault).RealName(); } return msg; }
看起来优雅一点,其实就是把循环调用改成一次调用。当然,服务数据库查询中很可能是in查询,效率比上一中方式更高(in查询会全表扫描,存在性能问题,查询优化器基本成本估算的,经过测试,在In语句条件字段有索引的时候,条件较少的情况是会走索引的)
-
通过上述简单封装,而不存在条件过滤,当遇到比较复杂的关联查询(左表和右表带条件查询等),不能像之前简单的封装,那怎么去做呢,有几种思路如下:
-
查出所有消息数据,然后地用用用户服务端拼装数据,再根据字段过滤,最后进行排序和分页返回数据。这种方式能够保证数据的完整性和准确性,但是影响性能,不建议使用。
-
查询出过滤条件不符合,userId不符合,在查询的时候使用函数过滤(如Not in,in),得到有效的消息数据,再调用用户进行封装,这种方式更为优雅。推荐使用
-
-
-
-
跨库事务(分布式事务)问题
- 业务数据库拆分之后,肯定遇到“分布式事务”问题,以往通过spring注解等配置方式进行实现事务,现在需要花费一定的成本保证一致性。(针对此,进行另一篇方案介绍)
-
垮库分页
-
全局视野法
- 将
order by create_time offset x limit y
,改成order by create_time where create_time > ${time} limit y
,保证每次只返回一页数据
- 将
-
业务折衷法-允许模糊数据
- 将
order by create_time offset X limit Y
,改写成order by create_time offset X/N limit Y/N
- 将
-
业务折衷法-禁止跳页查询
- 每次翻页,将
order by create_time offset X limit Y
,改写成order by create_time where create_time >$time_max limit Y
以保证每次只返回一页数据,性能为常量
- 每次翻页,将
-
二次查询法
-
将
order by create_time offset X limit Y
,改写成order by create_time offset X/N limit Y
-
找到最小值
time_min
-
between二次查询,
order by create_time between $$time_min and $time_i_max
-
设置虚拟
time_min
,找到time_min
在各个分库的offset,从而得到time_min
在全局的offset -
得到了
time_min
在全局的offset,自然得到了全局的offset X limit Y
-
-
拆分方式和方法
描述一种数据集的切分方式,是从物理上的切分,分库分表的顺序原则上是先垂直分,再水平分。因为这样更符合我们显示处理问题的方式。
-
垂直分库
- 垂直分库在“微服务中”已经非常普及,按照不同的业务模块划分出不同的数据库,而不像早期一样所有的数据表都放在一个数据库中。很多人没有从根本上搞清楚为什么要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的做法,导致拆分后遇到很多问题(如:跨库join,分布式事务等)
-
垂直分表
- 垂直分表日常开发比较常见,就是通过大表拆成小表,拆分的基于关系数据库中的字段(列)进行,同行建立一个扩展表,将不经常用的字段或者长度较大的字段拆分出去放到"扩展表"中,拆分开确实便于开发和维护,同时某种意义上避免了“跨页”的问题,(mysql、mssql底层都是通过"数据页"来存储的,会造成额外的性能开销)
-
水平分库
- 水平分库分表更有效的缓解单机和单库的性能瓶颈压力、连接数、硬件资源等
-
水平分表
-
水平分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。针对数据量巨大的单表,按照某种规则(RANGE,HASH取模等)切分到不同的表中,但本质上这些表还保存在同一个库中,所以库级别还是会有 IO 瓶颈。所以,一般不建议采用这种做法。
-
水平分库分表的规则常用如下:
-
RANGE
-
HASH取模
-
地理区域:比如按照华东,华南,华北等区分业务,参考七牛云
-
时间:按照时间切分,将一个时间段的数据放到另一张表,达到“热冷数据分离”。
-
-
开源产品
-
分表分库技术选型
-
Hibernate Shards:Hibernate提供,技术有限,不适用
-
Sharding-jdbc (ShardingSphere):当当开源,ShardingSphere在Sharding-jdbc已进军apache孵化产品,支持读写分离,支持部分sql函数,不提供监控(需要自身通过一些APM系统进行监控比如 SkyWalking,Zipkin 和 Jaeger)
-
TSharding:蘑菇街开源
-
Cobar Client:阿里产品,可代理,不支持读写分离,事务不够友好
-
Vitess:youtube产品,可代理、架构复杂、自身支持监控有简易GUI
-
Atlas:360产品,代理、不支持分库分表,算法不完善
-
建议和采纳
-
我们目前的数据库是否需要进行垂直分库?
TODO:根据实际业务商议
-
垂直拆分有没有原则或技巧?
没有标准或者黄金答案,一般参考系统业务模块进行数据库拆分,比如“用户中心”,对应的可能是“用户数据库”,但是也不一定严格一一对应,有些情况下,数据库拆分粒度可能比系统的粒度更粗,具体设计权衡,根据业务模块进行探讨,实际就是看开发者和架构师的水平了。
-
上述举例简单,我们后天报表系统中Join过多,分库后怎么关联查询?
针对复杂关联查询,其实互联网的业务系统中,本应该尽量避免join的,如果有多个join的,要么设计不合理,要么技术选型错误,参考OLAP和OLTP,报表类的系统在传统系统时代都是采用OLAP数据仓库实现,现在更多的借助于离线分析,流式计算等手段,而不该向上描述的那样在业务中直接在业务库中执行大量的Join和统计。
博主博客:https://blog.ituac.com/