导读

让人高兴的是,我需要为公司业务量正在(或者面临)告诉增长做出方案,技术方面也会面临一些挑战。

让人担忧的是,我们系统真的就需要“分表分库”了吗?“分表分库”如何去实践?

经过对分库分表的熟悉,做个初版的方案总结,从以下几个方面说起:

  1. 实际将会面临的问题

  2. 有哪几种切入方式(垂直和水平的适用面)

  3. 针对开源技术产品,优缺点是什么

  4. 建议和采纳

目的

  • 请求过大

    • 因为单机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语句条件字段有索引的时候,条件较少的情况是会走索引的)

      • 通过上述简单封装,而不存在条件过滤,当遇到比较复杂的关联查询(左表和右表带条件查询等),不能像之前简单的封装,那怎么去做呢,有几种思路如下:

        1. 查出所有消息数据,然后地用用用户服务端拼装数据,再根据字段过滤,最后进行排序和分页返回数据。这种方式能够保证数据的完整性和准确性,但是影响性能,不建议使用。

        2. 查询出过滤条件不符合,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以保证每次只返回一页数据,性能为常量
    • 二次查询法

      1. order by create_time offset X limit Y,改写成order by create_time offset X/N limit Y

      2. 找到最小值time_min

      3. between二次查询,order by create_time between $$time_min and $time_i_max

      4. 设置虚拟time_min,找到time_min在各个分库的offset,从而得到time_min在全局的offset

      5. 得到了time_min在全局的offset,自然得到了全局的offset X limit Y

拆分方式和方法

描述一种数据集的切分方式,是从物理上的切分,分库分表的顺序原则上是先垂直分,再水平分。因为这样更符合我们显示处理问题的方式。

  • 垂直分库

    • 垂直分库在“微服务中”已经非常普及,按照不同的业务模块划分出不同的数据库,而不像早期一样所有的数据表都放在一个数据库中。很多人没有从根本上搞清楚为什么要拆分,也没有掌握拆分的原则和技巧,只是一味的模仿大厂的做法,导致拆分后遇到很多问题(如:跨库join,分布式事务等)
  • 垂直分表

    • 垂直分表日常开发比较常见,就是通过大表拆成小表,拆分的基于关系数据库中的字段(列)进行,同行建立一个扩展表,将不经常用的字段或者长度较大的字段拆分出去放到"扩展表"中,拆分开确实便于开发和维护,同时某种意义上避免了“跨页”的问题,(mysql、mssql底层都是通过"数据页"来存储的,会造成额外的性能开销)
  • 水平分库

    • 水平分库分表更有效的缓解单机和单库的性能瓶颈压力、连接数、硬件资源等
  • 水平分表

    • 水平分表,能够降低单表的数据量,一定程度上可以缓解查询性能瓶颈。针对数据量巨大的单表,按照某种规则(RANGE,HASH取模等)切分到不同的表中,但本质上这些表还保存在同一个库中,所以库级别还是会有 IO 瓶颈。所以,一般不建议采用这种做法。

    • 水平分库分表的规则常用如下:

      1. RANGE

      2. HASH取模

      3. 地理区域:比如按照华东,华南,华北等区分业务,参考七牛云

      4. 时间:按照时间切分,将一个时间段的数据放到另一张表,达到“热冷数据分离”。

开源产品

  • 分表分库技术选型
    1. Hibernate Shards:Hibernate提供,技术有限,不适用

    2. Sharding-jdbc (ShardingSphere):当当开源,ShardingSphere在Sharding-jdbc已进军apache孵化产品,支持读写分离,支持部分sql函数,不提供监控(需要自身通过一些APM系统进行监控比如 SkyWalking,Zipkin 和 Jaeger)

    3. TSharding:蘑菇街开源

    4. Cobar Client:阿里产品,可代理,不支持读写分离,事务不够友好

    5. Vitess:youtube产品,可代理、架构复杂、自身支持监控有简易GUI

    6. Atlas:360产品,代理、不支持分库分表,算法不完善

建议和采纳

  • 我们目前的数据库是否需要进行垂直分库?

    TODO:根据实际业务商议

  • 垂直拆分有没有原则或技巧?

    没有标准或者黄金答案,一般参考系统业务模块进行数据库拆分,比如“用户中心”,对应的可能是“用户数据库”,但是也不一定严格一一对应,有些情况下,数据库拆分粒度可能比系统的粒度更粗,具体设计权衡,根据业务模块进行探讨,实际就是看开发者和架构师的水平了。

  • 上述举例简单,我们后天报表系统中Join过多,分库后怎么关联查询?

    针对复杂关联查询,其实互联网的业务系统中,本应该尽量避免join的,如果有多个join的,要么设计不合理,要么技术选型错误,参考OLAP和OLTP,报表类的系统在传统系统时代都是采用OLAP数据仓库实现,现在更多的借助于离线分析,流式计算等手段,而不该向上描述的那样在业务中直接在业务库中执行大量的Join和统计。


标题:分表分库实战方案(突出重点)
作者:ituac
地址:http://blog.ituac.com/sharding

评论列表

    针对分库分表开源技术选型不给予任何参考建议,一点是因为开源基本是给予个人而言,稳定等兼容会有分歧,结合团队能力而定。

添加新评论