允许将一个数据库服务器(主数据库)的数据复制到一个或多个数据库服务器中(从数据库)。
默认情况下为异步复制,不需要保持长连接;
可以复制所有的库或者指定的库,或者指定的表
(1)作为备份数据库,当主数据库出现问题时,可以切换到从数据库继续工作;
(2)读写分离,主库写,从库读,降低主数据库的压力;
(3)可以在从数据库上备份,不影响主数据库的性能。
master将数据库的改变记录在二进制日志中,salve同步这些二进制日志,并根据日志执行操作。
(1)主数据库中的所有操作,都被记录在数据库二进制日志文件中;
(2) 从数据库服务器会启动I/O线程,读取主数据库服务器的二进制日志文件,并写入Relay log(中继日志)中;
(3)从数据库服务器开启一个SQL线程,检查Relay log中是否有更新,如果有,则把更新的内容解析成具体的操作,在从数据库中执行一遍。
综上,数据库服务器必须开启二进制日志。
这种方式一般用来做读写分离,master写,其他slave读,这种架构最大问题:I/O压力集中在Master上,每台从服务器都需要在master上读取文件。
使用一台slave中继分担master的压力,只有中继服务器负责读取master中的二进制日志,其他从服务器在中继服务器中读取二进制日志。
中继服务器可以只负责读取,也可以读取二进制日志之后和主服务器数据同步,如果中继服务器只记录二进制日志而不执行,需要修改存储引擎为Black-hole。
开始复制之前,如果主数据库已有内容,应先将主数据库当前的内容同步到从数据库中。
可以新建一个用户专门用于复制,将复制权限Replication Slave赋给该用户。
(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
/home/data/mysql
不存在可以新建,mysql-bin为二进制日志文件的名字;(2)确认主服务器的skip_networking属性的值:
show variables like '%skip_networking%';
当skip_networking=ON时,从数据库服务器无法和主数据库服务器建立通信,复制失败。
(3)配置完成后,重启数据库。
(4)创建用于同步的数据库用户,并赋Replication Slave权限。
(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%
(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;
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)编辑数据库配置文件/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
/home/data/mysql
不存在可以新建,mysql-bin为二进制日志文件的名字;(2)确认主服务器的skip_networking属性的值:
show variables like '%skip_networking%';
当skip_networking=ON时,从数据库服务器无法和主数据库服务器建立通信,复制失败。
(3)配置完成后,重启数据库。
(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
(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)编辑数据库配置文件/etc/mysql/mysql.conf.d/mysqld.cnf
,添加以下内容:
log-bin=/home/data/mysql/mysql-bin-slave1
server-id=3
binlog-format=row
(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模式主从数据库复制的配置完成。
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
@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();
}
}
@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;
}
}
}
用于数据源切换,在获取Connection连接时调用。
根据ThreadLocal中存的数据源类型,决定选择哪个数据源
public class RoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
String typeKey = DataSourceContextHolder.getJdbcType();
if (typeKey == null) {
return "write";
}
return "write";
}
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface DynamicDataSource {
/**
* 数据源key值
*
* @return
*/
String value() default "write";
}
@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();
}
}
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();
}
}
在service层的方法上加入@DynamicDataSource("read")
,根据RoutingDataSource中的定义,如果不加注解,使用的是主数据源。
以上方式在加入@Transactional注解或@LcnTransaction注解后并不能实现切换数据源,原因如下:
在执行方法之前,会先进入事务的拦截器,去获取Connection,此时AbstractRoutingDataSource中determineTargetDataSource()方法会决定要使用哪个数据源,因为在determineCurrentLookupKey()方法中定义默认使用key为write的数据源,所以当前获取的Connection中的数据源为主数据源。
Conncetion获取后,将其放入在全局变量resources中,resources的类型为ThreadLocal
在service中的方法上加@LcnTransaction注解,该方法被调用时,在执行到第一个操作sql语句的方法时获取Connection对象,在此之前缓存中并没有当前groupId对应的对象,所以通过AbstractRoutingDataSource中determineTargetDataSource()方法获取数据源,从而得到Connection,并将其放入缓存中,数据格式为<分布式事务的groupId,
当前方法中之后再取得的连接对象,都是在缓存中取到的。
因为在一个方法中使用的连接对象是同一个,实现不了数据源的切换,所以在既有读又有写的方法中使用主数据源,在只有查询的方法中加@DynamicDataSource("read")
注解。