主从数据库复制+Springboot项目中配置主从数据库读写分离

一、主从复制:

允许将一个数据库服务器(主数据库)的数据复制到一个或多个数据库服务器中(从数据库)。
默认情况下为异步复制,不需要保持长连接;
可以复制所有的库或者指定的库,或者指定的表

1.1 主从复制的好处:

(1)作为备份数据库,当主数据库出现问题时,可以切换到从数据库继续工作;
(2)读写分离,主库写,从库读,降低主数据库的压力;
(3)可以在从数据库上备份,不影响主数据库的性能。

1.2 主从复制的原理:

master将数据库的改变记录在二进制日志中,salve同步这些二进制日志,并根据日志执行操作。
(1)主数据库中的所有操作,都被记录在数据库二进制日志文件中;
(2) 从数据库服务器会启动I/O线程,读取主数据库服务器的二进制日志文件,并写入Relay log(中继日志)中;
(3)从数据库服务器开启一个SQL线程,检查Relay log中是否有更新,如果有,则把更新的内容解析成具体的操作,在从数据库中执行一遍。
综上,数据库服务器必须开启二进制日志。

主从数据库复制+Springboot项目中配置主从数据库读写分离_第1张图片

1.3 主从复制的方案

1.3.1 一主多备

这种方式一般用来做读写分离,master写,其他slave读,这种架构最大问题:I/O压力集中在Master上,每台从服务器都需要在master上读取文件。
主从数据库复制+Springboot项目中配置主从数据库读写分离_第2张图片

1.3.2 M-S-S

使用一台slave中继分担master的压力,只有中继服务器负责读取master中的二进制日志,其他从服务器在中继服务器中读取二进制日志。
中继服务器可以只负责读取,也可以读取二进制日志之后和主服务器数据同步,如果中继服务器只记录二进制日志而不执行,需要修改存储引擎为Black-hole。
主从数据库复制+Springboot项目中配置主从数据库读写分离_第3张图片

1.4 MS模式主从数据库的配置:

开始复制之前,如果主数据库已有内容,应先将主数据库当前的内容同步到从数据库中。
可以新建一个用户专门用于复制,将复制权限Replication Slave赋给该用户。

1.4.1 主数据库:

(1)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf,添加以下内容:

log-bin=/home/data/mysql/mysql-bin 
server-id=1
# 指定要同步的数据库,多个数据库用逗号隔开
binlog-do-db=test,test123
# 指定不需要同步的数据库,多个数据库用逗号隔开
binlog-ignore-db=mysql


innodb_flush_log_at_trx_commit = 1
sync_binlog = 1
  • log-bin=/home/data/mysql/mysql-bin:定义二进制日志文件的位置和前缀,如果目录/home/data/mysql不存在可以新建,mysql-bin为二进制日志文件的名字;
  • server-id:定义数据库的Id,如果不配置或server-id=0,则表示主服务器拒绝来自从服务器的任何连接;
  • binlog-do-db:指定需要同步的数据库;
  • binlog-ignore-db:指定不需要同步的数据库;
  • innodb_flush_log_at_trx_commit、sync_binlog:这两个属性的配置是为了在复制时尽量提高持久性和一致性。

(2)确认主服务器的skip_networking属性的值:

show variables like '%skip_networking%';

当skip_networking=ON时,从数据库服务器无法和主数据库服务器建立通信,复制失败。
(3)配置完成后,重启数据库。
(4)创建用于同步的数据库用户,并赋Replication Slave权限。

1.4.2 从数据库:

(1)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf,添加以下内容:

server-id=2
# 需要复制的数据库,多数据库使用逗号隔开
replicate-do-db=test123
# 设定需要忽略的复制数据库
replicate-ignore-db=test

# 设定需要复制的表
replicate-do-table=test123.table1
replicate-do-table=test123.table2
# 和上面的设置区别:可以使用通配符,replicate-wild-ignore-table和replicate-ignore-table的区别同样
replicate-wild-do-table=test123.table%

  • 每个server-id必须是唯一的。

(2)重启从数据库
(3)进入数据库中,执行和主数据库的同步sql语句:

change master to master_host='10.2.29.146',master_user='repl',master_password='password',
master_log_file='mysql-bin.000001', master_log_pos=2256183;
  • master_log_file和master_log_pos的值的确定:
    这两个值决定从数据库从主数据库的哪个日志文件的哪个位置开始复制。
    主数据库执行show master status;返回当前二进制日志文件的名字和记录日志的位置。

(4)启动从数据库的复制线程:

start slave;

(5)检查复制是否成功:如果返回的内容中Slave_IO_Running和Slave_SQL_Running都是Yes,则表示成功。
Slave_IO_Running:标记与主服务器的IO通信是否正常, 如果为NO,检查是否能ping通主服务器,检查主服务器的防火墙、检查数据库是否启动等;
Slave_SQL_Running:从服务器负责执行SQL的,如果为NO,表示执行sql语句时出错。

show slave status\G

返回

*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 10.2.29.146
                  Master_User: repl
                  Master_Port: 3306
                Connect_Retry: 60
              Master_Log_File: mysql-bin.000001
          Read_Master_Log_Pos: 2256183
               Relay_Log_File: qkxsb-virtual-machine-relay-bin.000002
                Relay_Log_Pos: 196432
        Relay_Master_Log_File: mysql-bin.000001
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB: 
          Replicate_Ignore_DB: 
           Replicate_Do_Table: 
       Replicate_Ignore_Table: 
      Replicate_Wild_Do_Table: 
  Replicate_Wild_Ignore_Table: 
                   Last_Errno: 0
                   Last_Error: 
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 2256183
              Relay_Log_Space: 196656
              Until_Condition: None
               Until_Log_File: 
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File: 
           Master_SSL_CA_Path: 
              Master_SSL_Cert: 
            Master_SSL_Cipher: 
               Master_SSL_Key: 
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error: 
               Last_SQL_Errno: 0
               Last_SQL_Error: 
  Replicate_Ignore_Server_Ids: 
             Master_Server_Id: 1
                  Master_UUID: 7cc751e9-f2aa-11e8-b106-0050568ddbff
             Master_Info_File: mysql.slave_master_info
                    SQL_Delay: 0
          SQL_Remaining_Delay: NULL
      Slave_SQL_Running_State: Slave has read all relay log; waiting for more updates
           Master_Retry_Count: 86400
                  Master_Bind: 
      Last_IO_Error_Timestamp: 
     Last_SQL_Error_Timestamp: 
               Master_SSL_Crl: 
           Master_SSL_Crlpath: 
           Retrieved_Gtid_Set: 
            Executed_Gtid_Set: 
                Auto_Position: 0
         Replicate_Rewrite_DB: 
                 Channel_Name: 
           Master_TLS_Version: 
       Master_public_key_path: 
        Get_master_public_key: 0
1 row in set (0.00 sec)

以上,主从数据库复制的配置完成。

1.5 MSS模式主从复制

1.5.1 主数据库:

(1)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf,添加以下内容:

log-bin=/home/data/mysql/mysql-bin 
server-id=1

binlog-do-db=test
binlog-ignore-db=mysql

sync-binlog = 1
binlog-format=row
  • log-bin=/home/data/mysql/mysql-bin:启用二进制日志,定义二进制日志文件的位置和前缀,如果目录/home/data/mysql不存在可以新建,mysql-bin为二进制日志文件的名字;
  • server-id:定义数据库的Id,如果不配置或server-id=0,则表示主服务器拒绝来自从服务器的任何连接;
  • binlog-do-db:指定需要同步的数据库;
  • binlog-ignore-db:指定不需要同步的数据库;
  • sync-binlog:启用二进制的同步功能;
  • binlog-format=row:以行的方式格式化二进制日志。

(2)确认主服务器的skip_networking属性的值:

show variables like '%skip_networking%';

当skip_networking=ON时,从数据库服务器无法和主数据库服务器建立通信,复制失败。
(3)配置完成后,重启数据库。

1.5.2 中继数据库:

(1)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf,添加以下内容:

log-bin=/home/data/mysql/mysql-bin-slave
server-id=2

log-slave-updates=1
binlog-format=row
  • 每个server-id必须是唯一的;
  • log-slave-updates:在中继日志relay-log中读取的日志在本机上执行,并将其写入自己的二进制日志中

(2)重启从数据库
(3)进入从数据库中,执行和主数据库的同步sql语句:

change master to master_host='10.2.29.146',master_user='repl',master_password='password',
master_log_file='mysql-bin.000001', master_log_pos=2256183;

(4)启动从数据库的复制线程:

start slave;

(5)检查复制是否成功:如果返回的内容中Slave_IO_Running和Slave_SQL_Running都是Yes,则表示成功。
Slave_IO_Running:标记与主服务器的IO通信是否正常, 如果为NO,检查是否能ping通主服务器,检查主服务器的防火墙、检查数据库是否启动等;
Slave_SQL_Running:从服务器负责执行SQL的,如果为NO,表示执行sql语句时出错。

show slave status\G

1.5.3 从数据库:

(1)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf,添加以下内容:

log-bin=/home/data/mysql/mysql-bin-slave1
server-id=3

binlog-format=row
  • 每个server-id必须是唯一的;

(2)重启从数据库
(3)进入数据库中,给Slave数据库设置复制的中继Slave数据库信息:

change master to master_host='10.2.29.147',master_user='repl',master_password='password',
master_log_file='mysql-bin-slave.000001', master_log_pos=50;

(4)启动从数据库的复制线程:

start slave;

(5)检查复制是否成功。
以上,MSS模式主从数据库复制的配置完成。

二、Springboot项目中配置主从数据库读写分离

2.1 配置文件:

datasource下为主数据库,db为从数据库。

spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://10.2.29.146:3306/test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: bmis-service
    password: Qkxsbbmis
  db:
    driver-class-name: com.mysql.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:mysql://10.2.29.165:3306/test?useUnicode=true&characterEncoding=utf8&characterSetResults=utf8
    username: qkxsb
    password: qkxsb

2.2 配置数据源

@Configuration
public class DruidConfig {
    /**
     * 主据源
     *
     * @return 返回数据源对象
     */
    @Primary
    @Bean(name = "writeDataSource")
    @ConfigurationProperties(prefix = "spring.datasource")
    public DataSource dataSource() {
        return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
    }

    /**
     * 从数据源
     *
     * @return 返回数据源对象
     */
    @Bean(name = "readDataSource")
    @ConfigurationProperties(prefix = "spring.db")
    public DataSource readDataSource0() {
        return DataSourceBuilder.create().type(com.alibaba.druid.pool.DruidDataSource.class).build();
    }
}

2.3 配置数据源路由和事务等

@Configuration
@EnableTransactionManagement(order = 2)
@MapperScan(basePackages = {"com.qk.*.dao"})
public class MybatisConfig
        implements TransactionManagementConfigurer, ApplicationContextAware {
    private static ApplicationContext context;

    /**
     * 数据源路由代理
     * 将数据源以map的形式放入数据源路由中,key分别为write和read,
     * 切面拦截指定方法的调用,判断是读还是写,将read或write放入ThreadLocale变量中
     * RoutingDataSource重写的determineCurrentLookupKey决定要使用哪个数据源,
     * 然后AbstractRoutingDataSource中的determineTargetDataSource的方法在map变量中将数据源取出,
     * 
     * @return
     */
    @Bean
    public AbstractRoutingDataSource routingDataSourceProxy() {
        RoutingDataSource proxy = new RoutingDataSource();
        Map<Object, Object> targetDataSources = Maps.newHashMap();
        targetDataSources.put("write", context.getBean("writeDataSource", DataSource.class));
        targetDataSources.put("read", context.getBean("readDataSource", DataSource.class));
        proxy.setDefaultTargetDataSource(context.getBean("writeDataSource", DataSource.class));
        proxy.setTargetDataSources(targetDataSources);
        return proxy;
    }

    @Bean
    @ConditionalOnMissingBean
    public SqlSessionFactoryBean sqlSessionFactory() throws IOException {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        bean.setDataSource(routingDataSourceProxy());
        bean.setVfs(SpringBootVFS.class);
        bean.setTypeAliasesPackage("com.qk");
        Resource configResource = new ClassPathResource("mybatis/mybatis.cfg.xml");
        bean.setConfigLocation(configResource);
        ResourcePatternResolver mapperResource = new PathMatchingResourcePatternResolver();
        Resource[] resources = mapperResource.getResources("classpath*:mybatis/mapper/**/*.xml");
        bean.setMapperLocations(resources);
        return bean;
    }

    @Override
    public PlatformTransactionManager annotationDrivenTransactionManager() {
        return new DataSourceTransactionManager(routingDataSourceProxy());
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        if (context == null) {
            context = applicationContext;
        }
    }
}

2.4 数据源路由类

用于数据源切换,在获取Connection连接时调用。
根据ThreadLocal中存的数据源类型,决定选择哪个数据源

public class RoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        String typeKey = DataSourceContextHolder.getJdbcType();
        if (typeKey == null) {
            return "write";
        }
        return "write";
    }
}

2.5 定义切换数据源的注解

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {
    /**
     * 数据源key值
     *
     * @return
     */
    String value() default "write";
}

2.6 定义切面,拦截定义的注解,决定ThreadLocal中的值

@Aspect
@Component
@Order(1)
public class DynamicDataSourceAspect {

    @Pointcut("@annotation(DynamicDataSource)")
    public void annotationPointcut() {
    }
    /**
     * 切换数据源
     *
     * @param point 节点
     */
    @Before("annotationPointcut()")
    public void setDataSourceType(JoinPoint point) {
        MethodSignature methodSignature =  (MethodSignature) point.getSignature();
        Method method = methodSignature.getMethod();
        DynamicDataSource annotation = method.getAnnotation(DynamicDataSource.class);
        String value = annotation.value();
        if ("write".equals(value)){
            DataSourceContextHolder.write();
        }else {
            DataSourceContextHolder.read();
        }
    }

    @After("annotationPointcut()")
    public void clear() {
        DataSourceContextHolder.clearDbType();
    }
}

2.7 DataSourceContextHolder类

public class DataSourceContextHolder {
    private static Logger logger = LoggerFactory.getLogger(DataSourceContextHolder.class);

    private final static ThreadLocal<String> local = new ThreadLocal<>();

    public static ThreadLocal<String> getLocal() {
        return local;
    }

    public static void read() {
        logger.debug("切换至[读]数据源");
        local.set("read");
    }

    public static void write() {
        logger.debug("切换至[写]数据源");
        local.set("write");
    }

    public static String getJdbcType() {
        return local.get();
    }

    /**
     * 清理链接类型
     */
    public static void clearDbType() {
        local.remove();
    }
}

2.8 使用注解

在service层的方法上加入@DynamicDataSource("read"),根据RoutingDataSource中的定义,如果不加注解,使用的是主数据源。

2.9 问题

以上方式在加入@Transactional注解或@LcnTransaction注解后并不能实现切换数据源,原因如下:

2.9.1 加@Transactional注解的方法:

在执行方法之前,会先进入事务的拦截器,去获取Connection,此时AbstractRoutingDataSource中determineTargetDataSource()方法会决定要使用哪个数据源,因为在determineCurrentLookupKey()方法中定义默认使用key为write的数据源,所以当前获取的Connection中的数据源为主数据源。
Conncetion获取后,将其放入在全局变量resources中,resources的类型为ThreadLocal>,存入的数据格式为,ConnectionHolder中有Connection对象。
等到调用执行SQL语句的方法时,再去获取Connection,会发现resources中已经存在相同的key,直接将其对应的ConnectionHolder对象取出来,而不在重新获取新的连接对象。
AbstractRoutingDataSource中determineTargetDataSource()方法在第一次获取Connection时才会执行,所以,在同一个事务中使用的Connection始终是第一次得到的对象,实现不了数据源的切换。

2.9.2 加@LcnTransaction注解的方法:

在service中的方法上加@LcnTransaction注解,该方法被调用时,在执行到第一个操作sql语句的方法时获取Connection对象,在此之前缓存中并没有当前groupId对应的对象,所以通过AbstractRoutingDataSource中determineTargetDataSource()方法获取数据源,从而得到Connection,并将其放入缓存中,数据格式为<分布式事务的groupId,>,LcnConnectionProxy对象中有Connection对象。
当前方法中之后再取得的连接对象,都是在缓存中取到的。

因为在一个方法中使用的连接对象是同一个,实现不了数据源的切换,所以在既有读又有写的方法中使用主数据源,在只有查询的方法中加@DynamicDataSource("read")注解。

你可能感兴趣的:(java)