1.为什么要分库分表

跟着你的公司业务发展走的,你公司业务发展越好,用户就越多,数据量越大,请求量
越大,那你单个数据库一定扛不住。
比如你单表都几千万数据了,你确定你能抗住么?绝对不行,单表数据量太大,会极
大影响你的 sql 执行的性能,到了后面你的 sql 可能就跑的很慢了。一般来说,就以我的经
验来看,单表到几百万的时候,性能就会相对差一些了,你就得分表了。
分表是啥意思?就是把一个表的数据放到多个表中,然后查询的时候你就查一个表。
比如按照用户 id 来分表,将一个用户的数据就放在一个表中。然后操作的时候你对一个用
户就操作那个表就好了。这样可以控制每个表的数据量在可控的范围内,比如每个表就固定
在 200 万以内。
分库是啥意思?就是你一个库一般我们经验而言,最多支撑到并发 2000,一定要扩容
了,而且一个健康的单库并发值你最好保持在每秒 1000 左右,不要太大。那么你可以将一
个库的数据拆分到多个库中,访问的时候就访问一个库好了。

2.用过哪些分库分表中间件?

比较常见的包括:cobar、TDDL、atlas、sharding-jdbc、mycat(我没用过这些 我是根据这些业务实现的)

3.不同的分库分表中间件都有什么优点和缺点?

sharding-jdbc 这种 client 层方案的优点在于不用部署,运维成本低,不需要代理层的二
次转发请求,性能很高,但是如果遇到升级啥的需要各个系统都重新升级版本再发布,各个
系统都需要耦合 sharding-jdbc 的依赖;
mycat 这种 proxy 层方案的缺点在于需要部署,自己及运维一套中间件,运维成本高,
但是好处在于对于各个项目是透明的,如果遇到升级之类的都是自己中间件那里搞就行了。

垂直分库(表)水平分库(表)

image.png

垂直拆分

垂直拆分有两种,一种是单库的垂直拆分,另一种是多个数据库的垂直拆分。

单库垂直分表

单个表的字段数量建议控制在20~50个之间,之所以建议做这个限制,是因为如果字段加上数据累计的长度超过一个阈值后,数据就不是存储在一个页上,就会产生分页的问题,而这个问题会导致查询性能下降。
image.png

所以如果当某些业务表的字段过多时,我们一般会拆去垂直拆分的方式,把一个表的字段拆分成多个表,如图6-2所示,把一个订单表垂直拆分成一个订单主表和一个订单明细表。

在Innodb引擎中,单表字段最大限制为1017,参考mysql官网。

多库垂直分表

多库垂直拆分实际上就是把存在于一个库中的多个表,按照一定的纬度拆分到多个库中,如图6-3所示。这种拆分方式在微服务架构中也是很常见,基本上会按照业务纬度拆分数据库,同样该纬度也会影响到微服务的拆分,基本上服务和数据库是独立的。
image.png
多库垂直拆分最大的好处就是实现了业务数据的隔离。其次就是缓解了请求的压力,原本所有的表在一个库的时候,所有请求都会打到一个数据库服务器上,通过数据库的拆分,可以分摊掉请求,在这个层面上提升了数据库的吞吐能力。

水平拆分

垂直拆分的方式并没有解决单表数据量过大的问题,所以我们还需要通过水平拆分的方式把大表数据做数据分片。
水平切分也可以分成两种,一种是单库的,一种是多库的。

单库水平分表

image.png
两个案例:
银行的交易流水表,所有进出的交易都需要登记这张表,因为绝大部分时候客户都是查询当天的交易和一个月以内的交易数据,所以我们根据使用频率把这张表拆分成三张表:

当天表:只存储当天的数据。

当月表:我们在夜间运行一个定时任务,前一天的数据,全部迁移到当月表。用的是insert intoselect,然后delete。

历史表:同样是通过定时任务,把登记时间超过30天的数据,迁移到history历史表(历史表的数据非常大,我们按照月度,每个月建立分区)。

费用表: 消费金融公司跟线下商户合作,给客户办理了贷款以后,消费金融公司要给商户返费用,或者叫提成,每天都会产生很多的费用的数据。为了方便管理,我们每个月建立一张费用表,例如fee_detail_201901…fee_detail_201912。

但是注意,跟分区一样,这种方式虽然可以一定程度解决单表查询性能的问题,但是并不能解决单机存储瓶颈的问题

多库水平分表

多库水平分表,其实有点类似于分库分表的综合实现方案,从分表来说是减少了单表的数据量,从分库层面来说,降低了单个数据库访问的性能瓶颈,如图6-5所示。
image.png

雪花id

雪花算法

SnowFlake 算法,是 Twitter 开源的分布式 id 生成算法。其核心思想就是:使用一个 64 bit 的 long 型的数字作为全局唯一 id。雪花算法比较常见,在百度的UidGenerator、美团的Leaf中,都有用到雪花算法的实现。

如图6-11所示,表示雪花算法的组成,一共64bit,这64个bit位由四个部分组成。

第一部分, 1bit位,用来表示符号位,而ID一般是正数,所以这个符号位一般情况下是0。
第二部分, 占41 个 bit:表示的是时间戳,是系统时间的毫秒数,但是这个时间戳不是当前系统的时间,而是当前 系统时间-开始时间 ,更大的保证这个ID生成方案的使用的时间!
那么我们为什么需要这个时间戳,目的是为了保证有序性,可读性,我一看我就能猜到ID是什么时候生成的。
41位可以2 41 - 1表示个数字,
如果只用来表示正整数(计算机中正数包含0),可以表示的数值范围是:0 至 2 41 -1,减1
是因为可表示的数值范围是从0开始算的,而不是1。
也就是说41位可以表示2 41 -1个毫秒的值,转化成单位年则是(2 41 -1)/1000 60 60 24
365=69年,也就是能容纳69年的时间

第三部分, 用来记录工作机器id,id包含10bit,意味着这个服务最多可以部署在 2^10 台机器上,也就是 1024 台机器。
其中这10bit又可以分成2个5bit,前5bit表示机房id、5bit表示机器id,意味着最多支持2^5个机房(32),每个机房可以支持32台机器。
第四部分, 第四部分由12bit组成,它表示一个递增序列,用来记录同毫秒内产生的不同id。
image.png

ThreadLocal内存泄漏原因分析,以及如何避免

内存泄漏是程序在申请内存后,无法释放已申请的内存空间,多次内存泄漏就会导致内存耗光的严重问题。

强引用:

使用最普遍的引用(new),一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。

如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间回收该对象。

弱引用:

JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。

ThreadLocal的实现原理:每一个Thread维护一个ThreadLocalMap,key为使用使用弱引用的ThreadLocal实例,value为线程变量的副本。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部强引用时,key(ThreadLocal)就会被GC回收,这样就会导致ThreadLocalMap中key为null,而value还存在强引用,只用thread线程退出,value的强引用链条才会短掉,但如果当前线程未结束,这些key为null的Entry的value就会一直存在一条强引用链(红色链条)。

如果ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal就不会被回收,导致Entry内存泄漏。

ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()时会清除value的值。

因此,ThreadLocal内存泄漏的根源是由于ThreadLocalMap的生命周期和Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不因为弱引用。

ThreadLocal正确的使用方法:

  1. 每次使用完ThreadLocal都要调用它的remove()清除数据。<br /> ThreadLocal变量定义成private static,这样就一直存在ThreadLocal的强引用,也能保证任何时候都能通过ThreadLocal的弱引用访问到Entryvalue值,进而清除掉。

TransactionTemplate的编程式事务管理是使用模板方法设计模式对原始事务管理方式的封装。

  1. @Bean
  2. public TransactionTemplate transactionTemplate(DataSource dataSource) {
  3. //创建事务管理器
  4. DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
  5. //设置数据源
  6. dataSourceTransactionManager.setDataSource(dataSource);
  7. //编程式事务
  8. TransactionTemplate transactionTemplate = new TransactionTemplate();
  9. //设置事务管理器
  10. transactionTemplate.setTransactionManager(dataSourceTransactionManager);
  11. transactionTemplate.setPropagationBehaviorName("PROPAGATION_REQUIRED");
  12. return transactionTemplate;
  13. }

DynamicDataSource

Spring DriverManagerDataSource类:驱动管理器数据源

  1. <br />该类位于 [Spring](http://c.biancheng.net/spring/) 的 org.springframework.jdbc.datasource 类包。这是标准 JDBC 数据源的一个简单实现类,它用于开发简单的应用和程序测试,并且不支持连接池,每次连接数据库都是创建新的连接对象。 本示例使用默认的构造方法创建 DriverManagerDataSource 类的对象,然后设置连接属性,关键代码如下:

image.png

DriverManagerDataSource driverManagerDataSource = new DriverManagerDataSource();
driverManagerDataSource.setDriverClassName(“com.mysql.jdbc.Driver”); //加载驱动
String url = “jdbc:mysql://lzw:3306/testDatabase”;
//数据库路径driverManagerDataSource.setUrl(url);
driverManagerDataSource.setUsername(“root”);
driverManagerDataSource.setPassword(“111”);

继承 EnvironmentAware 接口 实现setEnvironment()方法

  1. @Override
  2. public void setEnvironment(Environment environment) {
  3. String prefix = "mini-db-router.jdbc.datasource.";
  4. dbCount = Integer.valueOf(environment.getProperty(prefix + "dbCount"));
  5. tbCount = Integer.valueOf(environment.getProperty(prefix + "tbCount"));
  6. routerKey = environment.getProperty(prefix + "routerKey");
  7. // 分库分表数据源
  8. String dataSources = environment.getProperty(prefix + "list");
  9. assert dataSources != null;
  10. for (String dbInfo : dataSources.split(",")) {
  11. Map<String, Object> dataSourceProps = PropertyUtil.handle(environment, prefix + dbInfo, Map.class);
  12. dataSourceMap.put(dbInfo, dataSourceProps);
  13. }
  14. // 默认数据源
  15. String defaultData = environment.getProperty(prefix + "default");
  16. defaultDataSourceConfig = PropertyUtil.handle(environment, prefix + defaultData, Map.class);
  17. }

分库分表问题

1、事务问题
方案一:使用分布式事务
优点:交由数据库管理,简单有效
缺点:性能代价高,特别是shard越来越多时
方案二:由应用程序和数据库共同控制
原理:将一个跨多个数据库的分布式事务分拆成多个仅处 于单个数据库上面的小事务,并通过应用程序来总控 各个小事务。
优点:性能上有优势
缺点:需要应用程序在事务控制上做灵活设计。如果使用 了spring的事务管理,改动起来会面临一定的困难。

2、跨节点的 Join 问题
只要是进行切分,跨节点 Join 的问题是不可避免的。但是良好的设计和切分却可以减少此类情况的发生。解决这一问题的普遍做法是分两次查询实现。
在第一次查询的结果集中找出关联数据的id,根据这些id发起第二次请求得到关联数据。

3、跨节点聚合问题
比如 count、order by、group by 等聚合函数问题,方案是各节点完成计算后,交由业务层进行合并
多节点的查询可以是并行的,因此大多数情况他比单一大表快很多,但是如果结果集很大,可能会导致内存消耗过高

4、数据迁移,容量规划,扩容等问题
这些问题目前都没有特别好的解决方案,每个方案都或多或少的有一些问题存在,因此这个问题的解决难度其实挺高的

5、ID 问题
数据被切分后,就不能依赖数据库的自增 ID 进行赋值,另外 ID 还需要承担携带路由信息的功能,以降低查询难度
一种方案是使用 UUID ,但是 UUID 比较长会占用较多的存储空间,另外一方面,UUID 对索引不友好
一种方案是通过维护一个 ID 签发表来对 ID 进行签发,但是这会导致插入需要增加一次查询,且该表容易成为性能瓶颈存在单点故障问题
一种方案是使用雪花算法进行 ID 的下发

6、跨分片的排序问题
如果排序字段是分片字段,则可以直接使用分片排序
如果排序字段不是分片字段,则需要先在分片上进行排序,然后到业务系统进行合并,然后再排序

7、分库策略、分库数量
这个需要根据实际的业务场景,进行合理的分配,否则容易给后期造成很大的问题