一、主从复制配置
首先,准备两台服务器,master:192.168.174.10,slave:192.168.174.11。
然后两台服务器上安装Mysql 5.5.5以上版本,因为其默认的执行引擎是支持事务的Innodb。
Centos上安装Mysql。
配置主服务器:
进入 /etc/my.cnf,在[mysqld]标识符下新增以下配置:
server-id =10 # 主服务器标识,唯一即可
log_bin=/usr/local/mysql/data/sql_log/mysql-bin #二进制日志存放路径
binlog-do-db=miaosha # 需要主从复制的数据库,不写默认为全部库
binlog-ignore-db=mysql # 不需要复制的数据库
max_binlog_size = 1000M # 二进制日志文件的最大容量
binlog_format = row # 它不记录sql语句上下文相关信息,仅保存哪条记录被修改
expire_logs_days = 7 # 日志文件保存天数
sync_binlog = 1
bin-log文件:
基本定义:二进制日志,也称为二进制日志,记录对数据发生或潜在发生更改的SQL语句,并以二进制的形式保存在磁盘中;
作用:可以用来查看数据库的变更历史(具体的时间点所有的SQL操作)、数据库增量备份和恢复(增量备份和基于时间点的恢复)、MySQL的复制(主主数据库的复制、主从数据库的复制)。
文件位置:默认存放位置为数据库文件所在目录下,也可以自定义,但是必须是mysql用户的包下。
文件的命名方式: 名称为hostname-bin.xxxxx (重启mysql一次将会自动生成一个新的binlog)
sync_binlog:
sync_binlog=0为默认情况,表示MySQL不控制binlog的刷新,由文件系统自己控制它的缓存的刷新。这时候的性能是最好的,但是风险也是最大的。因为一旦系统Crash,在binlog_cache中的所有binlog信息都会被丢失。
如果sync_binlog>0,表示每sync_binlog次事务提交,MySQL调用文件系统的刷新操作将缓存刷下去。最安全的就是sync_binlog=1了,表示每次事务提交,MySQL都会把binlog刷下去,是最安全但是性能损耗最大的设置。
配置从服务器:
同样进入 /etc/my.cnf,在[mysqld]标识符下新增以下配置:
server-id = 11 # 从服务器标识,唯一即可
relay_log=/usr/local/mysql/data/sql_log/mysqld-relay-log #relay_log 存放位置,必须是mysql用户的包下
master_info_repository = TABLE
relay_log_info_repository =TABLE
read_only=on # 只读
relay_log_recovery = on # 开启
relay log 文件:
由IO thread线程从主库读取的二进制日志事件组成,该日志被Slave上的SQL thread线程执行,从而实现数据的复制。
master_info_repository :
该文件保存slave连接master的状态以及配置信息,如用户名,密码,日志执行的位置。master_info_repository 配置为TABLE,这些信息会被写入mysql.slave_master_info 表中,代替原来的master.info文件了。使用表来代替原来的文件,主要为了crash-safe replication,从而大大提高从库的可靠性。
relay_log_info_repository :
该文件保存slave上relay log的执行位置。设置为TABLE可以避免relay.info更新不及时,SLAVE 重启后导致的主从复制出错。
relay_log_recovery :
当slave从库宕机后,假如relay-log损坏了,导致一部分中继日志没有处理,则自动放弃所有未执行的relay-log,并且重新从master上获取日志,这样就保证了relay-log的完整性。
数据备份与传输:
使用mysqldump 从主库中备份数据保存到all.sql文件,然后将all.sql文件传输给从数据库,最后从数据库执行该all.sql,从而实现数据传输。
mysqldump -u用户名-p密码 数据库 > sql脚本文件路径全名,
上述命令将指定数据库备份到某dump文件(转储文件)中,比如:
mysqldump -uroot -p123456 miaosha > all.sql;
然后将all.sql传输给从服务器:scp all.sql root@192.168.174.11:/home;
最后从服务器执行all.sql文件,实现数据传输:mysql –u用户名 –p密码 –D数据库 < sql脚本文件路径全名,
示例:mysql -uroot -p123456 -Dmiaosha < /home/all.sql
在主服务器上创建一个用户user,并且赋予REPLICATION SLAVE 权限:
登录主服务器的Mysql数据库,执行以下语句:
mysql> CREATE USER user@'192.168.174.%' IDENTIFIED BY '123456';
mysql> GRANT REPLICATION SLAVE ON * . * TO user@'192.168.174.%';
192.168.174.%:表示192.168.174.0~192.168.174.255范围内的服务器都可以使用user用户。
这样子,从服务器就可以通过user用户访问主服务器的二进制日志文件,从而实现数据的复制。
同时,使用 show master status命令,查看主服务器日志文件信息,后续从服务器上创建复制链路需要这些参数。
从服务器上创建复制链路:
登录从服务器的Mysql数据库,执行以下语句:
CHANGE MASTER TO
-> MASTER_HOST='192.168.174.10', # 主服务器ip
-> MASTER_USER='user', # 主服务器上创建的user用户
-> MASTER_PASSWORD='123456', # user用户密码
-> MASTER_LOG_FILE='mysql-bin.000001', # 为master中的二进制日志文件,与上面show master status结果的File一致
-> MASTER_LOG_POS=501; # master中二进制日志文件的起始复制位置,与上面show master status结果的Position一致
然后,开启从服务器的链路,执行 start slave 命令;至此,我们就实现了主从复制,我们可以通过执行 show slave status\G 命令来查询是否开启成功。
二、程序读写分离配置
本文主从数据库的切换需要用到AOP,所以直接引用jar包;
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
配置文件application.yml的数据库配置:
spring:
datasource:
master:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.174.10:3306/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=true
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
max-active: 1000
initial-size: 100
min-idle: 500
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
max-open-prepared-statements: 20
#对于长时间不使用的连接强制关闭
remove-abandoned: true
#超过5分钟开始关闭空闲连接
remove-abandoned-timeout: 300
slave:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.174.11:3306/miaosha?useUnicode=true&characterEncoding=utf8&useSSL=true
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
max-active: 1000
initial-size: 100
min-idle: 500
max-wait: 60000
time-between-eviction-runs-millis: 60000
min-evictable-idle-time-millis: 300000
validation-query: select 1
test-while-idle: true
test-on-borrow: false
test-on-return: false
max-open-prepared-statements: 20
#对于长时间不使用的连接强制关闭
remove-abandoned: true
#超过5分钟开始关闭空闲连接
remove-abandoned-timeout: 300
数据库配置类 DataSourceConfiguration:
@Configuration
@Slf4j
public class DataSourceConfiguration {
/**
* 主数据源
* @return
*/
@Bean(name = "writeDataSource")
@Primary
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource writeDataSource(){
log.info("---------------writeDataSource init -----------------");
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
/**
* 从数据源
* @return
*/
@Bean(name = "readDataSource")
@ConfigurationProperties(prefix = "spring.datasource.slave")
public DataSource readDataSource(){
log.info("---------------readDataSource init -----------------");
return DataSourceBuilder.create().type(DruidDataSource.class).build();
}
}
自定义一个数据库注解,用于标识读写数据库 @DataSource。
package com.imooc.miaosha.config;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Retention(RUNTIME)
@Target(METHOD)
public @interface DataSource {
String value() default "";
}
@DataSource的value有两种,一种是“write”写库,另一种是"read"读库,所以我们写一个枚举类。
package com.imooc.miaosha.enums;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum DataSourceType {
READ("read"),
WRITE("write");
private String type;
}
使用ThreadLocal来存放当前线程的读写类型(write或者read),后续会采用AOP获取@DataSource的value值,往ThreadLocal中set进去。
package com.imooc.miaosha.config;
import com.imooc.miaosha.enums.DataSourceType;
/**
* 本地线程全部变量-数据源
*/
public class DataSourceContextHolder {
private static final ThreadLocal<String> dataSourceContext = new ThreadLocal<>();
public static void read(){
dataSourceContext.set(DataSourceType.READ.getType());
}
public static void write(){
dataSourceContext.set(DataSourceType.WRITE.getType());
}
public static String getJdbcType(){
return dataSourceContext.get();
}
}
创建MyAbstractRoutingDataSource,继承AbstractRoutingDataSource,并重写determineCurrentLookupKey()方法。每次访问数据库,都会调用getConnection()方法,去获取数据库连接,该方法里面调用了determineTargetDataSource()方法,然后在determineTargetDataSource()方法里面调用了AbstractRoutingDataSource类里的抽象方法determineCurrentLookupKey()。这时候我们需要重写该抽象方法来通过ThreadLocal获取当前的数据库类型的标识(write或者read),从而决定采用哪种数据库。
package com.imooc.miaosha.config;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
/**
* 重写determineCurrentLookupKey方法,因为每次获取数据库连接时,会调用getConnection()方法,
* 该方法里面调用了determineTargetDataSource()方法,然后determineTargetDataSource()方法里面调用了
* AbstractRoutingDataSource类里的抽象方法determineCurrentLookupKey()。
*/
public class MyAbstractRoutingDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getJdbcType();
}
}
Mybatis配置类 MyBatisConfiguration :
package com.imooc.miaosha.config;
import com.google.common.collect.Maps;
import com.imooc.miaosha.enums.DataSourceType;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.Map;
@Configuration
public class MyBatisConfiguration {
@Resource(name = "writeDataSource")
private DataSource writeDataSource;
@Resource(name = "readDataSource")
private DataSource readDataSource;
@Bean
public DataSource dynamicDataSource(){
MyAbstractRoutingDataSource proxy = new MyAbstractRoutingDataSource();
Map<Object, Object> targetDataSources = Maps.newHashMap();
targetDataSources.put(DataSourceType.READ.getType(), readDataSource);
targetDataSources.put(DataSourceType.WRITE.getType(), writeDataSource);
proxy.setDefaultTargetDataSource(writeDataSource);
proxy.setTargetDataSources(targetDataSources);
return proxy;
}
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dynamicDataSource());
org.apache.ibatis.session.Configuration configuration = sqlSessionFactoryBean.getObject().getConfiguration();
configuration.setMapUnderscoreToCamelCase(true);
configuration.setUseGeneratedKeys(true);
return sqlSessionFactoryBean.getObject();
}
}
这里将读写数据源的bean放入AbstractRoutingDataSource 中,其中key为数据库标识write或者read,value为对应的读写数据源bean。这样就可以通过ThreadLocal获取到的当前数据库标识,去取对应的数据源bean了,从而实现读写分离。
添加事务管理配置 DataSourceTransactionManager ,因为只有写库涉及到事务,所以只需要将写数据源放入事务管理即可:
package com.imooc.miaosha.config;
import org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.annotation.Resource;
import javax.sql.DataSource;
@Configuration
@EnableTransactionManagement
public class DataSourceTransactionManager extends DataSourceTransactionManagerAutoConfiguration {
@Resource(name = "writeDataSource")
private DataSource writeDataSource;
@Bean
public org.springframework.jdbc.datasource.DataSourceTransactionManager transactionManager(){
return new org.springframework.jdbc.datasource.DataSourceTransactionManager(writeDataSource);
}
}
使用AOP,读取指定包下@DataSource的值:
package com.imooc.miaosha.aop;
import com.imooc.miaosha.config.DataSource;
import com.imooc.miaosha.config.DataSourceContextHolder;
import com.imooc.miaosha.enums.DataSourceType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
@Aspect
@Slf4j
@Component
public class DataSourceAop {
/**
* @annotation(com.imooc.miaosha.config.DataSource)
* 所有含有@DataSource 注解的方法都将匹配到
*/
@Before("@annotation(com.imooc.miaosha.config.DataSource)")
public void setDataSourceType(JoinPoint joinPoint) {
Object target = joinPoint.getTarget();
// 获取方法名称
String methodName = joinPoint.getSignature().getName();
// 获取方法参数类型
Class<?>[] parameterTypes = ((MethodSignature) joinPoint.getSignature()).getMethod().getParameterTypes();
try {
Method method = target.getClass().getMethod(methodName, parameterTypes);
DataSource dataSource = method.getAnnotation(DataSource.class);
// 为空什么都不做,因为一开始我们设置了master为默认数据库
if (dataSource == null) return;
String value = dataSource.value();
if (DataSourceType.READ.getType().equals(value)){
DataSourceContextHolder.read();
}else if (DataSourceType.WRITE.getType().equals(value)){
DataSourceContextHolder.write();
}
log.info("dataSource切换到:{}", value);
} catch (NoSuchMethodException e) {
e.printStackTrace();
}
}
/**
* 方法走完要清空ThreadLocal,否则下次如果没有指定@DataSource注解,
* 则不会使用默认的master数据源(即写数据源),而是上一次的数据源
* @param joinPoint
*/
@After("@annotation(com.imooc.miaosha.config.DataSource)")
public void afterSetDataSourceType(JoinPoint joinPoint) {
DataSourceContextHolder.clearDB();
}
}
以上配置完成之后,就实现了数据库的读写分离,是不是很给力!
package com.imooc.miaosha.service.impl;
import java.util.List;
import com.imooc.miaosha.config.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.imooc.miaosha.mapper.GoodsMapper;
import com.imooc.miaosha.model.Goods;
import com.imooc.miaosha.model.MiaoshaGoods;
import com.imooc.miaosha.service.GoodsService;
import com.imooc.miaosha.vo.GoodsVO;
@Service
public class GoodsServiceImpl implements GoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Override
@DataSource("read")
public List<GoodsVO> getAllGoodsInfo() {
return goodsMapper.findAllGoodsInfo();
}
@Override
public Goods findOne(Long goodsId) {
return goodsMapper.findOne(goodsId);
}
@Override
@DataSource("write")
public boolean reduceMiaoshaStock(Long goodsId) {
int i = goodsMapper.decreaseStock(goodsId);
return i == 1;
}
小结:当一个客户端请求过来,会调用impl包下的service实现类,aop通过扫描实现类中方法上的@DataSource注解,如果没有该注解,则采用默认的写数据源;如果有该注解,则获取注解中的value值,并且set进去ThreadLocal。接着去获取数据源,即调用getConnection()方法,会调用我们重写的determineCurrentLookupKey()方法从ThreadLocal中获取当前的数据源类型,AbstractRoutingDataSource会根据当前的数据源类型,取出对应的数据源,从而执行SQL语句。