java基础巩固-宇宙第一AiYWM:为了维持生计,MySQL基础Part5(主从复制【实现读写分离】、读写分离【提高数据库读并发,提高读写性能】、分库分表【解决数据库存储问题,避免表太大】)~整起

PART1-1:读写分离:我个人感觉,MySQL主从复制依赖于binlog,而读写分离一般是是通过主从复制来实现的【或者这样说,就是有了主从复制解决的数据不一致问题,咱们才敢读写分离从而提高读写性能呀,虽然读性能提高的多写性能提高的少但是至少是提高了呀】

  • 数据库或者缓存用的好好的,为啥要读写分离呢?
    • 读写分离主要是 为了将对数据库的读写操作分散到不同的数据库节点上,提升数据库的并发,从而提升小幅度写性能,大幅度提升读性能
      在这里插入图片描述
  • 一般情况下,我们都会选择 一主多从【也就是一台主数据库负责写,其他的从数据库负责读】
    • MySQL的一主一备和一主多从有什么区别?

      • 在一主一备的双 M 架构里,主备切换只需要把客户端流量切到备库;
      • 而在一主多从架构里,主备切换除了要把客户端流量切到备库外,还需要把从库接到新主库上。
    • 一主多从咋实现呀:

      • 设计的思路就是下面几种:【不论是使用哪一种读写分离具体的实现方案,想要实现读写分离一般包含如下几步:】
        • 部署多台数据库,选择其中的一台作为主数据库,其他的一台或者多台作为从数据库。
        • 保证主数据库和从数据库之间的数据是实时同步的,这个过程也就是我们常说的主从复制。见PART1-2
        • 系统将写请求交给主数据库处理,读请求交给从数据库处理
      • 具体读写分离的实现方式:
        • 代理方式:在应用和数据中间加了一个代理层【提供类似代理方式的中间件有MySQL Router(官方)、Atlas(基于 MySQL Proxy)、Maxscale、MyCat】。应用程序所有的数据请求都交给代理层处理,代理层负责分离读写请求,将它们路由到对应的数据库中
          在这里插入图片描述
          在这里插入图片描述
        • 组件方式:推荐使用,可以通过引入第三方组件来帮助我们读写请求。这种方式目前在各种互联网公司中用的最多的。
          • 推荐使用 shardingsphere 官方中的sharding-jdbc,直接引入 jar 包即可使用,Maven项目的话就引入依赖坐标即可
          • 现在的 ShardingSphere 不单单是指某个框架而是一个生态圈【ShardingSphere 的前身就是 Sharding-JDBC】,这个生态圈 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar 这三款开源的分布式数据库中间件解决方案所构成。
    • 这里肯定就要考虑问题了,读写分离对于提升DB的并发很有效,那么会带来什么问题?如何解决?你这样一份A读B写肯定存在数据不一致问题呀【主库和从库的数据存在延迟,比如你写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 主从同步延迟】,所以主库和从库之间会进行数据同步,以保证从库中数据的准确性。,那同步怎样同步数据呢?----->PART1-2主从复制。这一Part中有解决主从同步延迟的方案

    • SpringBoot中java代码实现 MySQL 读写分离技术:一般而言我们使用在 service 层或者 dao 层,在需要查询的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE),它表示该方法下所有的操作都走的是读库;在需要 update 或者 insert 的时候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来将会走写库芋道源码老师关于写一个读写分离中间件的文章
      在这里插入图片描述

      • 在高并发的场景中,关于数据库都有哪些优化的手段?常用的有以下的实现方法:
        • 读写分离
          在这里插入图片描述
        • 加缓存
        • 主从架构集群
        • 分库分表等,在互联网应用中,大部分都是读多写少 的场景,设置两个库,主库和读库
          • 主库的职能是负责写,从库主要是负责读,可以建立读库集群,通过读写职能在数据源上的隔离达到 减少读写冲突释压数据库负载 、保护数据库的目的 。在实际的使用中,凡是涉及到写的部分直接切换到主库,读的部分直接切换到读库,这就是典型的读写分离技术
          • 主从同步的局限性 :这里分为主数据库和从数据库,主数据库和从数据库保持数据库结构的一致,主库负责写,当写入数据的时候,会自动同步数据到从数据库;从数据库负责读,当读请求来的时候,直接从读库读取数据,主数据库会自动进行数据复制到从数据库中
            • 主从复制的延迟问题,当写入到主数据库的过程中,突然来了一个读请求,而此时数据还没有完全同步,就会出现读请求的数据读不到或者读出的数据比原始值少的情况。具体的解决方法最简单的就是 将读请求暂时指向主库,但是同时也失去了主从分离的部分意义。也就是说在严格意义上的数据一致性场景中,读写分离并非是完全适合的,注意更新的时效性是读写分离使用的缺点
      • 实现步骤:
        • 该项目需要引入如下依赖:springBoot、spring-aop、spring-jdbc、aspectjweaver 等
        • 主从数据源的配置:
          • 我们需要配置主从数据库,主从数据库的配置一般都是写在配置文件里面。通过 @ConfigurationProperties 注解,可以将配置文件(一般命名为:application.Properties)里的属性映射到具体的类属性上,从而读取到写入的值注入到具体的代码配置中,按照习惯大于约定的原则,主库我们都是注为 master,从库注为 slave
            /**
             * 主从配置
             *
             * @author wyq
             */
            @Configuration
            @MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")
            public class DataSourceConfig {
            
            	//采用了阿里的 druid 数据库连接池,使用 build 建造者模式创建 DataSource 对象,DataSource 就是代码层面抽象出来的数据源,接着需要配置 sessionFactory、sqlTemplate、事务管理器等
                /**
                 * 主库
                 */
                @Bean
                @ConfigurationProperties(prefix = "spring.datasource.master")
                public DataSource master() {
                    return DruidDataSourceBuilder.create().build();
                }
            
                /**
                 * 从库
                 */
                @Bean
                @ConfigurationProperties(prefix = "spring.datasource.slave")
                public DataSource slaver() {
                    return DruidDataSourceBuilder.create().build();
                }
            
            
                /**
                 * 实例化数据源路由
                 */
                @Bean
                public DataSourceRouter dynamicDB(@Qualifier("master") DataSource masterDataSource,
                                                  @Autowired(required = false) @Qualifier("slaver") DataSource slaveDataSource) {
                    DataSourceRouter dynamicDataSource = new DataSourceRouter();
                    Map<Object, Object> targetDataSources = new HashMap<>();
                    targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
                    if (slaveDataSource != null) {
                        targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
                    }
                    dynamicDataSource.setTargetDataSources(targetDataSources);
                    dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
                    return dynamicDataSource;
                }
            
            
                /**
                 * 配置sessionFactory
                 * @param dynamicDataSource
                 * @return
                 * @throws Exception
                 */
                @Bean
                public SqlSessionFactory sessionFactory(@Qualifier("dynamicDB") DataSource dynamicDataSource) throws Exception {
                    SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
                    bean.setMapperLocations(
                            new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
                    bean.setDataSource(dynamicDataSource);
                    return bean.getObject();
                }
            
            
                /**
                 * 创建sqlTemplate
                 * @param sqlSessionFactory
                 * @return
                 */
                @Bean
                public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
                    return new SqlSessionTemplate(sqlSessionFactory);
                }
            
            
                /**
                 * 事务配置
                 *
                 * @param dynamicDataSource
                 * @return
                 */
                @Bean(name = "dataSourceTx")
                public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) {
                    DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
                    dataSourceTransactionManager.setDataSource(dynamicDataSource);
                    return dataSourceTransactionManager;
                }
            }
            
        • 数据源路由的配置:
          • 路由在主从分离是非常重要的,基本是读写切换的核心。Spring 提供了 AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源,作用就是在执行查询之前,设置使用的数据源,实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源
            public class DataSourceRouter extends AbstractRoutingDataSource {
            
                /**
                 * 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解
                 * 应该给DataSourceContextHolder设值
                 *
                 * @return
                 */
                 //为了能有一个全局的数据源管理器,此时我们需要引入 DataSourceContextHolder 这个数据库上下文管理器,可以理解为全局的变量,随时可取,它的主要作用就是保存当前的数据源;
                @Override
                protected Object determineCurrentLookupKey() {
                    return DataSourceContextHolder.get();
            
                }
            }
            
        • 数据源上下文环境:
          在这里插入图片描述
        • 切换注解和 Aop 配置:
          在这里插入图片描述
          在这里插入图片描述
        • 用法以及测试:
          • 在配置好了读写分离之后,就可以在代码中使用了,一般而言我们使用在 service 层或者 dao 层,在需要查询的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE),它表示该方法下所有的操作都走的是读库;在需要 update 或者 insert 的时候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来将会走写库
            在这里插入图片描述

PART1-2:主从复制

  • 主从复制:主从同步使得数据可以从一个数据库服务器复制到其他服务器上,在复制数据时,一个服务器充当主服务器(master),其余的服务器充当从服务器(slave)。因为复制是 异步 进行的,所以从服务器不需要一直连接着主服务器,从服务器甚至可以通过拨号断断续续地连接主服务器。通过配置文件,可以指定复制所有的数据库,某个数据库,甚至是某个数据库上的某个表
    • 在实际应用中,建议使用可靠性优先策略,减少主备延迟【主从延迟】,提升系统可用性,尽量减少大事务操作,把大事务拆分小事务
      • 主从同步延迟:主库和从库的数据存在延迟,比如你 写完主库之后,主库的数据同步到从库是需要时间的,这个时间差就导致了主库和从库的数据不一致性问题。这也就是我们经常说的 主从同步延迟 。
        • Mysql 的读写分离的架构下 主从同步之间也会有时间差【主库和备库在执行同一个事务的时候出现时间差的问题】
          在这里插入图片描述
      • 出现主从同步延迟问题主要原因有:
        • 有些部署条件下,备库所在机器的性能要比主库性能差。菜,跟不上节奏
        • 备库的压力较大。忙,实在是跟不上节奏
        • 碰上了硬茬,大事务,一个主库上语句执行10分钟,那么这个事务可能会导致从库延迟10分钟。
      • 解决主从同步延迟问题的几种方案:
        • 解决主从延迟方案一:强制将读请求路由到主库处理【对于一些对延迟很敏感的业务直接使用主库读】:从库的数据过期了,从你从库读数据这事那事多麻烦,那我就直接从主库读取不美嘛。
          • Sharding-JDBC 就是采用的这种方案【通过使用 Sharding-JDBC 的 HintManager 分片键值管理器,我们可以强制使用主库。虽然这种方案会增加主库的处理压力但是咱们可以将那些必须获取最新数据的读请求都交给主库处理。其余的读请求依旧由从库处理】
            HintManager hintManager = HintManager.getInstance();
            hintManager.setMasterRouteOnly();
            // 继续JDBC操作
            ......
            
          • 分片:数据分片将原本一张数据量较大的表例如 t_order 拆分生成数个表结构完全一致的小数据量表 t_order_0、t_order_1、···、t_order_n,每张表只存储原大表中的一部分数据,当执行一条SQL时会通过 分库策略、分片策略 将数据分散到不同的数据库、表内
            • 分片算法:在实际开发中除了可以用分片健取模的规则分片,还可以用 >=、<=、>、<、BETWEEN 和 IN 等条件作为分片规则,自定义分片逻辑,这时就需要用到分片策略与分片算法【分片策略只是抽象出的概念,它是由分片算法和分片健组合而成,分片算法做具体的数据分片逻辑。每种策略中可以是多个分片算法的组合,每个分片算法可以对多个分片健做逻辑判断。。】【从执行 SQL 的角度来看,分库分表可以看作是一种路由机制,把 SQL 语句路由到期望的数据库或数据表中并获取数据,分片算法可以理解成一种路由规则。】。
              在这里插入图片描述
              • 分片键:用于分片的数据库字段。将 t_order 表分片以后,当执行一条SQL时,通过对作为分片健的字段 order_id 取模的方式来决定这条数据该在哪个数据库中的哪个表中执行,此时 order_id 字段就是 t_order 表的分片健。这样以来同一个订单的相关数据就会存在同一个数据库表中,大幅提升数据检索的性能,不仅如此 sharding-jdbc 还支持根据多个字段作为分片健进行分片
            • sharding-jdbc 并没有直接提供分片算法的实现,需要开发者根据业务自行实现。sharding-jdbc 提供了4种分片算法
              • 精确分片算法:精确分片算法(PreciseShardingAlgorithm)用于单个字段作为分片键,SQL中有 = 与 IN 等条件的分片,需要在标准分片策略(StandardShardingStrategy )下使用
              • 范围分片算法:范围分片算法(RangeShardingAlgorithm)用于单个字段作为分片键,SQL中有 BETWEEN AND、>、<、>=、<= 等条件的分片,需要在标准分片策略(StandardShardingStrategy )下使用
              • 复合分片算法:复合分片算法(ComplexKeysShardingAlgorithm)用于多个字段作为分片键的分片操作,同时获取到多个分片健的值,根据多个字段处理业务逻辑。需要在复合分片策略(ComplexShardingStrategy )下使用
              • Hint分片算法:Hint分片算法(HintShardingAlgorithm)稍有不同,上边的算法中都是解析SQL 语句提取分片键,并设置分片策略进行分片。但有些时候并没有使用任何的分片键和分片策略,可还想将 SQL 路由到目标数据库和表,就需要通过手动干预指定SQL的目标数据库和表信息,这也叫强制路由
            • 分片策略:分片策略是一种抽象的概念,实际分片操作的是由分片算法和分片健来完成的
              在这里插入图片描述
        • 解决主从延迟方案二:延迟读取:主从同步延迟 0.5s,那我就让从机 1s 之后再读取数据。这样设计业务流程就会好很多:对于一些对数据比较敏感的场景,你可以在完成写请求之后,避免立即进行请求操作【因为有延迟呀,你立即请求的处理,出来的数据可能就是错的数据呀,所以不能立即处理】。比如你支付成功之后,跳转到一个支付成功的页面或者其他指定的页面,当你点击返回之后才返回自己的账户。灵活一点嘛
        • 解决主从延迟方案三:并行复制:MySQL 5.6 版本以后,提供了一种并行复制的方式,通过将 SQL 线程转换为多个 work 线程来进行重放。
          • MySQL 的并行策略有哪些?
            • 按表分发策略:如果两个事务更新不同的表,它们就可以并行。因为数据是存储在表里的,所以按表分发,可以保证两个 worker 不会更新同一行。
              • 缺点:如果碰到热点表,比如所有的更新事务都会涉及到某一个表的时候,所有事务都会被分配到同一个 worker 中,就变成单线程复制了。
            • 按行分发策略:如果两个事务没有更新相同的行,它们在备库上可以并行。如果两个事务没有更新相同的行,它们在备库上可以并行执行。显然,这个模式要求 binlog 格式必须是 row
              • 缺点:相比于按表并行分发策略,按行并行策略在决定线程分发的时候,需要消耗更多的计算资源
        • 解决主从延迟方案四:提高机器配置(王道)
        • 解决主从延迟方案五:在业务初期就选择合适的分库、分表策略,避免单表单库过大带来额外的复制压力
        • 避免长事务
        • 避免让数据库进行各种大量运算
    • MySQL主从同步的目的?为什么要做主从同步?【做主从同步或者说主从复制不就是为了实现读写分离进而小幅度提高写性能,大幅度提高读性能嘛,或者一句话提高并发能力】
      • 通过增加从服务器来提高数据库的性能,在主服务器上执行写入和更新【在主服务器上生成实时数据,而在从服务器上分析这些数据,从而提高主服务器的性能】,在从服务器上向外提供读功能,可以动态地调整从服务器的数量,从而调整整个数据库的性能。
      • 提高数据安全,:因为数据已复制到从服务器,从服务器可以终止复制进程,所以,可以在从服务器上备份而不破坏主服务器相应数据
      • 数据备份。一般我们都会做数据备份,可能是写定时任务, 一些特殊行业可能还需要手动备份,有些行业要求备份和原数据不能在同一个地方,所以主从就能很好的解决这个问题,不仅备份及时,而且还可以多地备份,保证数据的安全
    • Mysql 主从之间是怎么同步数据的:或者说主从复制的原理【MySQL主从复制流程和原理?】
      在这里插入图片描述
      • 基本原理流程【MySQL 的主从复制依赖于 binlog ,也就是记录 MySQL 上的所有变化并以二进制形式保存在磁盘上。复制的过程就是将 binlog 中的数据从主库传输到从库上。】,是3个线程以及之间的关联
        在这里插入图片描述
        • MySQL中一个事务完整的同步过程:对上面图的补充
          • 备库B和主库A建立来了 **长连接,**主库A内部专门线程用于维护了这个长连接。
          • 在备库B上通过changemaster命令设置主库A的IP端口用户名密码以及从哪个位置开始请求binlog包括文件名和日志偏移量
          • 在备库B上执行start-slave命令备库会启动两个线程:io_thread和sql_thread分别负责建立连接和读取中转日志进行解析执行
          • 备库读取主库传过来的binlog文件备库收到文件写到本地成为中转日志
          • 后来由于 多线程复制方案 的引入,sql_thread演化成了多个线程。
            • 为什么要有多线程复制策略?因为单线程复制的能力全面低于多线程复制,对于更新压力较大的主库,备库可能是一直追不上主库的,带来的现象就是备库上seconds_behind_master值越来越大
              在这里插入图片描述
        • MySQL 是如何保证主备同步?
          • 肯定得先建立主从关系呀,一开始创建主备关系的时候,是由备库指定的,比如基于位点的主备关系,备库说“我要从binlog文件A的位置P”开始同步,主库就从这个指定的位置开始往后发。
          • 而主备关系搭建之后,是主库决定要发给数据给备库的,所以主库有新的日志也会发给备库。
        • 完成主从复制之后,你就可以在写数据时只写主库,在读数据时只读从库,这样即使写请求会锁表或者锁记录,也不会影响读请求的执行。
          在这里插入图片描述
    • 如何最快的复制一张表?
      • 为了避免对源表加读锁,更稳妥的方案是先将数据写到外部文本文件,然后再写回目标表
        • 一种方法是,使用 mysqldump 命令将数据导出成一组 INSERT 语句
        • 另一种方法是**直接将结果导出成.csv 文件**。MySQL 提供语法,用来将查询结果导出到服务端本地目录:select * from db1.t where a>900 into outfile ‘/server_tmp/t.csv’;得到.csv 导出文件后,你就可以用下面的 load data 命令将数据导入到目标表 db2.t 中:load data infile ‘/server_tmp/t.csv’ into table db2.t;
        • 物理拷贝:在 MySQL 5.6 版本引入了可传输表空间(transportable tablespace) 的方法,可以通过导出 + 导入表空间的方式,实现物理拷贝表的功能。
    • MySQL 主从复制还有哪些模型【同步策略】?主要有三种:
      • 同步复制:MySQL 主库提交事务的线程要等待所有从库的复制成功响应,才返回客户端结果。这种方式在实际项目中基本上没法用,原因有两个:一是性能很差,因为要复制到所有节点才返回响应;二是可用性也很差,主库和所有从库任何一个数据库出问题,都会影响业务。
      • 异步复制(默认模型):MySQL 主库提交事务的线程并不会等待 binlog 同步到各从库,就返回客户端结果。这种模式一旦主库宕机,数据就会发生丢失
        • MySQL binlog(binary log 即二进制日志文件) 主要记录了 MySQL 数据库中数据的所有变化(数据库执行的所有 DDL 和 DML 语句)。因此,我们根据主库的 MySQL binlog 日志就能够将主库的数据同步到从库中。
          在这里插入图片描述
      • 半同步复制:MySQL 5.7 版本之后增加的一种复制方式,半同步复制用来解决主库数据丢失问题,介于两者之间,事务线程不用等待所有的从库复制成功响应,只要一部分复制成功响应回来就行,比如一主二从的集群,只要数据成功复制到任意一个从库上,主库的事务线程就可以返回给客户端。这种半同步复制的方式,兼顾了异步复制和同步复制的优点,即使出现主库宕机,至少还有一个从库有最新的数据,不存在数据丢失的风险
        • 半同步复制,也叫 semi-sync 复制,指的就是主库写入 binlog 日志之后,就会将强制此时立即将数据同步到从库从库将日志写入自己本地的 relay log 之后,接着会返回一个 ack 给主库,主库接收到至少一个从库的 ack 之后才会认为写操作完成了
        • MySQL 实际上在有两个同步机制,一个是半同步复制,用来 解决主库数据丢失问题;一个是并行复制,用来 解决主从同步延时问题。】并行复制,指的是从库开启多个线程,并行读取 relay log 中不同库的日志,然后并行重放不同库的日志,这是库级别的并行。
    • 主库出问题如何解决?
      • 基于位点的主备切换:存在找同步位点这个问题
      • MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。那么,GTID 到底是什么意思,又是如何解决找同步位点这个问题呢?
        • GTID:全局事务 ID,是一个事务在提交的时候生成的,是这个事务的唯一标识
          • 它由两部分组成,格式是:GTID=server_uuid:gno
          • 每个 MySQL 实例都维护了一个 GTID 集合,用来对应“这个实例执行过的所有事务”。
          • 在基于 GTID 的主备关系里,系统认为只要建立主备关系,就必须保证主库发给备库的日志是完整的。因此,如果实例 B 需要的日志已经不存在,A’就拒绝把日志发给 B。
    • MySQL 读写分离涉及到过期读问题的几种解决方案?
      • 强制走主库方案
      • sleep 方案
      • 判断主备无延迟方案
      • 配合 semi-sync 方案
      • 等主库位点方案
      • GTID 方案。
      • 实际生产中,先客户端对请求做分类,区分哪些请求可以接受过期读,而哪些请求完全不能接受过期读;然后,对于不能接受过期读的语句,再使用等 GTID 或等位点的方案。
    • MySQL的并发链接和并发查询有什么区别?
      • 在执行show processlist的结果里,看到了几千个连接,指的是并发连接。而"当前正在执行"的语句,才是并发查询。
      • 并发连接数多影响的是内存,并发查询太高对CPU不利。一个机器的CPU核数有限,线程全冲进来,上下文切换的成本就会太高。
        • 所以需要设置参数:innodb_thread_concurrency 用来限制线程数,当线程数达到该参数,InnoDB就会认为线程数用完了,会阻止其他语句进入引擎执行。

PART2:分库分表

  • 分库分表
    • 分库分表怎么搞呢,因为他毕竟是一种优化手段,所以就不可能那么死板【一般来说,单表行数超过 500 万行或者单表容量超过 2GB之后,才需要考虑做分库分表了,小于这个数据量,遇到性能问题先考虑通过其他优化来解决。】,有可能我:
      • 只分库不分表
        • 分库主要解决的是并发量大的问题因为并发量一旦上来了,那么数据库就可能会成为瓶颈,因为数据库的连接数是有限的。当咱们的数据库的读或者写的QPS【这篇文章中ctrl+F搜索QPS,就有QPS的简介】过高,导致你的数据库连接数不足了的时候,就需要考虑分库了,通过增加数据库实例的方式来提供更多的可用数据库链接,从而提升系统的并发度。比较典型的分库的场景有如下几个:
          • 我们在做微服务拆分的时候,就会 按照业务边界,把各个业务的数据从一个单一的数据库中拆分开,分表把订单、物流、商品、会员等单独放到单独的数据库中
          • 把历史订单挪到历史库里面去
      • 只分表不分库:
        • 分表其实主要解决的是数据量大的问题。假如你的单表数据量非常大,因为并发不高,数据量连接可能还够,但是 存储和查询的性能遇到了瓶颈了,你做了很多优化之后还是无法提升效率的时候,就需要考虑做分表了。通过将数据拆分到多张表中,来减少单表的数据量,从而提升查询速度。
      • 分库又分表
        • 为什么要分库分表或者说 什么情况下需要分库分表【那就是既需要解决并发量大的问题,又需要解决数据量大的问题时候。通常情况下,高并发和数据量大的问题都是同时发生的,所以,我们会经常遇到分库分表需要同时进行的情况。】?:如果 MySQL 一张表的数据量过大怎么办?不就是上分库分表嘛
          在这里插入图片描述
          • 单表的数据达到千万级别以上,数据库读写速度比较缓慢(分表)。或者数据库中的数据占用的空间越来越大,备份时间越来越长(分库)
          • 应用的并发量太大(分库),数据库链接或者说数据库实例也不够了
    • 分库与分表:
      • 分库:就是 将数据库中的数据分散到不同的数据库上,或者说一个数据库分成多个数据库,部署到不同机器
        • 比如:将数据库中的用户表和用户订单表分别放在两个不同的数据库。由于用户表数据量太大,你 对用户表进行了水平切分,然后将切分后的 2 张用户表分别放在两个不同的数据库。【这个还有点像集群与分布式的关系,或者说可以这样说集群与分布式一般并存,而分库与分表大概率并存】
        • 从库是不是越多越好?
          在这里插入图片描述
      • 分表:就是 对单表的数据进行拆分,可以是垂直拆分,也可以是水平拆分
        在这里插入图片描述
        在这里插入图片描述
        • 垂直拆分:垂直拆分是 对数据表列的拆分,把一张列比较多的表拆分为多张表【表中的一些列单独抽出来作为一个表。】
        • 水平拆分:水平拆分是对数据表行的拆分,把一张行比较多的表拆分为多张表。比如将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
      • 两种分库分表的方式:
        在这里插入图片描述
        • 一种是按照 range 来分,就是每个库一段连续的数据,这个一般是按比如时间范围来的,但是这种一般较少用,因为很容易产生热点问题,大量的流量都打在最新的数据上了
          • range 来分,好处在于说,扩容的时候很简单,因为你只要预备好,给每个月都准备一个库就可以了,到了一个新的月份的时候,自然而然,就会写新的库了;缺点,但是大部分的请求,都是访问最新的数据。实际生产用 range,要看场景。
          • 通常,如果有特殊的诉求,比如按照月度汇总、地区汇总等以外,通常 按照买家Id进行分表【按照买家Id做分表,保证的是同一个买家的所有订单都在同一张表 ,并不是要给每个买家都单独分配一张表】。因为这样可以避免一个关键的问题那就是——数据倾斜(热点数据)。【比如,电商网站上面是有很多买家和卖家的,但是,一个大的卖家可能会产生很多订单,比如像苏宁易购、当当等这种店铺,他每天在天猫产生的订单量就非常的大。如果按照卖家Id分表的话,那同一个卖家的很多订单都会分到同一张表那就会使得有一些表的数据量非常的大,但是有些表的数据量又很小,这就是发生了数据倾斜。这个卖家的数据就变成了热点数据,随着时间的增长,就会使得这个卖家的所有操作都变得异常缓慢。】
        • 按照某个字段hash一下均匀分散,这个较为常用
          • hash 分发,好处在于说,可以平均分配每个库的数据量和请求压力;坏处在于说扩容起来比较麻烦,会有一个数据迁移的过程,之前的数据需要重新计算 hash 值重新分配到不同的库或表
            • 在做分表路由的时候,是可以设定一定的规则的,比如我们想要分1024张表,那么我们可以用买家ID或者买家ID的hashcode对1024取模,结果是0000-1023,那么就存储到对应的编号的分表中就行了然后为了解决跨表查询的问题
              • 我们用买家ID做了分表,那么买家来查询的时候,是一定可以把买家ID带过来的,我们直接去对应的表里面查询就行了。
              • 卖家查询的话,同样可以带卖家id过来,那么,我们可以有一个基于binlog、flink等准实时的同步一张卖家维度的分表,这张表只用来查询,来解决卖家查询的问题。本质上就是用空间换时间的做法。【同步一张卖家维度的表来,但是其实所有的写操作还是要写到买家表的,只不过需要准实时同步的方案同步到卖家表中。也就是说,我们的这个卖家表理论上是没有业务的写操作,只有读操作的。这就避免了大卖家的热点问题】。所以,这个卖家库只需要有高性能的读就行了,那这样的话就可以有很多选择了,比如可以部署到一些配置不用那么高的机器、或者其实可以干脆就不用MYSQL,而是采用HBASE、PolarDB、Lindorm等数据库就可以了。这些数据库都是可以海量数据,并提供高性能查询的。大卖家一般都是可以识别的,提前针对大卖家,把他的订单,再按照一定的规则拆分到多张表中。因为只有读,没有写操作,所以拆分多张表也不用考虑事务的问题
              • 用订单号直接查时,在生成订单号的时候,我们一般会把分表解决编码到订单号中去,因为订单生成的时候是一定可以知道买家ID的,那么我们就把买家ID的路由结果比如1023,作为一段固定的值放到订单号中就行了。这就是所谓的"基因法"。这样按照订单号查询的时候,解析出这段数字,直接去对应分表查询就好了。
              • 其他的查询,没有买卖家ID,也没订单号的,那其实就属于是低频查询或者非核心功能查询了,那就可以用ES等搜索引擎的方案来解决了
      • 分表算法:选定了分表字段之后(避免数据倾斜【一个卖家对应过一个订单,但是一个买家就不会出现数据倾斜,所以选买家】以及事务【保持分出来的表只有读操作,实时同步来保证数据一致性】),基于这个分表字段来准确的把数据分表到某一张表中,确保一个前提就是同一个分表字段,经过这个算法处理后,得到的结果一定是一致的,不可变的。比如当我们对order表进行分表的时候,比如我们要分成128张表的话,那么得到的128表应该是:order_0000、order_0001、order_0002…order_0126、order_0127。常用的分表算法如下:
        • 直接取模:比如我们要分成128张表的话,就 用一个整数来对128取模就行了,得到的结果如果是0002,那么就把数据放到order_0002这张表中
        • Hash取模:常针对分表字段不是数字类型,而是字符串类型,先对这个分表字段取Hash,然后在再取模,Java中的hash方法得到的结果有可能是负数,需要考虑这种负数的情况。
        • 一致性Hash:对于前面两种方法,如果需要扩容二次分表,表的总数量发生变化时,就需要重新计算hash值,就需要涉及到数据迁移了【下面有数据迁移相关知识】可以采用一致性哈希的方式来做分表,一致性哈希可以按照常用的hash算法来将对应的key哈希到一个具有2^32次方个节点的空间中,形成成一个顺时针首尾相接的闭合的环形。所以当添加一台新的数据库服务器时,只有增加服务器的位置和逆时针方向第一台服务器之间的键会受影响。
    • 常见的分库分表中间件,比较一下
      在这里插入图片描述
      • 推荐 Sharding-JDBC。ShardingSphere 项目(包括 Sharding-JDBC、Sharding-Proxy 和 Sharding-Sidecar) 是当当捐入 Apache 的,目前主要由京东数科的一些巨佬维护。
        在这里插入图片描述
        • 芋道源码老师关于SpringBoot分库分表入门,文章
        • ShardingSphere由(Sharding-JDBC、Sharding-Proxy和Sharding-Sidecar这3款相互独立的产品组成),在Java的JDBC层提供的额外服务。ShardingSphere使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。
        • TDDL 是淘宝开源的一个用于访问数据库的中间件, 它集成了分库分表, 读写分离,权重调配,动态数据源配置等功能。封装 jdbc 的 DataSource给用户提供统一的基于客户端的使用。
    • 主从复制实现的读写分离中,存在过数据不一致以及出现主从延迟等问题,新技术肯定会伴随很多的问题,这个分库分表肯定也不例外。分库分表后,数据怎么迁移呢【我们如何将老库(单库单表)的数据迁移到新库(分库分表后的数据库系统)呢?】
      • 停机迁移:写个脚本老库的数据写到新库中。比如你在凌晨 2 点,系统使用的人数非常少的时候,挂一个公告说系统要维护升级预计 1 小时。然后,你写一个脚本将老库的数据都同步到新库中。
      • 双写方案:双写方案是针对那种不能停机迁移的场景【想要在项目中实施双写还是比较麻烦的,很容易会出现问题。我们可以借助上面提到的数据库同步工具 Canal【阿里开源的一个叫做 canal 的工具。这个工具可以帮助我们实现 MySQL 和其他数据源比如 Elasticsearch 或者另外一台 MySQL 数据库之间的数据同步。很显然,这个工具的底层原理肯定也是依赖 binlog。canal 的原理就是模拟 MySQL 主从复制的过程,解析 binlog 将数据同步到其他的数据源。】 做增量数据迁移(还是依赖 binlog,开发和维护成本较低)。】
        • 双写方案具体原理:
          • 我们对老库的更新操作(增删改),同时也要写入新库(双写)。如果操作的数据不存在于新库的话,需要插入到新库中。 这样就能保证,咱们新库里的数据是最新的。
          • 在迁移过程,双写只会让被更新操作过的老库中的数据同步到新库,我们还需要自己写脚本将老库中的数据和新库的数据做比对。如果新库中没有,那咱们就把数据插入到新库。如果新库有,旧库没有,就把新库对应的数据删除(冗余数据清理)。
          • 重复上一步的操作,直到老库和新库的数据一致为止。
    • 分库分表会带来什么问题呢?数据库分库分表的缺点是啥
      • 在公司做的任何技术决策,不光是要考虑这个技术能不能满足我们的要求是否适合当前业务场景还要重点考虑其带来的成本
      • 做了分库分表之后,所有的读和写操作,都需要带着分表字段,这样才能知道具体去哪个库、哪张表中去查询数据。如果不带的话,就得支持全表扫描。但是,做了分库分表之后,就没办法做扫表的操作了,如果要扫表的话就要把所有的物理表都要扫一遍
      • 引入分库分表之后,会给系统带来什么挑战呢?
        • join 操作 : 同一个数据库中的表分布在了不同的数据库中,导致无法使用 join 操作。这样就导致我们需要手动进行数据的封装,比如你在一个数据库中查询到一个数据之后,再根据这个数据去另外一个数据库中找对应的数据。也就是说跨节点Join的问题:解决这一问题可以分两次查询实现
          • 跨节点的count,order by,group by以及聚合函数问题:分别在各个节点上得到结果后在应用程序端进行合并
          • 跨分片的排序分页问题(后台加大pagesize处理?)
        • 事务问题 :同一个数据库中的表分布在了不同的数据库中,如果单个操作涉及到多个数据库,那么数据库自带的事务就无法满足我们的要求了【分库分表之后就会带来因为不支持事务而导致的数据一致性的问题】。事务问题,已经不可以用本地事务了,需要用分布式事务。
        • 分布式 id :分库之后, 数据遍布在不同服务器上的数据库,数据库的自增主键已经没办法满足生成的主键唯一了。我们如何为不同的数据节点【数据节点是分库分表中一个不可再分的最小数据单元(表),它由数据源名称和数据表组成】生成全局唯一主键呢?这个时候,我们就需要为我们的系统引入分布式 id 了。
          • 分布式ID需要满足的要求或者说特点:【ID生成的三大核心需求:全局唯一(unique)、按照时间粗略有序(sortable by time)、尽可能短
            在这里插入图片描述
          • 分布式 ID 常见解决方案:
            • 数据库:数据库主键自增【通过关系型数据库的自增主键产生来唯一的 ID。】javaGuide老师关于数据库实现分布式ID的例子
              • 基于某个单表做自增主键:多张单表生成的自增主键会冲突,但是如果所有的表中的主键都从同一张表生成是不是就可以了。所有的表在需要主键的时候,都到这张表中获取一个自增的ID。但是问题是这个单表就变成整个系统的瓶颈,而且也存在单点问题,一旦他挂了,那整个数据库就都无法写入了。
              • 基于多个单表+步长做自增主键:引入多个表来一起生成自增主键,此时注意的问题就是保证id别重复就行
                在这里插入图片描述
            • 算法:ULID - 一种比UUID更好的方案
              • UUID 是 Universally Unique Identifier(通用唯一标识符) 的缩写,UUID是一类算法的统称,具体有不同的实现。UUID 包含 32 个 16 进制数字(8-4-4-4-12)。JDK 就提供了现成的生成 UUID 的方法【UUID.randomUUID()
                • UUID优缺点:
                  • 优点是每台机器可以独立产生ID,理论上保证不会重复,所以天然是分布式的
                  • 缺点是生成的ID太长,不仅占用内存,而且索引查询效率低
                • MongoDB会自动给每一条数据赋予一个唯一的ObjectId,它用的是一种UUID算法,生成的ObjectId占12个字节,由这几个部分组成【4个字节表示的Unix timestamp、3个字节表示的机器的ID、2个字节表示的进程ID、3个字节表示的计数器】
              • 用多台MySQL服务器也组成一个高性能的分布式发号器:
                • 假设用8台MySQL服务器协同工作,第一台MySQL初始值是1,每次自增8,第二台MySQL初始值是2,每次自增8,依次类推。前面用一个 round-robin load balancer 挡着,每来一个请求,由 round-robin balancer 随机地将请求发给8台MySQL中的任意一个,然后由接收到请求的MySQL返回一个ID
                • 这个方法虽然简单无脑,但是性能足够好。不过要注意,在MySQL中,不需要把所有ID都存下来,每台机器只需要存一个MAX_ID就可以了。这需要用到MySQL的一个REPLACE INTO特性。这个方法跟单台数据库比,缺点是ID是不是严格递增的,只是粗略递增的
                • Flickr就是这么做的,仅仅使用了两台MySQL服务器。
              • Twitter Snowflake(雪花算法):Snowflake 是 Twitter 开源的分布式 ID 生成算法,一个开源项目。有很多基于 Snowflake 算法的开源实现比如美团 的 Leaf、百度的 UidGenerator,并且这些开源实现对原有的 Snowflake 算法进行了优化在实际项目中,我们一般也会对 Snowflake 算法进行改造,最常见的就是在 Snowflake 算法生成的 ID 中加入业务类型信息
                在这里插入图片描述
                • Snowflake的核心算法:Snowflake 由 64 bit 的二进制数字组成,这 64bit 的二进制被分成了几部分,每一部分存储的数据都有特定的含义:
                  在这里插入图片描述
                  在这里插入图片描述
                  • 这种格式的ID,含有时间戳,且在高位,恰好满足要求。
                  • 在分布式这个场景下,是做不到的,要想高性能,只能做到粗略有序,无法保证ID严格有序。
                • Instagram用了类似的方案,41位表示时间戳,13位表示shard Id(一个shard Id对应一台PostgreSQL机器),最低10位表示自增ID,怎么样,跟Snowflake的设计非常类似吧。这个方案 用一个PostgreSQL集群代替了Twitter Snowflake 集群,优点是利用了现成的PostgreSQL,容易懂,维护方便
            • 开源框架:javaGuide老师关于分布式ID的框架解说,很赞
        • 引入分库分表之后,一般需要 DBA 的参与,同时还需要更多的数据库服务器,这些都属于成本
    • 如何理解 MySQL 的边读边发:
      • 如果客户端接受慢,会导致 MySQL 服务端由于结果发不出去,这个事务的执行时间会很长。
      • 服务端并不需要保存一个完整的结果集,取数据和发数据的流程都是通过一个 next_buffer 来操作的。
      • 内存的数据页都是在 Buffer_Pool中操作的。
        • InnoDB 管理 Buffer_Pool 使用的是改进的 LRU 算法,使用链表实现,实现上,按照 5:3 的比例把整个 LRU 链表分成了 young 区域和 old 区域。

巨人的肩膀:
高性能Mysql
Mysql技术内幕
https://shardingsphere.apache.org/document/legacy/3.x/document/cn/manual/sharding-jdbc/usage/read-write-splitting/
JavaGuide
Tom哥老师在MySQL+docker+SpringBoot中的读写分离、分库分表的讲述
芋道源码的选择路由key的文章:路由key应该在每个表中都存在而且唯一。路由策略应尽量保证数据能均匀进行分布
小姐姐养的狗老师关于设计糟糕的分库分表是如何把系统搞挂的?
Hollis老师
捡田螺的小男孩

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值