文章目录
前言
- 理解分库分表的意义
- 理解数据切分的不同方式,以及带来的问题与解决方案
一、为什么要分库分表
- 随着业务变得越来越复杂,用户越来越多,集中式的架构性能回出现巨大问题,比如系统会越来越慢,而且时不时会宕机,所以必须要解决高性能和可用性的问题。
1.数数据性能瓶颈的出现
- 高并发下,链接数不够
- 数据量太大,查询效率下降
- 存储问题,数据库所在的机器性能下降
2.数据库优化方案对比
2.1 重启
重启:是释放资源的最好方法
2.2 SQL与索引
慢SQL:SQL语句写的非常复杂,比如关联的表特别多,条件也特别多,导致查询所消耗的时间越来越长
方案:SQL优化,尽可能用到索引
2.3 表与存储引擎
如果SQL本身没有什么大问题,接着检查表结构的设计。比如字段类型和长度的选择、表结构是不是需要拆分或合并,不同表应该选择什么存储引擎,是不是要分区等等。
2.4 架构
表结构没有问题的话,就要从数据库服务、结构层面进行优化
- 添加缓存:部署缓存服务器,把数据在内存里面缓存起来。先查缓存,没有再查数据库,提升查询效率,减少数据库压力。
- 集群部署:一台数据库承受不了访问压力,可以部署集群做负载均衡。实现读写分离,写服务访问主节点,读服务访问从节点。
- 分库分表:如果读写分离没有解决问题,因为只有一个主节点,写的压力并没有得到分摊,可以采用分库分表方案。
2.5 数据库配置
如果通过结构层面没有解决问题,活着机器随让配置的蘅皋但是性能没有发挥到极致,还可以优化数据库的配置,比如连接数、缓冲区大小等等。
2.6 操作系统
数据库是安装在操作系统上的,所以操作系统的配置也有优化的空间。
3. 什么时候分表
推荐: 单表行数超过500万行活着单表容量超过2GB,才推荐进行分库分表。(如果预计三年后数据量根本达不到这个级别,请不要在创建表时就分库分表)
- 可以不用分库分表,尽量不要分库分表
- 分库分表是一个长期的规划,要解决的不只是现在的问题。
二、分库分表的类型和特点
- 垂直切分:基于表或字段划分,表结构不同。有单库的分表,也有多库的分表。
- 水平切分:基于数据划分,表结构相同,数据不同,也有同库的水平切分和多库的切分。
1. 垂直切分
垂直切分有两种,一种是单库的,一种是多库的。字段太多了,就要拆表,表太多了,就要拆库。
2. 水平切分
水平切分就是按照数据的维度分布不同的表中,可以是单库的,也可以是多库。
3. 分库分表带来的问题
3.1 跨库关联查询
因为在不同的(指在不同服务器上的)数据库,不能直接使用join做关联查询
3.1.1 解决方案
1) 字段冗余
可以将需要查询的字段,做成冗余字段,避免跨库关联查询的问题(反范式的设计)
第一范式:属性不可以再拆分。
第二范式:行必须有一个唯一标识。
第三范式:非主属性之间不能存在依赖关系(比如在表中关联另一张表的ID,就不能存在另一张表中的其他属性,实际项目中,为了提高查询效率都会进行反范式设计)
2) 数据同步
比如商户系统要查询产品系统的产品表,可以直接在商户系统中创建一张产品表,通过ETL、MQ或者Canal定时同步产品数据。
3) 全局表(广播表)
有一些基础信息表,比如行名行号表、行政区划表,被很多业务系统用到,如果放在核心系统,每个系统都要调用接口去查询,这个时候可以在所有的数据库都存储相同的基础数据,各个系统自己维护,保持同步。
4) 系统组装层
在不同的数据库节点,各自利用查询条件,把符合条件数据的数据查询出来,然后再内存中重新组装,返回给客户端。
3.2 分布式事务
如果是在一个数据库里面,我们可以用本地事务来控制,但是在不同的数据库里面就不行了。这里必须要出现一个协调者的角色,让大家统一行动,而且要分成多个阶段,一般是先确定都能成功才成功,只要有一个人不能成功,就要全部失败。
核心思想其实就是在预先提交能够成功的情况下,尽量缩短同时提交的时间差,来提升成功的概率。
3.3 排序、翻页、函数计算问题
跨节点多库进行查询时,会出现limit分页,order by排序的问题。
3.4 分布式全局ID
MySQL的数据库里面字段有一个自增的属性,Oracle也有Sequence序列。如果是一个数据库,那么可以保证ID是不重复的,但是水平分表以后,每个表都按照自己的规律自增,不同的表之间肯定会出现ID重复的问题。
3.4.1 UUID
UUID标准形式包含32个16进制数字,分为5段,形式为8-4-4-4-12的36个字符
UUID是主键最简单的方案,本地生成,性能高,没有网络耗时。缺点是UUID非常长,会占用大量的存储空间;另外,作为主键建立索引和基于索引进行查询时都会存在性能问题,在InnoDB中,UUID的无序性会引起数据位置频繁变动,导致分页。
3.4.2 数据库
把序号维护在数据库的一张表中。这张表记录了全局主键的类型、位数、起始值、当前值。当其他应用需要获取全局ID时,先for update锁行,取到值+1后并且更新后返回。并发性比较差。
3.4.3 Redis
基于Redis的INT自增的特性,使用批量的方式降低数据的写压力,每次获取一段区间的ID号段,用完之后再去数据库获取,可以大大减轻数据库的压力。
3.4.4 雪花算法
- 使用41bit作为毫秒数,可以使用69年
- 10bit作为机器的ID(5bit是数据中心,5bit的机器ID),支持1024个节点
- 12bit作为毫秒内的流水号(每个节点在每毫秒可以产生4096个ID)
- 最后还有一个符号位,永远是0
3.5 多数据源(动态数据源)
在SSM的项目里面,查询一般要经过这些流程:在任何阶段都可以实现动态切换数据源
DAO——Mapper(ORM)——JDBC——代理——数据库服务
3.5.1 编码层(DAO)
在我们连接到某一个数据源之前,先根据配置的分片规则,判断需要连接到哪些节点,再建立连接。
Spring中提供了一个抽象类AbstractRoutingDataSource,可以实现数据源的动态切换。
步骤:
1)application.yml定义多个数据源
2)创建@TargetDataSource注解和DataSourceNames
3)创建DynamicDataSource继承AbstractRoutingDataSource
4)多数据源配置类DynamicDataSourceConfig
5)创建切面类DataSourceAspect,对添加了@TargetDataSource注解的类进行拦截设置数据源,使用DynamicDATa Source.setDataSource动态设置数据源
6)在启动类上自动装配数据源配置@Import({DynamicDataSourceConfig.class})
7)在实现类SysUserServiceImpl上加上注解,如@TargetDataSource(name=DataSourceNames.SECOND)
优点:不需要依赖ORM框架,即使替换了ORM框架也不受影响。实现简单(不需要解析SQL和路由规则),可以灵活的定制
缺点:不能复用,不能跨语言。
动态数据源的实现方案
3.5.2 框架层
比如我们用的是MyBatis连接数据库,也可以指定数据源。可以基于MyBatis插件的拦截机制(拦截query和update方法),实现数据源的选择。
3.5.3 驱动层
不管是MyBatis还是Hibernate,还是JdbcTemplate,本质上都是对JDBC的封装,所以第三层就是驱动层。比如Sharding-JDBC,就是对JDBC的对象进行了封装。JDBC里面的两个核心对象,一个是Connection,是对连接的封装;一个是DataSource,是对一个数据库的封装。
实现思路:自己封装一个DataSource,在项目中配置多个数据源,这样就可以随心所欲的切换数据源了
3.5.4 代理层
前面都是在客户端实现的。也就是说,如果你有10个项目,那就要对10个项目进行改造,当然这种情况下我们会把逻辑抽取出来达成jar包,直接依赖使用。单如果不是Java项目,同样的逻辑还是要实现一遍。
所以可以直接做一个服务,不同语言的项目都可以直接连接,使用同一个逻辑。只需要提供跟数据库一样的协议,减少客户端的变动,比如原来用的Spring、MyBatis、Druid,都不用去修改,只要修改数据库Ip和连接规则就行了。
比如Mycat、Sharding-Proxy
3.5.5 服务层
在数据库服务前面加一个路由层,后面支持多个数据源。
腾讯云现在主推TDSQL,就是这样的一种实现。不过,这样的数据库在部署的时候,多了很多的节点需要部署。