为什么需要读写分离

当项目越来越大和并发越来大的情况下,单个数据库服务器的压力肯定也是越来越大,最终演变成数据库成为性能的瓶颈,而且当数据越来越多时,查询也更加耗费时间,当然数据库数据过大时,可以采用数据库分库分表,同时数据库压力过大时,也可以采用Redis等缓存技术来降低压力,但是任何一种技术都不是万金油,很多时候都是通过多种技术搭配使用,而本文主要就是介绍通过读写分离来加快数据库读取速度

实现方式

读写分离实现的方式有多种,但是多种都需要配置数据库的主从复制

方式一

数据库中间件实现,如Mycat等数据库中间件,对于项目本身来说,只有一个数据源,就是链接到Mycat,再由mycat根据规则去选择从哪个库获取数据

方式二

代码中配置多数据源,通过代码控制使用哪个数据源,本文也是主要介绍这种方式

优点

1.降低数据库读取压力,尤其是有些需要大量计算的实时报表类应用

2.增强数据安全性,读写分离有个好处就是数据近乎实时备份,一旦某台服务器硬盘发生了损坏,从库的数据可以无限接近主库

3.可以实现高可用,当然只是配置了读写分离并不能实现搞可用,最多就是在Master(主库)宕机了还能进行查询操作,具体高可用还需要其他操作

缺点

1.增大成本,一台数据库服务器和多台数据库的成本肯定是不一样的

2.增大代码复杂度,不过这点还比较轻微吧,但是也的确会一定程度上加重

3.增大写入成本,虽然降低了读取成本,但是写入成本却是一点也没有降低,毕竟还有从库一直在向主库请求数据

实践:

项目结构:

application.yml

spring:
datasource:
master: # 写账户
jdbc-url: jdbc:mysql://localhost:3306/otadb?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456
slave1: # 只读账户
jdbc-url: jdbc:mysql://localhost:3306/otadb1?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456
slave2: # 只读账户
jdbc-url: jdbc:mysql://localhost:3306/otadb2?useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456

多数据源配置

import com.microservice.readwriteseparat.bean.MyRoutingDataSource;
import com.microservice.readwriteseparat.enums.DBTypeEnum;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
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 { /**
* 写库
* @return
*/
@Bean
@ConfigurationProperties("spring.datasource.master")
public DataSource masterDataSource() {
return DataSourceBuilder.create().build();
} /**
* 读库
* @return
*/
@Bean
@ConfigurationProperties("spring.datasource.slave1")
public DataSource slave1DataSource() {
return DataSourceBuilder.create().build();
} /**
* 读库
* @return
*/
@Bean
@ConfigurationProperties("spring.datasource.slave2")
public DataSource slave2DataSource() {
return DataSourceBuilder.create().build();
} @Bean
public DataSource myRoutingDataSource(@Qualifier("masterDataSource") DataSource masterDataSource,
@Qualifier("slave1DataSource") DataSource slave1DataSource,
@Qualifier("slave2DataSource") DataSource slave2DataSource) {
Map<Object, Object> targetDataSources = new HashMap<>();
targetDataSources.put(DBTypeEnum.MASTER, masterDataSource);
targetDataSources.put(DBTypeEnum.SLAVE1, slave1DataSource);
targetDataSources.put(DBTypeEnum.SLAVE2, slave2DataSource);
MyRoutingDataSource myRoutingDataSource = new MyRoutingDataSource();
myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);
myRoutingDataSource.setTargetDataSources(targetDataSources);
return myRoutingDataSource;
} }

这里,我们配置了4个数据源,1个master,2两个slave,1个路由数据源。前3个数据源都是为了生成第4个数据源,而且后续我们只用这最后一个路由数据源myRoutingDataSource。

MyBatis配置

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.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement; import javax.annotation.Resource;
import javax.sql.DataSource; @EnableTransactionManagement
@Configuration
public class MyBatisConfig {
@Resource(name = "myRoutingDataSource")
private DataSource myRoutingDataSource; /**
* 扫描mybatis下的xml文件
* @return
* @throws Exception
*/
@Bean
public SqlSessionFactory sqlSessionFactory() throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(myRoutingDataSource);
sqlSessionFactoryBean.setMapperLocations(new PathMatchingResourcePatternResolver().getResources("classpath:mybatis/*.xml"));
return sqlSessionFactoryBean.getObject();
} @Bean
public PlatformTransactionManager platformTransactionManager() {
return new DataSourceTransactionManager(myRoutingDataSource);//由于Spring容器中现在有4个数据源,所以我们需要为事务管理器和MyBatis手动指定一个明确的数据源。
}
}

定义一个枚举来代表这三个数据源

public enum DBTypeEnum {
MASTER,SLAVE1,SLAVE2
}

通过ThreadLocal将数据源设置到每个线程上下文中

import com.microservice.readwriteseparat.enums.DBTypeEnum;

import java.util.concurrent.atomic.AtomicInteger;

public class DBContextHolder {
private static final ThreadLocal<DBTypeEnum> contextHolder = new ThreadLocal<>();
private static final AtomicInteger counter = new AtomicInteger(-1); public static void set(DBTypeEnum dbType) {
contextHolder.set(dbType);
} public static DBTypeEnum get() {
return contextHolder.get();
} public static void master() {
set(DBTypeEnum.MASTER);
System.out.println("切换到master");
} public static void slave() {
// 轮询
int index = counter.getAndIncrement() % 2;
if (counter.get() > 9999) {
counter.set(-1);
}
if (index == 0) {
set(DBTypeEnum.SLAVE1);
System.out.println("切换到slave1");
}else {
set(DBTypeEnum.SLAVE2);
System.out.println("切换到slave2");
}
} }

获取路由key

import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable; public class MyRoutingDataSource extends AbstractRoutingDataSource {
@Nullable
@Override
protected Object determineCurrentLookupKey() {
return DBContextHolder.get();
}
}

默认情况下,所有的查询都走从库,插入/修改/删除走主库。我们通过方法名来区分操作类型(CRUD)

import com.microservice.readwriteseparat.bean.DBContextHolder;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component; @Aspect
@Component
public class DataSourceAop {
/**
* 只读:
* 不是Master注解的对象或方法 && select开头的方法 || get开头的方法
*/
@Pointcut("!@annotation(com.microservice.readwriteseparat.annotation.Master) " +
"&& (execution(* com.microservice.readwriteseparat.service..*.select*(..)) " +
"|| execution(* com.microservice.readwriteseparat.service..*.get*(..)))")
public void readPointcut() { } /**
* 写:
* Master注解的对象或方法 || insert开头的方法 || add开头的方法 || update开头的方法
* || edlt开头的方法 || delete开头的方法 || remove开头的方法
*/
@Pointcut("@annotation(com.microservice.readwriteseparat.annotation.Master) " +
"|| execution(* com.microservice.readwriteseparat.service..*.insert*(..)) " +
"|| execution(* com.microservice.readwriteseparat.service..*.add*(..)) " +
"|| execution(* com.microservice.readwriteseparat.service..*.update*(..)) " +
"|| execution(* com.microservice.readwriteseparat.service..*.edit*(..)) " +
"|| execution(* com.microservice.readwriteseparat.service..*.delete*(..)) " +
"|| execution(* com.microservice.readwriteseparat..*.remove*(..))")
public void writePointcut() { } @Before("readPointcut()")
public void read() {
DBContextHolder.slave();
} @Before("writePointcut()")
public void write() {
DBContextHolder.master();
}
}

有一般情况就有特殊情况,特殊情况是某些情况下我们需要强制读主库,针对这种情况,我们定义一个主键,用该注解标注的就读主库

public @interface Master {
}

mapper:

import com.microservice.readwriteseparat.po.TestPO;
import org.apache.ibatis.annotations.Mapper; @Mapper
public interface TestPOMapper {
int deleteByPrimaryKey(Long id); int insert(TestPO record); int insertSelective(TestPO record); TestPO selectByPrimaryKey(Long id); int updateByPrimaryKeySelective(TestPO record); int updateByPrimaryKey(TestPO record);
}

po:

public class TestPO {
private Long id; private String name; public Long getId() {
return id;
} public void setId(Long id) {
this.id = id;
} public String getName() {
return name;
} public void setName(String name) {
this.name = name == null ? null : name.trim();
}
}

service:

import com.microservice.readwriteseparat.annotation.Master;
import com.microservice.readwriteseparat.mapper.TestPOMapper;
import com.microservice.readwriteseparat.po.TestPO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; @Service
public class TestService{ @Autowired
private TestPOMapper testPOMapper; public int insert(TestPO aaa) {
return testPOMapper.insert(aaa);
} @Master
public int save(TestPO aaa) {
return testPOMapper.insert(aaa);
} public TestPO selectByPrimaryKey(Long id) {
return testPOMapper.selectByPrimaryKey(id);
} @Master
public TestPO getById(Long id) {
// 有些读操作必须读主数据库
// 比如,获取微信access_token,因为高峰时期主从同步可能延迟
// 这种情况下就必须强制从主数据读
return testPOMapper.selectByPrimaryKey(id);
}
}

测试:

import com.microservice.readwriteseparat.po.TestPO;
import com.microservice.readwriteseparat.service.TestService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class)
@SpringBootTest
public class ReadwriteseparatApplicationTests {
private static final Logger logger = LoggerFactory.getLogger(ReadwriteseparatApplicationTests.class); @Autowired
private TestService aaaService; /**
* 写库进行写入
*/
@Test
public void testWrite() {
TestPO aaa = new TestPO();
aaaService.insert(aaa);
} /**
* 读库(otadb1和otadb2随机)进行读取
*/
@Test
public void testRead() {
TestPO aaa = aaaService.selectByPrimaryKey(1l);
logger.info("aaa="+aaa.toString());
} /**
* 写库进行写入
*/
@Test
public void testSave() {
TestPO aaa = new TestPO();
aaaService.save(aaa);
} /**
* 写库进行读取
*/
@Test
public void testReadFromMaster() {
aaaService.getById(10001l);
} }

查看控制台:

源码地址:https://github.com/qjm201000/micro_service_readwriteseparat.git

参考资料:https://www.cnblogs.com/cjsblog/p/9712457.html

微服务-springboot-读写分离(多数据源切换)的更多相关文章

  1. Mycat读写分离、主从切换、分库分表的操作记录

    系统开发中,数据库是非常重要的一个点.除了程序的本身的优化,如:SQL语句优化.代码优化,数据库的处理本身优化也是非常重要的.主从.热备.分表分库等都是系统发展迟早会遇到的技术问题问题.Mycat是一 ...

  2. mycat读写分离与主从切换【转】

    什么是mycat,以及mycat的优点和特性本文不做赘述,本文继续本着实战的态度,来分享一些个人对mycat的基础功能实践.本文mycat的读写分离和主从切换的环境为mysql主从环境. 如何安装my ...

  3. Mycat读写分离、主从切换学习(转)

    http://blog.csdn.net/zhanglei_16/article/details/50707487 Mycat读写分离.主从切换学习问题一:分表.分库的优缺点,以及分表无法成为主流分表 ...

  4. SpringBoot与动态多数据源切换

      本文简单的介绍一下基于SpringBoot框架动态多数据源切换的实现,采用主从配置的方式,配置master.slave两个数据库. 一.配置主从数据库 spring: datasource: ty ...

  5. springcloud vue.js 微服务分布式 前后分离 集成代码生成器 shiro权限 activiti工作流

    1.代码生成器: [正反双向](单表.主表.明细表.树形表,快速开发利器)freemaker模版技术 ,0个代码不用写,生成完整的一个模块,带页面.建表sql脚本.处理类.service等完整模块2. ...

  6. Spring AOP /代理模式/事务管理/读写分离/多数据源管理

    参考文章: http://www.cnblogs.com/MOBIN/p/5597215.html http://www.cnblogs.com/fenglie/articles/4097759.ht ...

  7. redis的主从复制,读写分离,主从切换

    当数据量变得庞大的时候,读写分离还是很有必要的.同时避免一个redis服务宕机,导致应用宕机的情况,我们启用sentinel(哨兵)服务,实现主从切换的功能. redis提供了一个master,多个s ...

  8. Redis哨兵模式(sentinel)学习总结及部署记录(主从复制、读写分离、主从切换)

    Redis的集群方案大致有三种:1)redis cluster集群方案:2)master/slave主从方案:3)哨兵模式来进行主从替换以及故障恢复. 一.sentinel哨兵模式介绍Sentinel ...

  9. mycat 1.6.6.1安装以及配置docker 安装mysql 5.7.24 双主多从读写分离主主切换

    mycat和mysql的高可用参考如下两个图 简介:应用程序仅需要连接HAproxy或者mycat,后端服务器的读写分离由mycat进行控制,后端服务器数据的同步由MySQL主从同步进行控制. 服务器 ...

随机推荐

  1. 更新EF(更新表 添加表…)

           

  2. mysql数据库编码、字段编码、表编码 专题

    CREATE DATABASE `mybatis-subject` /*!40100 DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin */ 其中的 ...

  3. Android Camera2拍照(一)——使用SurfaceView

    原文:Android Camera2拍照(一)--使用SurfaceView Camera2 API简介 Android 从5.0(21)开始,引入了新的Camera API Camera2,原来的a ...

  4. ELINK编程器能用来做什么

    以前   产品量产与测试的时候,在电脑上用JATG/SWD编程器或串口下载器等工具下载程序到产品中,效率低且操作复杂 现在  可以用ELINK脱机编程器来摆脱电脑并降低操作复杂度,只需把程序文件下载到 ...

  5. C#命名约定

    推荐的标识命名风格 风格名称 描述 使用建议 示例 Pascal大小写 标识符中每个单词都首字母大写 用于类型名和成员名 CarDeck, DealersHand Camel大小写 除第一个单词以外, ...

  6. 基于VUE实现的新闻后台管理系统-一

    基于VUE实现的新闻后台管理系统 前段时间拿到一个关于新闻后台的API,测试数据库使用SQLite,Restful服务是用Go写的,只要运行特定环境下的脚本(run.*)就会启动一个服务,依次后台为接 ...

  7. 一小部分机器学习算法小结: 优化算法、逻辑回归、支持向量机、决策树、集成算法、Word2Vec等

    优化算法 先导知识:泰勒公式 \[ f(x)=\sum_{n=0}^{\infty}\frac{f^{(n)}(x_0)}{n!}(x-x_0)^n \] 一阶泰勒展开: \[ f(x)\approx ...

  8. How to Move SSL certificate from Apache to Tomcat

    https://www.sslsupportdesk.com/how-to-move-ssl-certificate-from-apache-to-tomcat/ Apache uses x509 p ...

  9. 在不开启事件循环的线程中使用QTimer(QThread::run函数自带事件循环,在构造函数里创建线程,是一种很有意思的线程用法) good

    引入 QTimer是Qt自带的定时器类,QTimer运行时是依赖于事件循环的,简单来说,在一个不开启事件循环(未调用exec() )的线程中,QTimer是无法使用的.通过分析Qt源码可发现,调用QT ...

  10. QThread多线程编程经典案例分析(三种方法,解释了为什么使用moveToThread的根本原因,即为了避免调用QThread::exec() )

    传统的图形界面应用程序都只有一个线程执行,并且一次执行一个操作.如果用户调用一个比较耗时的操作,就会冻结界面响应. 一个解决方法是按照事件处理的思路: 调用 Void QApplication::pr ...