在实际生产中,数据的重要性不言而喻
如果我们的数据库只有一台服务器,那么很容易产生单点故障的问题,比如这台服务器访问压力过大而没有响应或者奔溃,那么服务就不可用了,再比如这台服务器的硬盘坏了,那么整个数据库的数据就全部丢失了,这是重大的安全事故.
为了避免服务的不可用以及保障数据的安全可靠性,我们至少需要部署两台或两台以上服务器来存储数据库数据,也就是我们需要将数据复制多份部署在多台不同的服务器上,即使有一台服务器出现故障了,其他服务器依然可以继续提供服务.
MySQL提供了主从复制功能以提高服务的可用性与数据的安全可靠性。主从复制是指服务器分为主服务器和从服务器,主服务器负责读和写,从服务器只负责读,主从复制也叫 master/slave
,master
是主,slave
是从,但是并没有强制,也就是说从也可以写,主也可以读,只不过一般我们不这么做。
主从复制可以实现对数据库备份和读写分离
注意:主从复制的过程会有很小的延迟,基本没有影响
MySQL多实例是指安装MySQL之后,在一台Linux服务器上同时启动多个MySQL数据库(实例),不需要安装多个MySQL(适合技术研究和学习的场景)
如果是有多台Linux服务器,那么我们需要每台服务器都分别安装MySQL(适用于实际线上生产环境)
我们此处计划在一台Linux上启动多个MySQL,这样适合我们的技术研究和学习,如果要在多台Linux分别启动MySQL,这个与我们在一台机器上的配置与操作都是完全一样的。
如何实现在一台Linux服务器上同时启动多个MySQL数据库(实例)?
通过为各个数据库实例配置独立的配置文件来实现,即每个数据库实例有自己单独的配置文件
下载地址:https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-5.7.18-linux-glibc2.5-x86_64.tar.gz
检查linux是否安装了mariadb数据库,mariadb数据库是mysql的分支。是免费开源的。mariadb和msyql会有冲突。首先要检查安装了mariadb, 卸载掉。
yum list installed | grep mariadb
若linux中安装了mariadb数据库,先卸载掉,mariadb数据库可能与安装mysql发生冲突,
yum -y remove mariadb-libs.x86_64
其中mariadb-libs.x86_64是第2步搜索出来的mariadb软件包,不同机器可能不一样, -y参数确认删除。
首先使用Xftp上传文件mysql-5.7.18-linux-glibc2.5-x86_64.tar
然后解压MySQL:
tar -zxvf mysql-5.7.18-linux-glibc2.5-x86_64.tar.gz -C /usr/local/
将解压后的mysql-5.7.18-linux-glibc2.5-x86_64改名为mysql-5.7.18 或者 mysql,为了明确mysql的版本,建议改名为mysql-5.7.18
mv mysql-5.7.18-linux-glibc2.5-x86_64 mysql-5.7.18
data文件夹是mysql用来存放数据库文件的,数据库的表数据都放在data目录。
默认没有data目录,可以手工创建data目录,在mysql-5.7.18文件夹目录下创建一个data文件夹,切换到mysql-5.7.18目录,执行创建文件夹命令
mkdir data
mkdir data/3307
mkdir data/3308
mkdir data/3309
mkdir data/3310
参数说明:
注意:目录要根据实际情况修改
./mysqld --initialize-insecure --basedir=/usr/local/mysql-5.7.18 --datadir=/usr/local/mysql-5.7.18/data/3307 --user=mysql
./mysqld --initialize-insecure --basedir=/usr/local/mysql-5.7.18 --datadir=/usr/local/mysql-5.7.18/data/3308 --user=mysql
./mysqld --initialize-insecure --basedir=/usr/local/mysql-5.7.18 --datadir=/usr/local/mysql-5.7.18/data/3309 --user=mysql
./mysqld --initialize-insecure --basedir=/usr/local/mysql-5.7.18 --datadir=/usr/local/mysql-5.7.18/data/3310 --user=mysql
在data/3307, data/3308, data/3309, data/3310 四个目录下分别创建一个my.cnf文件,在四个my.cnf文件分别配置如下内容
注意:不同的实例下配置要修改端口号
[client]
port=3307
socket=/usr/local/mysql-5.7.18/data/3307/mysql.sock
default-character-set=utf8
[mysqld]
port=3307
socket=/usr/local/mysql-5.7.18/data/3307/mysql.sock
datadir=/usr/local/mysql-5.7.18/data/3307
log-error=/usr/local/mysql-5.7.18/data/3307/error.log
pid-file=/usr/local/mysql-5.7.18/data/3307/mysql.pid
character-set-server=utf8 # 字符编码格式
lower_case_table_names=1 # 表名是否区分大小写,默认为1,不区分大小写;0区分大小写
autocommit=1 # 事务是否自动提交,默认为1,自动提交;0不自动提交
切换到/usr/local/mysql-5.7.18/bin目录下,使用 msyqld_safe 命令指定配置文件并启动MySQL服务:
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.18/data/3307/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.18/data/3308/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.18/data/3309/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.18/data/3310/my.cnf &
登录进入每一个MySQL实例客户端,在mysql-5.7.24/bin目录下执行命令:
./mysql -uroot -p -P3307 -h127.0.0.1
./mysql -uroot -p -S /usr/local/mysql-5.7.24/data/3307/mysql.sock
修改mysql的密码,其中123456是我们设置的密码:
alter user 'root'@'localhost' identified by '123456';
授权远程访问(这样远程客户端navicat才能访问)
*.*
的第一个*
表示所有数据库名,第二个*
表示所有的数据库表root@'%'
中的root表示用户名,%
表示所有ip地址,%也可以指定具体的ip地址,比如root@localhost,[email protected]grant all privileges on *.* to root@'%' identified by '123456';
刷新权限
flush privileges;
多实例关闭,切换到/usr/local/mysql-5.7.24/bin目录下,使用 mysqladmin 命令 shutdown
./mysqladmin -uroot -p -P3307 -h127.0.0.1 shutdown
./mysqladmin -uroot -p -S /usr/local/mysql-5.7.24/data/3307/mysql.sock shutdown
当系统对数据的读取比较多时,为了分摊读的压力,可以采用一主多从架构,实现读写分离
这里,我们使用上面配置好的多个MySQL实例,3307为主,3308,3309,3310为从服务器
log-bin=mysql-bin
server-id=3307
server-id=3308
启动主MySQL服务器,登入主MySQL客户端,在主服务器上创建复制数据的账号并授权:
grant replication slave on *.* to 'copy'@'%' identified by '123456';
创建一个MySQL用户,该用户是主服务器专门给从服务器拷贝数据用的
注意:该语句可完成授权、创建用户、修改密码操作
查看主服务器状态:
show master status;
如果主服务状态不是初始状态,需要重置状态:
reset master;
查看从服务器状态
show slave status;
如果从服务器不是初始状态,建议重置一下
stop slave; #停止复制,相当于终止从服务器上的IO和SQL线程
reset slave;
设置从服务器的master,在从服务器客户端执行:
change master to master_host='192.168.235.128',master_user='copy',
master_port=3306,master_password='123456',
master_log_file='mysql-bin.000001',master_log_pos=154;
在从机器上执行开始复制命令:
start slave;
在从服务器的客户端执行以下命令:
\G
:表示格式化输出show slave status \G;
显示结果:
注意:如果 Slave_IO_Running
和 Slave_SQL_Running
均为 YES,则表示主从关系正常
在主服务器上创建数据库、表、数据,然后在从服务器上查看是否已经复制
查看主从复制binlog日志文件内容:
在主服务器客户端执行:
show binlog events in 'mysql-bin.000001'\G;
一主多从,可以缓解读的压力,但是一旦主宕机了,就不能写了,所以我们可以采用双主双从架构来改进它的不足。
架构规划:
在主MySQL的配置文件my.cnf中分别加入(要删除掉中文注释):
log-bin=mysql-bin
server-id=3307
从MySQL配置文件my.cnf中分别加入(注意server-id不能相同):
server-id=3309
在第一台主服务器(3307)的配置文件my.cnf中加入:
auto_increment_increment=2 #主键自动增长的偏移量
auto_increment_offset=1 #不一样的地方,主键增长的起始值
log-slave-updates
sync_binlog=1
而在第一台主服务器(3308)的配置文件my.cnf中加入:
auto_increment_increment=2
auto_increment_offset=2 #不一样的点 相当于起始值
log-slave-updates
sync_binlog=1
配置项说明:
进入/usr/local/mysql-5.7.24/bin目录,重启四个MySQL服务,启动时指定配置文件:
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.24/data/3307/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.24/data/3308/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.24/data/3309/my.cnf &
./mysqld_safe --defaults-file=/usr/local/mysql-5.7.24/data/3310/my.cnf &
登录到主服务器3307|3308的客户端:
在/usr/local/mysql-5.7.24/bin目录下执行 :
./mysql -uroot -p -P3307|3308 -h127.0.0.1
在两台主服务器(3307|3308)上创建复制数据的账号并授权
grant replication slave on *.* to 'copy'@'%' identified by '123456';
在两台主服务器(3307|3308)上停止复制并重置服务器状态
reset master;
登录到从服务器(四个服务器均为从)的客户端:
在/usr/local/mysql-5.7.24/bin目录下执行 :
./mysql -uroot -p -P3307|3308|3309|3310 -h127.0.0.1
在从服务器上停止复制并重置服务器状态:
stop slave;
reset slave;
设置从服务器的master(相当于是4台都需要设置)
设置从服务器3308、3309的主为3307:
change master to master_host='192.168.235.128',master_user='copy',
master_port=3307,master_password='123456',
master_log_file='mysql-bin.000001',master_log_pos=154;
设置从服务器3307、3310的主为3308:
change master to master_host='192.168.235.128',master_user='copy',
master_port=3308,master_password='123456',
master_log_file='mysql-bin.000001',master_log_pos=154;
在从机器上执行开始复制命令(4台MySQL上都执行)
start slave;
验证同一主多从的验证方式,这里就不赘述了
多数据源问题是指在一个项目工程中,需要连接多个数据库,以上配置了数据库的主从,在实际开发中可能会有以下几种情况
我们这里主要给大家介绍Spring+Mybatis和SpringBoot+Mybatis开发模式下的多数据源问题
核心思想:基于动态数据源,在运行的时候才知道要使用哪个数据源
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.1.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-txartifactId>
<version>5.1.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.1.4.RELEASEversion>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatisartifactId>
<version>3.4.6version>
dependency>
<dependency>
<groupId>org.mybatisgroupId>
<artifactId>mybatis-springartifactId>
<version>1.3.2version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>5.1.43version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.1.1version>
dependency>
添加resource,指定编译Mybatis映射文件
<resource>
<directory>src/main/javadirectory>
<includes>
<include>**/*.xmlinclude>
includes>
resource>
在每个线程中存储一个值,该值表示该线程操作数据库使用的是哪个数据源,使用的是ThreadLocal
public class MyThreadLocal {
private static ThreadLocal<String> local = new ThreadLocal<String>();
//指定当前线程使用的是哪个数据源
public static void addDataSource(String dataSourceName) {
local.set(dataSourceName);
}
//获取当前线程使用的数据源
public static String getDataSource() {
return local.get();
}
//重置当前线程使用的数据源
public static void removeDataSource() {
local.remove();
}
}
ThreadLocal详解:ThreadLocal
设计的思路是将所有数据源放到一个map集合中,通过指定map集合的key,可以动态获取不同的数据源
实现一个自定义数据源,数据源对象都要实现 javax.sql.DataSource 接口,AbstractRoutingDataSource 对象的父类实现了这个接口
DynamicDataSource类继承AbstractRoutingDataSource抽象类,并实现抽象方法determineCurrentLookupKey()
在进行数据库操作前,我们提前设置使用哪个数据库,然后在指定的数据源的时候,会自动调用这个determineCurrentLookupKey()方法,得到指定的值,调用指定的数据源。
如何为determineCurrentLookupKey()动态的指定key呢?直观的想法,可能是定义一个静态变量,但是静态变量存在线程安全问题,所以我们需要为每一个线程都维护一个变量,用于指定连接的数据库是哪个,所以我们这里使用线程的副本ThreadLocal类
推荐定义常量,作为动态数据源对应的key
public static final String DATASOURCE_KEY_3307 = "3307";
...
import org.springframework.jdbc.datasource.AbstractDataSource;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
//定义自定义数据源类,继承一个动态数据源的抽象父类
public class DynamicDataSource extends AbstractRoutingDataSource {
//实现父类中的抽象方法,用于返回某个具体的数据源
protected Object determineCurrentLookupKey() {
return MyThreadLocal.getDataSource();
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="masterDataSource3307" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.245.128:3307/workdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
bean>
<bean id="masterDataSource3308" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.245.128:3308/workdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
bean>
<bean id="slaveDataSource3309" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.245.128:3309/workdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
bean>
<bean id="slaveDataSource3310" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://192.168.245.128:3310/workdb"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
bean>
<bean id="dynamicDataSource" class="com.wkcto.masterslave.datasource.DynamicDataSource">
<property name="defaultTargetDataSource" ref="masterDataSource"/>
<property name="targetDataSources">
<map>
<entry key="3307" value-ref="masterDataSource3307">entry>
<entry key="3308" value-ref="masterDataSource3308">entry>
<entry key="3309" value-ref="slaveDataSource3309">entry>
<entry key="3310" value-ref="slaveDataSource3310">entry>
map>
property>
bean>
<bean id="sessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dynamicDataSource"/>
bean>
<bean id="mapperScannerConfigurer" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="sqlSessionFactoryBeanName" value="sessionFactory"/>
<property name="basePackage" value="com.wkcto.masterslave.mapper"/>
bean>
beans>
public void addStu(Stu stu) {
//数据源切换
MyThreadLocal.addDataSource("master");
stuMapper.insert(stu);
stuMapper.insert(stu);
//使用完之后清空ThreadLoacl
MyThreadLocal.removeDataSource();
}
public List<Stu> selectAll() {
MyThreadLocal.addDataSource("slave");
List<Stu>list=stuMapper.selectAll();
MyThreadLocal.removeDataSource();
return list;
}
方法与Spring+Mybatis一样,只是将配置文件改为了配置类:
配置文件标签中的id为配置类中的方法名,class为方法返回值类型
import com.alibaba.druid.pool.DruidDataSource;
import com.wkcto.masterslave.datasource.DynamicDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.mapper.MapperScannerConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
@Configuration
public class DataSourceConfig {
@Bean
public DruidDataSource masterDataSource3307(){
DruidDataSource druidDataSource=new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://192.168.245.128:3307/workdb");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
return druidDataSource;
}
@Bean
public DruidDataSource masterDataSource3308(){
DruidDataSource druidDataSource=new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://192.168.245.128:3308/workdb");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
return druidDataSource;
}
@Bean
public DruidDataSource slaveDataSource3309(){
DruidDataSource druidDataSource=new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://192.168.245.128:3309/workdb");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
return druidDataSource;
}
@Bean
public DruidDataSource slaveDataSource3310(){
DruidDataSource druidDataSource=new DruidDataSource();
druidDataSource.setDriverClassName("com.mysql.jdbc.Driver");
druidDataSource.setUrl("jdbc:mysql://192.168.245.128:3310/workdb");
druidDataSource.setUsername("root");
druidDataSource.setPassword("123456");
return druidDataSource;
}
@Bean
public DynamicDataSource dynamicDataSource(DataSource masterDataSource, DataSource slaveDataSource){
DynamicDataSource dataSource=new DynamicDataSource();
dataSource.setDefaultTargetDataSource(masterDataSource);
Map map=new HashMap();
map.put("3307r",masterDataSource3307);
map.put("3308",masterDataSource3308);
map.put("3309",slaveDataSource3309);
map.put("3310",slaveDataSource3310);
dataSource.setTargetDataSources(map);
return dataSource;
}
@Bean
public SqlSessionFactoryBean sessionFactory(DataSource dynamicDataSource){
SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
factoryBean.setDataSource(dynamicDataSource);
return factoryBean;
}
@Bean
public MapperScannerConfigurer mapperScannerConfigurer(){
MapperScannerConfigurer mapperScannerConfigurer=new MapperScannerConfigurer();
mapperScannerConfigurer.setSqlSessionFactoryBeanName("sessionFactory");
mapperScannerConfigurer.setBasePackage("com.wkcto.masterslave.mapper");
return mapperScannerConfigurer;
}
}