【进阶篇】MySQL的读写分离详解


在这里插入图片描述

0. 前言

例如,你的团队开发和维护着一个电商平台,随着业务的发展和网购习惯的不断成熟,目前平台每天有数以百万计的用户进行商品浏览和购买。但是经过你们团队的具体分析,发现1000商品的查询下单商品为1也就是大量请求都是在处理商品的查询,以及快递信息的查询。

尤其在高峰期,服务器可能需要处理大量的读和写请求。如果所有的读请求都由一个数据库服务器处理,那么可能会造成服务器响应缓慢,甚至服务器无法响应下单请求,甚至服务器直接崩溃。

这时,作为资深的你应该已经在考虑使用读写分离策略。具体来说,你肯定想,可以设置一个主库用于处理所有的写操作(如订单创建、用户注册),然后设置多个从库用于处理读操作(如商品浏览、订单查询)。这样,主库的压力就会大大减轻,同时,由于读请求可以分发到多个从库,用户查询的响应速度也会得到提升。没错,大多数都是这样想的,分库分表,读写分离其实就是解决数据量大的问题,但是等你完成读写分离的时候发现了一系列问题还需要进一步处理,本文我们就关于读写分离重点展开。


关于分库分表,可以查看我上一篇博客《MySQL分库分表详解》。这篇我们着重讲解MySQL的读写分离常见的实现方式。以及问题处理。

是否需要实施读写分离,需要根据具体的业务需求(如系统的并发量、用户体验要求等)以及系统状况(如硬件资源、维护成本等)来权衡。

关于MySQL 2000万条性能瓶颈的传言

作为工作3-5年以上的开发同学,应该都听说过前辈或者网上帖子说MySQL单表性能瓶颈的说法,即当单表数据量超过2000万行时,性能会明显下降。其实这个传言一直存在,并且一直传承者,我觉得也不是什么坏事,至少在性能优化方面有个底。经过我看到的野史是。关于MySQL单表性能瓶颈的说法,即当单表数据量超过2000万行时,性能会明显下降。野史说,这个说法最早据说起源于百度,后来被百度的工程师带到了其他公司,逐渐在业界流传开来。


我很早之前,也曾好奇做过一个测试验证。其实在8核、16G、 机械硬盘、单表32个字段 情况下。数据库表数据达到1000多万条,没有经过索引优化的情况下。时候性能大概在查询一次的时间5-8秒不等。但是经过索引优化后,可以降低到3秒内。其实这也算是有性能瓶颈,达不到2000万条。

阿里巴巴给出的最佳实践

阿里巴巴的《Java开发手册》提出了单表行数超过500万行或者单表容量超过2GB时才推荐进行分库分表。然而,这个数值并不是固定的,它与MySQL的配置和机器的硬件有关。当单表数据库达到某个上限时,内存无法存储其索引,导致之后的SQL查询产生磁盘IO,从而性能下降。增加硬件配置可能会提升性能。

阿里巴巴的《Java开发手册》补充到,如果预计三年后的数据量不会达到这个级别,就不要在创建表时就进行分库分表。根据自身机器情况综合评估,可以将500万行作为一个统一的标准。根据实际测试,在InnoDB引擎下,MySQL单表在800万数据量下的查询性能可能会很差。使用MyISAM引擎查询速度可能更快,但是对于数据完整性和事务支持不如InnoDB。因此,根据实际需求选择合适的优化方案。

1. MySQL读写分离

1.为什么需要读写分离

在业务系统中,当面临大量的读操作,且读操作对数据库的压力增大,造成写操作的效率降低时,就需要使用读写分离。读写分离可以有效地分担数据库的压力,优化系统性能,提高数据处理效率。

例如,一个电子商务网站,用户浏览商品、查看订单等操作都是读操作,而提交订单、修改订单等操作则是写操作。在大促销期间,用户访问量加大,读取商品信息的请求激增,如果所有的读写操作都在一个数据库上进行,会导致数据库压力增大,影响写操作的效率,可能会造成订单处理的延迟。

这时,就需要进行读写分离。配置一个主数据库进行写操作,配置多个从数据库进行读操作。这样,读操作就可以在多个从数据库上进行分担,不会影响到主数据库的写操作,从而提高了系统的处理效率。

同时,读写分离还能提高系统的可用性。当主数据库出现故障时,可以立即切换到从数据库进行读写操作,减少了系统的停机时间。

2. mysql的性能瓶颈,读写分离方案

关于MySQL的性能瓶颈,我们从前言里也能看出,所以主要来2方面:

  1. IO瓶颈由于磁盘读写速度远低于内存读写速度,导致大量磁盘IO操作会成为性能瓶颈。
  2. CPU瓶颈在大量复杂查询时,CPU资源会被大量占用,影响数据库性

针对这些瓶颈,可以采用读写分离的方案来提升MySQL的性能:

读写分离是将数据库的读和写操作分开,分别由不同的数据库服务器处理。一般情况下,我们会设置一个主数据库(Master)负责写操作,多个从数据库(Slave)负责读操作。当有新的数据写入时,主数据库会将数据复制到各个从数据库。

  1. 降低单个数据库服务器的压力:通过把读和写操作分散到不同的服务器,可以有效地降低单个数据库服务器的负载,提高响应速度。
  2. 提高数据库的并发处理能力:在读多写少的应用场景中,可以通过增加从数据库的数量,提高数据库的并发处理能力。
  3. 提高数据的可用性和安全性:在主数据库出现故障时,可以迅速切换到从数据库进行读写操作,保证数据的可用性。同时,由于数据分布在多个服务器,可以提高数据的安全性。

引发的常见问题

读写分离也会带来一些问题,比如数据同步延迟,可能导致读取到的数据不是最新的。所以在实施读写分离时,还需要考虑到这些因素。

其他常见读写分离场景

那么还有其他什么情况下我们才需要读写分离呢
读写分离是数据库架构中的一种常见策略,主要用于解决以下几种情况:

  1. 当数据库的并发读写请求非常高时,单一的数据库服务器可能无法承受这样的压力,这时就需要通过读写分离来分散压力。读请求可以分发到多个从库,写请求则由主库处理。

  2. 资源优化,读写分离可以使得主库专注于处理写操作和事务性的操作,从库则处理读操作。这样可以根据主从库的不同特性进行硬件和系统资源的优化配置。

  3. 数据备份和故障转移,这是一个间接作用。通过读写分离,可以实现数据的实时备份。当主库出现故障时,可以迅速切换到从库,保证服务的持续可用。

  4. 提高查询性能,读写分离可以将复杂的查询操作分散到多个从库上执行,从而提高查询性能。

2. 项目实践

在我们的项目中,我们使用Java和MySQL数据库进行读写分离。我们的目标是确保系统的稳定性和性能,同时也需要满足业务的快速发展和数据量的持续增长。以下是我们实现读写分离的过程:

1. 建立数据库副本

我们在MySQL中创建了一个主库(用于写操作)和多个从库(用于读操作)。主库和从库都是独立的数据库服务器,我们通过MySQL的复制功能,使从库能够实时同步主库的数据。

2. 配置数据源

在Java项目中,我们配置了两个数据源,一个连接到主库,一个连接到从库。我们使用了Spring Boot的数据源配置,可以方便地管理多个数据源。

3. 实现数据库路由

我们使用了MyBatis作为我们的ORM框架,通过实现RoutingDataSource接口,我们实现了一个自定义的数据源路由策略。在执行数据库操作时,根据操作类型(读或写),我们决定由哪个数据源去处理。

定义一个DbContextHolder类来保存当前线程的数据库类型 创建一个RoutingDataSource类,继承自AbstractRoutingDataSource

public class RoutingDataSource extends AbstractRoutingDataSource {
    
    

    @Override
    protected Object determineCurrentLookupKey() {
    
    
        return DbContextHolder.getDbType();
    }

}

接下来,我们需要在MyBatis的配置文件中配置数据源:

@Configuration
public class DataSourceConfig {
    
    

    @Bean
    public DataSource masterDataSource() {
    
    
        // 配置主数据源
    }

    @Bean
    public DataSource slaveDataSource() {
    
    
        // 配置从数据源
    }

    @Bean
    public DataSource routingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
                                        @Qualifier("slaveDataSource") DataSource slaveDataSource) {
    
    
        Map<Object, Object> targetDataSources = new HashMap<>();
        targetDataSources.put(DbContextHolder.DbType.MASTER, masterDataSource);
        targetDataSources.put(DbContextHolder.DbType.SLAVE, slaveDataSource);

        RoutingDataSource routingDataSource = new RoutingDataSource();
        routingDataSource.setDefaultTargetDataSource(masterDataSource);
        routingDataSource.setTargetDataSources(targetDataSources);
        return routingDataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("routingDataSource") DataSource routingDataSource) throws Exception {
    
    
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(routingDataSource);
        return sessionFactory.getObject();
    }

}

我们可以在需要切换数据源的地方使用DbContextHolder.setDbType(DbType)方法来切换数据源。
好了,基本逻辑梳理的差不多,我们来实现一个读写分离的注解,方便在代码中使用。
@ReadOnly 自定义注解,用于标记一个方法或者类只读取数据,不进行数据的修改。这个注解用于数据库读写分离的场景,当系统检测到这个注解时,会自动切换到从数据库进行操作。然后, 需要在 数据源切换逻辑中检测这个注解,如果检测到这个注解,就切换到从数据源。这部分的代码可能会有其他路基判断比较复杂,这是我的简化写法。其实代码比这个复杂,要容错处理。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({
    
    ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ReadOnly {
    
    
}

为了解析这个注解,我们创建一个切面(Aspect)来在处理读操作时切换到从数据源:

@Aspect
@Component
public class DataSourceAspect {
    
    

    @Around("@annotation(ReadOnly)")
    public Object setReadDataSourceType(ProceedingJoinPoint joinPoint) throws Throwable {
    
    
        try {
    
    
            DbContextHolder.setDbType(DbContextHolder.DbType.SLAVE);
            return joinPoint.proceed();
        } finally {
    
    
            DbContextHolder.clearDbType();
        }
    }

}

4. 切换数据源

我们创建了一个自定义注解ReadOnly,在Service层的方法上使用这个注解,可以指定该方法需要使用哪种数据库操作。然后,我们实现了一个AOP切面,切面代码示例如上,当调用被ReadOnly注解的方法时,会根据注解的参数,切换到相应的数据源。
在你需要用到读的操作的方法上使用@ReadOnly注解,这样在执行这个方法的时候,系统就会自动切换到从数据源。例如当调用 getUserById 方法的时候,使用的就是从数据源。

@Service
public class UserService {
    
    

    @Autowired
    private UserDao userDao;

    @ReadOnly
    public User getUserById(int id) {
    
    
        return userDao.getUserById(id);
    }

}

对于写的操作,不需要做任何特殊的标记,系统默认会使用主数据源。调用 addUser 方法时,使用的就是主数据源。 在处理读请求时,系统会自动切换到从数据源,提高了系统的读性能;在处理写请求时,系统会使用主数据源,保证了数据的一致性。

@Service
public class UserService {
    
    

    @Autowired
    private UserDao userDao;

    public void addUser(User user) {
    
    
        userDao.addUser(user);
    }

}

5. 负载均衡

由于我们有多个从库,所以我们实现了一个简单的负载均衡策略,以平均分配读请求到不同的从库。

2. 参考文档

建议大家看下这篇美团写的很是详细。
《SQL解析在美团的应用 作者: 广友》 https://tech.meituan.com/2018/05/20/sql-parser-used-in-mtdp.html

猜你喜欢

转载自blog.csdn.net/wangshuai6707/article/details/132657321