基于 SpringBoot 实现 MySQL 读写分离的问题

-     前言     -

首先思考一个问题: 在高并发的场景中,关于数据库都有哪些优化的手段? 常用的实现方法有以下几种:读写分离、加缓存、主从架构集群、分库分表等,在互联网应用中,大部分都是读多写少的场景,设置两个库,主库和读库。

主库的职能是负责写,从库主要是负责读 , 可以建立读库集群 , 通过读写职能在数据源上的隔离达到减少读写冲突、 释压数据库负载、保护数据库的目的。在实际的使用中,凡是涉及到写的部分直接切换到主库,读的部分直接切换到读库,这就是典型的读写分离技术。本文将聚焦读写分离,探讨如何实现它。

主从同步的局限性: 这里分为主数据库和从数据库,主数据库和从数据库保持数据库结构的一致 , 主库负责写 , 当写入数据的时候 , 会自动同步数据到从数据库;从数据库负责读 , 当读请求来的时候 , 直接从读库读取数据 , 主数据库会自动进行数据复制到从数据库中。不过本篇博客不介绍这部分配置的知识 , 因为它更偏运维工作一点。

这里涉及到一个问题:主从复制的延迟问题。 当写入到主数据库的过程中 , 突然来了一个读请求 , 而此时数据还没有完全同步 , 就会出现读请求的数据读不到或者读出的数据比原始值少的情况。具体的解决方法最简单的就是将读请求暂时指向主库 , 但是同时也失去了主从分离的部分意义。也就是说在严格意义上的数据一致性场景中 , 读写分离并非是完全适合的 , 注意更新的时效性是读写分离使用的缺点。

好了 , 这部分只是了解 , 接下来我们看下具体如何通过 java 代码来实现读写分离:

该项目需要引入如下依赖:SpringBoot、Spring-aop、Spring-jdbc、aspectjweaver 等。

-     主从数据源的配置     -

我们需要配置主从数据库 , 主从数据库的配置一般都是写在配置文件里面。通过@ConfigurationProperties 注解 , 可以将配置文件(一般命名为:application.Properties)里的属性映射到具体的类属性上 , 从而读取到写入的值注入到具体的代码配置中 , 按照习惯大于约定的原则 , 主库我们都是注为 master , 从库注为 slave。

本项目采用了阿里的 druid 数据库连接池 , 使用 build 建造者模式创建 DataSource 对象 , DataSource 就是代码层面抽象出来的数据源 , 接着需要配置 sessionFactory、sqlTemplate、事务管理器等:

/**
 * 主从配置
 *
 * @author wyq
 */
@Configuration
@MapperScan(basePackages = "com.wyq.mysqlreadwriteseparate.mapper", sqlSessionTemplateRef = "sqlTemplate")
public class DataSourceConfig {

 /**
  * 主库
  */
 @Bean
 @ConfigurationProperties(prefix = "spring.datasource.master")
 public DataSource master() {
  return DruidDataSourceBuilder.create().build();
 }

 /**
  * 从库
  */
 @Bean
 @ConfigurationProperties(prefix = "spring.datasource.slave")
 public DataSource slaver() {
  return DruidDataSourceBuilder.create().build();
 }

 /**
  * 实例化数据源路由
  */
 @Bean
 public DataSourceRouter dynamicDB(@Qualifier("master") DataSource masterDataSource,
          @Autowired(required = false) @Qualifier("slaver") DataSource slaveDataSource) {
  DataSourceRouter dynamicDataSource = new DataSourceRouter();
  Map<Object, Object> targetDataSources = new HashMap<>();
  targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(), masterDataSource);
  if (slaveDataSource != null) {
   targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(), slaveDataSource);
  }
  dynamicDataSource.setTargetDataSources(targetDataSources);
  dynamicDataSource.setDefaultTargetDataSource(masterDataSource);
  return dynamicDataSource;
 }

 /**
  * 配置sessionFactory
  * @param dynamicDataSource
  * @return
  * @throws Exception
  */
 @Bean
 public SqlSessionFactory sessionFactory(@Qualifier("dynamicDB") DataSource dynamicDataSource) throws Exception {
  SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
  bean.setMapperLocations(
    new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*Mapper.xml"));
  bean.setDataSource(dynamicDataSource);
  return bean.getObject();
 }

 /**
  * 创建sqlTemplate
  * @param sqlSessionFactory
  * @return
  */
 @Bean
 public SqlSessionTemplate sqlTemplate(@Qualifier("sessionFactory") SqlSessionFactory sqlSessionFactory) {
  return new SqlSessionTemplate(sqlSessionFactory);
 }

 /**
  * 事务配置
  *
  * @param dynamicDataSource
  * @return
  */
 @Bean(name = "dataSourceTx")
 public DataSourceTransactionManager dataSourceTransactionManager(@Qualifier("dynamicDB") DataSource dynamicDataSource) {
  DataSourceTransactionManager dataSourceTransactionManager = new DataSourceTransactionManager();
  dataSourceTransactionManager.setDataSource(dynamicDataSource);
  return dataSourceTransactionManager;
 }
}

-     数据源路由的配置     -

路由在主从分离是非常重要的 , 基本是读写切换的核心。Spring 提供了 AbstractRoutingDataSource 根据用户定义的规则选择当前的数据源 , 作用就是在执行查询之前 , 设置使用的数据源 , 实现动态路由的数据源,在每次数据库查询操作前执行它的抽象方法 determineCurrentLookupKey() 决定使用哪个数据源。

为了能有一个全局的数据源管理器,此时我们需要引入 DataSourceContextHolder 这个数据库上下文管理器,可以理解为全局的变量 , 随时可取(见下面详细介绍) , 它的主要作用就是保存当前的数据源:

public class DataSourceRouter extends AbstractRoutingDataSource {

 /**
  * 最终的determineCurrentLookupKey返回的是从DataSourceContextHolder中拿到的,因此在动态切换数据源的时候注解
  * 应该给DataSourceContextHolder设值
  *
  * @return
  */
 @Override
 protected Object determineCurrentLookupKey() {
  return DataSourceContextHolder.get();

 }
}

-     数据源上下文环境     -

数据源上下文保存器 , 便于程序中可以随时取到当前的数据源 , 它主要利用 ThreadLocal 封装 , 因为 ThreadLocal 是线程隔离的 , 天然具有线程安全的优势。这里暴露了 set 和 get、clear 方法 , set 方法用于赋值当前的数据源名 , get 方法用于获取当前的数据源名称 , clear 方法用于清除 ThreadLocal 中的内容 , 因为 ThreadLocal 的 key 是 weakReference 是有内存泄漏风险的 , 通过 remove 方法防止内存泄漏:

/**
 * 利用ThreadLocal封装的保存数据源上线的上下文context
 */
public class DataSourceContextHolder {

 private static final ThreadLocal<String> context = new ThreadLocal<>();

 /**
  * 赋值
  *
  * @param datasourceType
  */
 public static void set(String datasourceType) {
  context.set(datasourceType);
 }

 /**
  * 获取值
  * @return
  */
 public static String get() {
  return context.get();
 }

 public static void clear() {
  context.remove();
 }
}

-     切换注解和 Aop 配置     -

首先我们来定义一个@DataSourceSwitcher 注解 , 拥有两个属性 ① 当前的数据源 ② 是否清除当前的数据源,并且只能放在方法上 , (不可以放在类上 , 也没必要放在类上 , 因为我们在进行数据源切换的时候肯定是方法操作) , 该注解的主要作用就是进行数据源的切换 , 在 dao 层进行操作数据库的时候 , 可以在方法上注明表示的是当前使用哪个数据源:

@DataSourceSwitcher 注解的定义:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented
public @interface DataSourceSwitcher {
 /**
  * 默认数据源
  * @return
  */
 DataSourceEnum value() default DataSourceEnum.MASTER;
 /**
  * 清除
  * @return
  */
 boolean clear() default true;

}

DataSourceAop 配置:

为了赋予@DataSourceSwitcher 注解能够切换数据源的能力,我们需要使用 AOP,然后使用@Aroud 注解找到方法上有@DataSourceSwitcher.class 的方法,然后取注解上配置的数据源的值,设置到 DataSourceContextHolder 中,就实现了将当前方法上配置的数据源注入到全局作用域当中:

@Slf4j
@Aspect
@Order(value = 1)
@Component
public class DataSourceContextAop {

 @Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")
 public Object setDynamicDataSource(ProceedingJoinPoint pjp) throws Throwable {
  boolean clear = false;
  try {
   Method method = this.getMethod(pjp);
   DataSourceSwitcher dataSourceSwitcher = method.getAnnotation(DataSourceSwitcher.class);
   clear = dataSourceSwitcher.clear();
   DataSourceContextHolder.set(dataSourceSwitcher.value().getDataSourceName());
   log.info("数据源切换至:{}", dataSourceSwitcher.value().getDataSourceName());
   return pjp.proceed();
  } finally {
   if (clear) {
    DataSourceContextHolder.clear();
   }

  }
 }

 private Method getMethod(JoinPoint pjp) {
  MethodSignature signature = (MethodSignature) pjp.getSignature();
  return signature.getMethod();
 }

}

-     用法以及测试     -

在配置好了读写分离之后,就可以在代码中使用了 , 一般而言我们使用在 service 层或者 dao 层,在需要查询的方法上添加@DataSourceSwitcher(DataSourceEnum.SLAVE) , 它表示该方法下所有的操作都走的是读库;在需要 update 或者 insert 的时候使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来将会走写库。

其实还有一种更为自动的写法 , 可以根据方法的前缀来配置 AOP 自动切换数据源,比如 update、insert、fresh 等前缀的方法名一律自动设置为写库 , Select、get、query 等前缀的方法名一律配置为读库,这是一种更为自动的配置写法。缺点就是方法名需要按照 aop 配置的严格来定义 , 否则就会失效:

@Service
public class OrderService {

 @Resource
 private OrderMapper orderMapper;

 /**
  * 读操作
  *
  * @param orderId
  * @return
  */
 @DataSourceSwitcher(DataSourceEnum.SLAVE)
 public List<Order> getOrder(String orderId) {
  return orderMapper.listOrders(orderId);

 }

 /**
  * 写操作
  *
  * @param orderId
  * @return
  */
 @DataSourceSwitcher(DataSourceEnum.MASTER)
 public List<Order> insertOrder(Long orderId) {
  Order order = new Order();
  order.setOrderId(orderId);
  return orderMapper.saveOrder(order);
 }
}

-     总结     -

上面是基本流程简图,本文介绍了如何实现数据库读写分离 , 注意读写分离的核心点就是数据路由 , 需要继承 AbstractRoutingDataSource , 复写它的 determineCurrentLookupKey 方法 , 同时需要注意 全局的上下文管理器 DataSourceContextHolder , 它是保存数据源上下文的主要类 , 也是路由方法中寻找的数据源取值 , 相当于数据源的中转站。 再结合 jdbc-Template 的底层去创建和管理数据源、事务等,我们的数据库读写分离就完美实现了。

到此这篇关于基于 SpringBoot 实现 MySQL 读写分离的问题的文章就介绍到这了,更多相关SpringBoot 实现 MySQL 读写分离内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Springboot + Mysql8实现读写分离功能

    在实际的生产环境中,为了确保数据库的稳定性,我们一般会给数据库配置双机热备机制,这样在master数据库崩溃后,slave数据库可以立即切换成主数据库,通过主从复制的方式将数据从主库同步至从库,在业务代码中编写代码实现读写分离(让主数据库处理 事务性增.改.删操作,而从数据库处理查询操作)来提升数据库的并发负载能力. 下面我们使用最新版本的Mysql数据库(8.0.16)结合SpringBoot实现这一完整步骤(一主一从). 安装配置mysql 从 https://dev.mysql.com/d

  • springboot基于Mybatis mysql实现读写分离

    近日工作任务较轻,有空学习学习技术,遂来研究如果实现读写分离.这里用博客记录下过程,一方面可备日后查看,同时也能分享给大家(网上的资料真的大都是抄来抄去,,还不带格式的,看的真心难受). 完整代码:https://github.com/FleyX/demo-project/tree/master/dxfl 1.背景 一个项目中数据库最基础同时也是最主流的是单机数据库,读写都在一个库中.当用户逐渐增多,单机数据库无法满足性能要求时,就会进行读写分离改造(适用于读多写少),写操作一个库,读操作多个库

  • SpringBoot自定义注解使用读写分离Mysql数据库的实例教程

    需求场景 为了防止代码中有的SQL慢查询,影响我们线上主数据库的性能.我们需要将sql查询操作切换到从库中进行.为了使用方便,将自定义注解的形式使用. mysql导入的依赖 <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.16</version> </dependency&

  • 基于 SpringBoot 实现 MySQL 读写分离的问题

    -     前言     - 首先思考一个问题: 在高并发的场景中,关于数据库都有哪些优化的手段? 常用的实现方法有以下几种:读写分离.加缓存.主从架构集群.分库分表等,在互联网应用中,大部分都是读多写少的场景,设置两个库,主库和读库. 主库的职能是负责写,从库主要是负责读 , 可以建立读库集群 , 通过读写职能在数据源上的隔离达到减少读写冲突. 释压数据库负载.保护数据库的目的.在实际的使用中,凡是涉及到写的部分直接切换到主库,读的部分直接切换到读库,这就是典型的读写分离技术.本文将聚焦读写分

  • SpringBoot+Mybatis-Plus实现mysql读写分离方案的示例代码

    1. 引入mybatis-plus相关包,pom.xml文件 2. 配置文件application.property增加多库配置 mysql 数据源配置 spring.datasource.primary.jdbc-url=jdbc:mysql://xx.xx.xx.xx:3306/portal?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=

  • SpringBoot项目中如何实现MySQL读写分离详解

    目录 1.MySQL主从复制 1.1.介绍 二进制日志: MySQL复制过程分成三步: 1.2.主从库搭建 1.2.1.主库配置 1.2.2.从库配置 1.3.坑位介绍 1.3.1.UUID报错 1.3.2.server_id报错 1.3.3.同步异常解决 操作不规范,亲人两行泪…… 2.项目中实现 2.1.ShardingJDBC 2.2.依赖导入 2.3.配置文件 2.4.测试跑路 总结 1.MySQL主从复制 但我们仔细观察我们会发现,当我们的项目都是用的单体数据库时,那么就可能会存在如下

  • SpringBoot+ShardingSphereJDBC实现读写分离详情

    目录 1 概述 2 环境 3 一些基础理论 3.1 读写分离 3.2 主从复制 3.3 数据库中间件简介 4MySQL主从复制环境准备 4.1 主库操作 4.1.1 拉取镜像并创建容器运行 4.1.2 修改配置文件 4.1.3 准备数据源 4.1.4 创建一个复制操作的用户(可选但推荐) 4.1.5 数据备份(可选) 4.1.6 查看主库状态 4.2 从库操作 4.2.1 拉取镜像并创建容器运行 4.2.2 修改配置文件 4.2.3 查看ip地址 4.2.4 导入数据(可选) 4.2.5 准备数

  • Sharding-JDBC自动实现MySQL读写分离的示例代码

    目录 一.ShardingSphere和Sharding-JDBC概述 1.1.ShardingSphere简介 1.2.Sharding-JDBC简介 1.3.Sharding-JDBC作用 1.4.ShardingSphere规划线路图 1.5.ShardingSphere三种产品的区别 二.数据库中间件 2.1.数据库中间件简介 2.2.Sharding-JDBC和MyCat区别 三.Sharding-JDBC+MyBatisPlus实现读写分离 3.0.项目代码结构和建表SQL语句 3.

  • 使用PHP实现Mysql读写分离

    本代码是从uchome的代码修改的,是因为要解决uchome的效率而处理的.这个思维其实很久就有了,只是一直没有去做,相信也有人有同样的想法,如果有类似的,那真的希望提出相关的建议. 封装的方式比较简单,增加了只读数据库连接的接口扩展,不使用只读数据库也不影响原代码使用.有待以后不断完善..为了方便,试试建立了google的一个项目:http://code.google.com/p/mysql-rw-php/希望给有需要的朋友带来帮助. PHP实现的Mysql读写分离主要特性:1.简单的读写分离

  • MySQL 读写分离实例详解

    MySQL 读写分离 MySQL读写分离又一好办法 使用 com.mysql.jdbc.ReplicationDriver 在用过Amoeba 和 Cobar,还有dbware 等读写分离组件后,今天我的一个好朋友跟我讲,MySQL自身的也是可以读写分离的,因为他们提供了一个新的驱动,叫 com.mysql.jdbc.ReplicationDriver 说明文档:http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-replic

  • PHP实现的mysql读写分离操作示例

    本文实例讲述了PHP实现的mysql读写分离操作.分享给大家供大家参考,具体如下: 首先mysql主从需配置好,基本原理就是判断sql语句是否是select,是的话走master库,否则从slave查 <?php /** * mysql读写分离 */ class db{ public function __construct($sql){ $chestr = strtolower(trim($sql)); //判断sql语句有select关键字的话,就连接读的数据库,否则就连接写数据库 if(s

  • Mysql读写分离过期常用解决方案

    mysql读写分离的坑 读写分离的主要目标是分摊主库的压力,由客户端选择后端数据库进行查询.还有种架构就是在MYSQL和客户端之间有一个中间代理层proxy,客户端之连接proxy,由proxy根据请求类型和上下文决定请求的分发路由. 客户端直连方案:因为少了一层proxy转发,所以查询性能稍微好一点儿,并且整体架构简单,排查问题更方便.但是这种方案,由于要了解后端部署细节,所以在出现主备切换.库迁移等操作的时候,客户端都会感知到,并且需要调整数据库连接信息. 带proxy架构:对客户端比较友好

随机推荐