一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中。当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库,通常会做一个数据库集群,开启主从备份,一主多从,以提高读取性能。当用户更多读写分离也无法满足时,就需要分布式数据库了-NoSQL。
正常情况下读写分离的实现,首先要做一个一主多从的数据库集群,同时还需要进行数据同步。
MySQL主从复制
Master配置
1.修改/etc/my.cnf
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
server-id=1
log-bin=mysql-bin
binlog-do-db=zpark
binlog-do-db=baizhi
binlog-ignore-db=mysql
binlog-ignore-db=test
expire_logs_days=10
auto_increment_increment=2
auto_increment_offset=1
[mysqld_safe]
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
2.重启mysql服务
[root@CentOS ~]# service mysqld restart
Stopping mysqld: [ OK ]
Starting mysqld: [ OK ]
3.登陆mysql主机查看运行状态
[root@CentOS ~]# mysql -u root -proot
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 2
Server version: 5.1.73-log Source distribution
Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> show master status\G
*************************** 1. row ***************************
File: mysql-bin.000001
Position: 106
Binlog_Do_DB: zpark,baizhi
Binlog_Ignore_DB: mysql,test
1 row in set (0.00 sec)
Slave配置
1.修改/etc/my.cnf
[mysqld]
datadir=/var/lib/mysql
socket=/var/lib/mysql/mysql.sock
user=mysql
# Disabling symbolic-links is recommended to prevent assorted security risks
symbolic-links=0
server-id=2
log-bin=mysql-bin
replicate-do-db=zpark
replicate-do-db=baizhi
replicate-ignore-db=mysql
replicate-ignore-db=test
expire_logs_days=10
auto_increment_increment=2
auto_increment_offset=2
[mysqld_safe]
log-error=/var/log/mysqld.log
pid-file=/var/run/mysqld/mysqld.pid
2.重启MySQL服务
[root@CentOS ~]# service mysqld restart
Stopping mysqld: [ OK ]
Starting mysqld: [ OK ]
3.登陆MySQL从机配置
- 复制master节点状态的File,以及Position,执行以下命令
change master to
master_host='10.15.0.9',
master_user='root',
master_password='root',
master_log_file='mysql-bin.000001',
master_log_pos=106;
- 开启从节点
start slave;
- 查看从节点状态
show slave status\G
************************** 1. row ***************************
Slave_IO_State: Waiting for master to send event
Master_Host: 10.15.0.9
Master_User: root
Master_Port: 3306
Connect_Retry: 60
Master_Log_File: mysql-bin.000001
Read_Master_Log_Pos: 106
Relay_Log_File: mysqld-relay-bin.000002
Relay_Log_Pos: 283
Relay_Master_Log_File: mysql-bin.000001
Slave_IO_Running: Yes
Slave_SQL_Running: Yes
注意:
1.出现 Slave_IO_Running: Yes 和 Slave_SQL_Running: Yes 说名成功,
2.如果在搭建过程出现错误,可以查看查看错误日志文件 cat /var/log/mysqld.log
注意:如果出现Slave I/O: Fatal error: The slave I/O thread stops because master and slave have equal MySQL server UUIDs; these UUIDs must be different for replication to work. Error_code: 1593错误,请执行如下命令,rm -rf /var/lib/mysql/auto.cnf删除这个文件,之所以出现会出现这样的问题,是因为我的从库主机是克隆的主库所在的主机,所以auto.cnf文件中保存的UUID会出现重复.
基于mycat实现读写分离
mycat引言
基于阿里开源的Cobar产品而研发,Cobar的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得MYCAT一开始就拥有一个很好的起点,站在巨人的肩膀上,我们能看到更远。业界优秀的开源项目和创新思路被广泛融入到MYCAT的基因中,使得MYCAT在很多方面都领先于目前其他一些同类的开源项目,甚至超越某些商业产品。
MYCAT背后有一支强大的技术团队,其参与者都是5年以上资深软件工程师、架构师、DBA等,优秀的技术团队保证了MYCAT的产品质量。MYCAT并不依托于任何一个商业公司,因此不像某些开源项目,将一些重要的特性封闭在其商业产品中,使得开源项目成了一个摆设.
1. 下载mycat
http://dl.mycat.io/1.6-RELEASE/Mycat-server-1.6-RELEASE-20161028204710-linux.tar.gz
2.解压
tar -zxvf Mycat-server-1.6-RELEASE-20161028204710-linux.tar.gz -C /usr
3.配置mycat中conf下的配置schema.xml
<!-- 定义MyCat的逻辑库 -->
<schema name="test_schema" checkSQLschema="false" sqlMaxLimit="100" dataNode="testNode"></schema>
<!-- 定义MyCat的数据节点 -->
<dataNode name="testNode" dataHost="dtHost" database="test" />
<dataHost name="dtHost" maxCon="1000" minCon="10" balance="1"
writeType="0" dbType="mysql" dbDriver="native" switchType="-1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<!--写节点-->
<writeHost host="hostM1" url="192.168.28.128:3306" user="root"
password="root">
<!--从节点-->
<readHost host="hostS1" url="192.168.28.129:3306" user="root" password="root" />
</writeHost>
</dataHost>
4.配置登陆mycat的权限server.xml
<system>
<!-- 这里配置的都是一些系统属性,可以自己查看mycat文-->
<property name="defaultSqlParser">druidparser</property>
<property name="charset">utf8mb4</property>
</system>
<user name="root">
<property name="password">root</property>
<property name="schemas">test_schema</property>
</user>
5.启动mycat
./bin/mycat start
6.连接测试
url: jdbc:mysql://mycat主机名:8066/test_schema(数据库逻辑名一定配置保持一样)
基于SpringBoot实现应用层面的读写分离
读写分离要做的事情就是对于一条SQL该选择哪个数据库去执行,至于谁来做选择数据库这件事儿,无非两个,要么中间件帮我们做,要么程序自己做。因此,一般来讲,读写分离有两种实现方式。第一种是依靠中间件(比如:MyCat),也就是说应用程序连接到中间件,中间件帮我们做SQL分离;第二种是应用程序自己去做分离。
编码思想
所谓的手写读写分离,需要用户自定义一个动态的数据源,该数据源可以根据当前上下文中调用方法是读或者是写方法决定返回主库的链接还是从库的链接。这里我们使用Spring提供的一个代理数据源AbstractRoutingDataSource接口。
该接口需要用户完善一个determineCurrentLookupKey抽象法,系统会根据这个抽象返回值决定使用系统中定义的数据源。
其次该类还有两个属性需要指定defaultTargetDataSource和targetDataSources,其中defaultTargetDataSource需要指定为Master数据源。targetDataSources是一个Map需要将所有的数据源添加到该Map中,以后系统会根据determineCurrentLookupKey方法的返回值作为key从targetDataSources查找相应的实际数据源。如果找不到则使用defaultTargetDataSource指定的数据源。
pom.xml
需要用到AOP的切面编程思想,导入aop.jar
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
application.yml
spring:
#数据源
datasource:
#url: jdbc:mysql://localhost:3306/bigdata?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
# url: jdbc:mysql://10.15.0.40:8066/logic_bigdata
# type: com.alibaba.druid.pool.DruidDataSource
# username: root
# password: root
# driver-class-name: com.mysql.jdbc.Driver
#自定义数据源
master:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://CentOS:3306/bigdata
slave1:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.192.19:3306/bigdata
slave2:
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.192.19:3306/bigdata
#mybatis配置信息
mybatis:
#自定义DataSource不需要如下配置了
#mapper-locations: classpath:com/baizhi/mapper/*.xml
#executor-type: batch
#type-aliases-package: com.baizhi.entities
#开启二级缓存
configuration:
cache-enabled: true
OperatorType操作类型枚举
/操作类型
public enum OperatorType {
WRITE,//写操作
READ//读操作
}
读操作(从机执行操作标记注解)
/**
* 标记的业务方法是否是读操作
*/
@Retention(value = RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD})
public @interface SlaveDB {
}
利用线程局部变量实现操作类型的赋值
//通过线程局部变量传递操作类型
public class OperatorTypeHolderWithThreadLocal {
//定义线程局部变量
private static final ThreadLocal<OperatorType> OPERATOR_TYPE_THREAD_LOCAL = new ThreadLocal<>();
//设置变量值
public static void set(OperatorType type){
OPERATOR_TYPE_THREAD_LOCAL.set(type);
}
//拿到变量里面的值
public static OperatorType get(){
return OPERATOR_TYPE_THREAD_LOCAL.get();
}
//清空
public static void remove(){
OPERATOR_TYPE_THREAD_LOCAL.remove();
}
}
自定义数据源的配置类
//自定义dataSource配置类,用于替换掉默认数据源
@Configuration
public class UserDefinedDataSourceConfiguration {
//masterSource
@Bean
@ConfigurationProperties(prefix = "spring.datasource.master")
public DataSource masterDataSource(){
//主动从上下文中去拿连接数据库的相关配置(application.yml)
return DataSourceBuilder.create().build();
}
//slave1Source
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource(){
return DataSourceBuilder.create().build();
}
//slave2Source
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource(){
return DataSourceBuilder.create().build();
}
//代理数据源
@Bean
public DataSource proxyDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource){
DataSourceProxy proxy = new DataSourceProxy();
//设置默认的数据源
proxy.setDefaultTargetDataSource(masterDataSource);
//设置targetDataSource
HashMap<Object, Object> map = new HashMap<>();
map.put("master",masterDataSource);
map.put("slave-01",slave1DataSource);
map.put("slave-02",slave2DataSource);
//注册所有数据源
proxy.setTargetDataSources(map);
return proxy;
}
/**
* 当自定义数据源,用户必须覆盖SqlSessionFactory创建
* @param dataSource
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory(@Qualifier("proxyDataSource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource);
sqlSessionFactoryBean.setTypeAliasesPackage("com.baizhi.entities");
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath*:com/baizhi/mapper/*.xml"));
return sqlSessionFactoryBean.getObject();
}
/**
* 当自定义数据源,用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
* @param sqlSessionFactory
* @return
*/
@Bean
public SqlSessionTemplate sqlSessionTemplate(@Qualifier("sqlSessionFactory") SqlSessionFactory sqlSessionFactory) {
return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
}
/***
* 当自定义数据源,用户必须注入,否则事务控制不生效
* @param dataSource
* @return
*/
@Bean
public PlatformTransactionManager platformTransactionManager(@Qualifier("proxyDataSource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
}
配置切面,分离读操作,写操作
//在Service方法加一个环绕切面,判断适度方法还是写方法
@Component
@Aspect
@Order(0)//控制切面的顺序,保证在事务切面之前运行切面
@Slf4j
public class ServiceMethodAOP {
//环绕通知
@Around("execution(* com.baizhi.service..*.*(..))")
public Object methodIntercepter(ProceedingJoinPoint pjp) throws Throwable {
//获取当前方发信息
MethodSignature signature = (MethodSignature)pjp.getSignature();
Method method = signature.getMethod();
//获取方法上的注解
boolean isRead = method.isAnnotationPresent(SlaveDB.class);
OperatorType type = OperatorType.READ;
if(isRead){
//如果是写方法
type = OperatorType.WRITE;
}
//设置线程局部变量
OperatorTypeHolderWithThreadLocal.set(type);
log.info("=====当前操作" + type +"==========");
//执行业务方法
Object proceed = pjp.proceed();
//方法执行结束,清空线程局部变量值
OperatorTypeHolderWithThreadLocal.remove();
//返回业务方法的结果
return proceed;
}
}
动态数据源,实现从机之间的负载均衡
//动态数据源,负责负载均衡
@Slf4j
public class DataSourceProxy extends AbstractRoutingDataSource {
//主表key
private String masterKey = "master";
//从表key
private List<String> slaveKeys = Arrays.asList("slave-01","slave-02");
//轮询因子
private static final AtomicInteger round = new AtomicInteger(0);
@Override
/**
* 系统会根据 该方法的返回值,决定使用定义的那种数据源
* 在该方法中,需要判断用户的操作是读操作还是写操作
*/
protected Object determineCurrentLookupKey() {
//返回的key,决定使用哪一个自定义数据源
String useKey = "";
//拿到线程局部变量的值,拿到用户操作状态
OperatorType operatorType = OperatorTypeHolderWithThreadLocal.get();
if(operatorType.equals(OperatorType.WRITE)){
//如果是写操作,需要让主库完成
useKey = masterKey;
}else{
//读操作,由从库完成
//实现一个轮询的负载均衡
int value = round.getAndIncrement();
if(value < 0)
round.set(0);
int size = slaveKeys.size();
int index = round.get() % size;
useKey = slaveKeys.get(index);
}
log.info("=======使用了"+useKey+"数据源===========");
return useKey;
}
}