MySQL读写分离实现
数据库写入效率要低于读取效率,一般系统中数据读取频率高于写入频率,单个数据库实例在写入的时候会影响读取性能,这是做读写分离的原因。
实现方式主要基于mysql的主从复制,通过路由的方式使应用对数据库的写请求只在master上进行,读请求在slave上进行。
mysql主从复制:https://www.jianshu.com/p/a68551347d7d
路由的方式主要有两种:
1.代理
在应用和数据库之间增加代理层,代理层接收应用对数据库的请求,根据不同请求类型转发到不同的实例,在实现读写分离的同时可以实现负载均衡。

目前常用的mysql的读写分离中间件有amoeba,MySQL-Proxy
2.应用内路由
在应用程序中实现,针对不同的请求类型去不同的实例执行sql

本文主要介绍第二种方式。基于springboot、 mybatis实现。
思路:之前在做项目的时候实现过mybatis数据源的动态切换。基于原来的方案,用aop来拦截dao层方法,根据方法名称就可以判断要执行的sql类型,动态切换主从数据源。
1.mybatis和数据源配置

2.数据源切换
切换数据源需要用到类AbstractRoutingDataSource

targetDataSources用一个map来存储配置的数据源,defaultTargetDataSource默认的数据源

项目启动时targetDataSources中的值会放到resolvedDataSources,key默认为targetDataSources中的key,可以实现resolveSpecifiedLookupKey()方法处理。
resolvedDefaultDataSource会被赋值给defaultTargetDataSource,因此如果defaultTargetDataSource没有配启动会报错 。

在需要与mysql交互时检索resolvedDataSources中的数据源,通过抽象determineCurrentLookupKey()获取当前数据源的key,因此实现这个方法可以实现数据源的切换。
数据源加载:
/**
* Title:MybatisConfiguration
*
* @author angla
**/
@Configuration
public class MybatisConfiguration {
@Autowired
private Environment env;
/**
* 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
*/
@Bean
public DataSource masterDataSource() throws Exception {
Properties props = new Properties();
props.put("driverClassName", env.getProperty("spring.mastersource.driver-class-name"));
props.put("url", env.getProperty("spring.mastersource.url"));
props.put("username", env.getProperty("spring.mastersource.username"));
props.put("password", env.getProperty("spring.mastersource.password"));
return DruidDataSourceFactory.createDataSource(props);
} @Bean
public DataSource slaveDataSource() throws Exception {
Properties props = new Properties();
props.put("driverClassName", env.getProperty("spring.slavesource1.driver-class-name"));
props.put("url", env.getProperty("spring.slavesource1.url"));
props.put("username", env.getProperty("spring.slavesource1.username"));
props.put("password", env.getProperty("spring.slavesource1.password"));
return DruidDataSourceFactory.createDataSource(props);
}
/**
* @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
* @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
*/
@Bean
@Primary
@DependsOn({"masterDataSource","slaveDataSource"})
public DynamicDataSource dataSource(DataSource masterDataSource, DataSource slaveDataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName(), masterDataSource);
targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName(),slaveDataSource); DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
dataSource.setDefaultTargetDataSource(slaveDataSource);// 默认的datasource设置为myTestDbDataSource return dataSource;
} /**
* 根据数据源创建SqlSessionFactory
*/
@Bean
public SqlSessionFactory sqlSessionFactory(DynamicDataSource ds) throws Exception {
SqlSessionFactoryBean fb = new SqlSessionFactoryBean();
fb.setDataSource(ds);// 指定数据源
fb.setTypeAliasesPackage(env.getProperty("mybatis.typeAliasesPackage"));// 指定基包
fb.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources(Objects.requireNonNull(env.getProperty(
"mybatis.mapperLocations"))));
return fb.getObject();
} /**
* 配置事务管理器
*/
@Bean
public DataSourceTransactionManager transactionManager(DynamicDataSource dataSource) throws Exception {
return new DataSourceTransactionManager(dataSource);
} }
数据源枚举: /**
* Title:DataSourceTypeEnum
*
* @author angla
**/ public enum DataSourceTypeEnum { DATA_SOURCE_MASTER(1,"master"),
DATA_SOURCE_SLAVE(2,"slave"); DataSourceTypeEnum(Integer code, String name) {
this.code = code;
this.name = name;
} private Integer code; private String name; public Integer getCode() {
return code;
} public String getName() {
return name;
} }
定义ThreadLocal存储
/**
* Title:DataSourceContextHolder
*
* @author angla
**/
public class DataSourceContextHolder { private static final ThreadLocal<DataSourceTypeEnum> contextHolder = new ThreadLocal<>(); public static void setDatabaseType(DataSourceTypeEnum databaseType) {
contextHolder.set(databaseType);
} public static DataSourceTypeEnum getDatabaseType() {
return contextHolder.get();
} }
实现determineCurrentLookupKey方法 /**
* Title:DynamicDataSource
*
* @author angla
**/
public class DynamicDataSource extends AbstractRoutingDataSource {
protected Object determineCurrentLookupKey() {
return DataSourceContextHolder.getDatabaseType();
}
}
定义aop拦截dao层方法:
@Component
@Aspect
@Slf4j
public class DataSourceAspect { private static final String[] queryStrs = {"query", "select", "get"}; /**
* 定义切入点,切入点为com.angla.demo.dao下的所有方法
*/
@Pointcut("execution(* com.angla.demo.dao.*.*(..))")
public void executeSql() {
} /**
* 前置通知:在连接点之前执行的通知
*
* @param joinPoint
* @throws Throwable
*/
@Before("executeSql()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
String mName = methodSignature.getMethod().getName();
log.info("拦截sql方法:{}", mName);
DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_MASTER);
for (String name : queryStrs) {
if (mName.startsWith(name)) {
log.info("查询语句,设置数据源为slave");
DataSourceContextHolder.setDatabaseType(DataSourceTypeEnum.DATA_SOURCE_SLAVE);
break;
}
}
log.info("当前数据源:{}",DataSourceContextHolder.getDatabaseType().getName());
} }
至此,一个简单的读写分离实现就完成了,测试下结果:

停掉master实例,写数据报错,可以正常读取数据,停掉slave实例可以正常写数据,不能读取数据,结果是没问题的。但是这样还不够,现在加载数据源只能加载一主一从,不能适用一主多从或者多主多从的情况,后面需要改下数据源加载和获取方式。
多主多从配置:

加载数据源配置:
@Data
@Component
@ConfigurationProperties(prefix = "spring")
public class DataSourceProperties { private List<Map<String,String>> mastersources; private List<Map<String,String>> slavesources; } @Autowired
private DataSourceProperties dataSourceProperties; /**
* 创建数据源(数据源的名称:方法名可以取为XXXDataSource(),XXX为数据库名称,该名称也就是数据源的名称)
*/
@Bean
public List<DataSource> masterDataSources() throws Exception { List<Map<String, String>> mastersources = dataSourceProperties.getMastersources();
if (CollectionUtils.isEmpty(mastersources)) {
throw new IllegalArgumentException("需要至少一个主数据源");
}
List<DataSource> dataSources = new ArrayList<>();
for (Map map : mastersources) {
dataSources.add(DruidDataSourceFactory.createDataSource(map));
}
return dataSources;
} @Bean
public List<DataSource> slaveDataSources() throws Exception {
List<Map<String, String>> slavesources = dataSourceProperties.getSlavesources();
if (CollectionUtils.isEmpty(slavesources)) {
throw new IllegalArgumentException("需要至少一个从数据源");
}
List<DataSource> dataSources = new ArrayList<>();
for (Map map : slavesources) {
dataSources.add(DruidDataSourceFactory.createDataSource(map));
}
return dataSources;
} /**
* @Primary 该注解表示在同一个接口有多个实现类可以注入的时候,默认选择哪一个,而不是让@autowire注解报错
* @Qualifier 根据名称进行注入,通常是在具有相同的多个类型的实例的一个注入(例如有多个DataSource类型的实例)
*/
@Bean
@Primary
@DependsOn({"masterDataSources", "slaveDataSources"})
public DynamicDataSource dataSource(List<DataSource> masterDataSources, List<DataSource> slaveDataSources) {
Map<Object, Object> targetDataSources = new HashMap<>();
for (int i = 0; i < masterDataSources.size(); i++) {
targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_MASTER.getName() + i, masterDataSources.get(i));
}
for (int i = 0; i < slaveDataSources.size(); i++) {
targetDataSources.put(DataSourceTypeEnum.DATA_SOURCE_SLAVE.getName() + i, slaveDataSources.get(i));
} DynamicDataSource dataSource = new DynamicDataSource();
dataSource.setTargetDataSources(targetDataSources);// 该方法是AbstractRoutingDataSource的方法
dataSource.setDefaultTargetDataSource(slaveDataSources.get(0));// 默认的datasource设置为myTestDbDataSource return dataSource;
}
用随机的方式获取数据源:
@Slf4j
public class DynamicDataSource extends AbstractRoutingDataSource { @Autowired
private DataSourceProperties dataSourceProperties; protected Object determineCurrentLookupKey() {
DataSourceTypeEnum dataSourceType = DataSourceContextHolder.getDatabaseType();
int i;
List masterSources = dataSourceProperties.getMastersources();
List slaveSources = dataSourceProperties.getSlavesources();
if (dataSourceType.equals(DataSourceTypeEnum.DATA_SOURCE_MASTER)) {
i = ThreadLocalRandom.current().nextInt(masterSources.size()) % masterSources.size();
} else {
i = ThreadLocalRandom.current().nextInt(slaveSources.size()) % slaveSources.size();
}
return dataSourceType.getName() + i;
}
}
当然数据源加载完成后也可以用其他方式来做多数据源的负载均衡,只需要重写determineCurrentLookupKey()方法就行。
MySQL读写分离实现的更多相关文章
- mysql读写分离(PHP类)
mysql读写分离(PHP类) 博客分类: php mysql 自己实现了php的读写分离,并且不用修改程序 优点:实现了读写分离,不依赖服务器硬件配置,并且都是可以配置read服务器,无限扩展 ...
- amoeba实现MySQL读写分离
amoeba实现MySQL读写分离 准备环境:主机A和主机B作主从配置,IP地址为192.168.131.129和192.168.131.130,主机C作为中间件,也就是作为代理服务器,IP地址为19 ...
- PHP代码实现MySQL读写分离
关于MySQL的读写分离有几种方法:中间件,Mysql驱动层,代码控制 关于中间件和Mysql驱动层实现Mysql读写分离的方法,今天暂不做研究, 这里主要写一点简单的代码来实现由PHP代码控制MyS ...
- 转:Mysql读写分离实现的三种方式
1 程序修改mysql操作类可以参考PHP实现的Mysql读写分离,阿权开始的本项目,以php程序解决此需求.优点:直接和数据库通信,简单快捷的读写分离和随机的方式实现的负载均衡,权限独立分配缺点:自 ...
- 使用Atlas实现MySQL读写分离+MySQL-(Master-Slave)配置
参考博文: MySQL-(Master-Slave)配置 本人按照博友北在北方的配置已成功 我使用的是 mysql5.6.27版本. 使用Atlas实现MySQL读写分离 数据切分——Atlas读 ...
- MySQL读写分离技术
1.简介 当今MySQL使用相当广泛,随着用户的增多以及数据量的增大,高并发随之而来.然而我们有很多办法可以缓解数据库的压力.分布式数据库.负载均衡.读写分离.增加缓存服务器等等.这里我们将采用读写分 ...
- php实现MySQL读写分离
MySQL读写分离有好几种方式 MySQL中间件 MySQL驱动层 代码控制 关于 中间件 和 驱动层的方式这里不做深究 暂且简单介绍下 如何通过PHP代码来控制MySQL读写分离 我们都知道 &q ...
- [记录]MySQL读写分离(Atlas和MySQL-proxy)
MySQL读写分离(Atlas和MySQL-proxy) 一.阿里云使用Atlas从外网访问MySQL(RDS) (同样的方式修改配置文件可以实现代理也可以实现读写分离,具体看使用场景) 1.在跳板机 ...
- docker环境 mysql读写分离 mycat maxscale
#mysql读写分离测试 环境centos 7.4 ,docker 17.12 ,docker-compose mysql 5.7 主从 mycat 1.6 读写分离 maxscale 2.2.4 读 ...
- mysql读写分离总结
随着一个网站的业务不断扩展,数据不断增加,数据库的压力也会越来越大,对数据库或者SQL的基本优化可能达不到最终的效果,我们可以采用读写分离的策略来改变现状.读写分离现在被大量应用于很多大型网站,这个技 ...
随机推荐
- gravity layout_gravity
gravity:控制当前视图的内容/子view layout_gravity:控制视图本身
- Linux监控命令
dd命令用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换.注意:指定数字的地方若以下列字符结尾,则乘以相应的数字:b=512:c=1:k=1024:w=2它不是一个专业的测试工具,不过如果对于 ...
- mysql 中varchar(50)最多能存多少个汉字
首先要确定mysql版本4.0版本以下,varchar(50),指的是50字节,如果存放UTF8汉字时,只能存16个(每个汉字3字节) 5.0版本以上,varchar(50),指的是50字符,无论存放 ...
- TCO 2016 Round 1B
problem 250 Problem Statement Vasa likes to construct sequences of numbers. If you tell him a positi ...
- C/S转分布式数据库的解决方法
C/S转分布式数据库的解决方法1. 直接VPN建一个网不就行了.(大概是虚拟成一个网络)2. 直连也可以,就是速度慢3. 还是三层吧,推荐RTC4. 弄个花生壳硬件试试呢,成本低,不用改程序5. 搞一 ...
- DedeCMS模板中用彩色tag做彩色关键词
DedeCMS模板中用彩色tag做彩色关键词,下面分享一下吧!修改方法: 1.在/include/common.func.php 中加入如下函数: function getTagStyle() { $ ...
- codeforces 443 B. Kolya and Tandem Repeat 解题报告
题目链接:http://codeforces.com/contest/443/problem/B 题目意思:给出一个只有小写字母的字符串s(假设长度为len),在其后可以添加 k 个长度的字符,形成一 ...
- oracle:数据库版本问题导致的bug
公司开发出来的系统,由于各现场oracle数据库版本有10.2.0.4.11.2.0.1.11.2.0.3.11.2.0.4: 进而会导致版本不一导致错误问题.下面列举2个: 1.wm_concat ...
- java -- 虚拟机和内存
从大方向来分:栈内存,堆内存,方法区,本地方法栈,程序计数器 java从存储数据的角度来分: 寄存器(register):最快的存储区,由编译器根据需求进行分配,不由认为控制. 堆栈(statck): ...
- 【转】图像金字塔PyrDown,PyrUP
原文链接:http://blog.csdn.net/davebobo/article/details/51885043 [图像金字塔] 图像金字塔这个词,我们经常在很多地方可以看到.它是图像多尺度表达 ...