基于springboot的mysql实现读写分离

news2024/12/23 8:44:44

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

主从同步的局限性:这里分为主数据库和从数据库,主数据库和从数据库保持数据库结构的一致,主库负责写,当写入数据的时候,会自动同步数据到从数据库;从数据库负责读,当读请求来的时候,直接从读库读取数据,主数据库会自动进行数据复制到从数据库中。不过本篇博客不介绍这部分配置的知识,因为它更偏运维工作一点。这里涉及到一个问题:主从复制的延迟问题,当写入到主数据库的过程中,突然来了一个读请求,而此时数据还没有完全同步,就会出现读请求的数据读不到或者读出的数据比原始值少的情况。具体的解决方法最简单的就是将读请求暂时指向主库,但是同时也失去了主从分离的部分意义。也就是说在严格意义上的数据一致性场景中,读写分离并非是完全适合的,注意更新的时效性是读写分离使用的缺点。

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

该项目需要引入如下依赖:springBoot、spring-aop、spring-jdbc、aspectjweaver等

一: 主从数据源的配置

  我们需要配置主从数据库,主从数据库的配置一般都是写在配置文件里面。通过@ConfigurationProperties注解,可以将配置文件(一般命名为:application.Properties)里的属性映射到具体的类属性上,从而读取到写入的值注入到具体的代码配置中,按照习惯大于约定的原则,主库我们都是注为master,从库注为slave,本项目采用了阿里的druid数据库连接池,使用build建造者模式创建DataSource对象,DataSource就是代码层面抽象出来的数据源,接着需要配置sessionFactory、sqlTemplate、事务管理器等

/**
 * 主从配置
 *
 * @author wyq
 * @date 2020年07月24日01:24:42
 */
@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);
    }
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1538216.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

鸿蒙一次开发,多端部署(十一)交互归一

对于不同类型的智能设备&#xff0c;用户可能有不同的交互方式&#xff0c;如通过触摸屏、鼠标、触控板等。如果针对不同的交互方式单独做适配&#xff0c;会增加开发工作量同时产生大量重复代码。为解决这一问题&#xff0c;我们统一了各种交互方式的API&#xff0c;即实现了交…

Qt如何直接处理系统事件(比如鼠标事件),而不是post事件

#include <QtGui/5.15.2/QtGui/qpa/qwindowsysteminterface.h> // 方便调试事件 QWindowSystemInterface::setSynchronousWindowSystemEvents(true); 直接再 qWindowsWndProc函数中处理 通常情况: 事件被放到一个队列中

【vue3.0】实现导出的PDF文件内容是红头文件格式

效果图: 编写文件里面的主要内容 <main><div id"report-box"><p>线索描述</p><p class"label"><span>线索发现时间:</span> <span>{{ detailInfoVal?.problem.createdDate }}</span></p><…

springboot295基于Mysql的商业辅助决策系统的设计与实现

商业辅助决策系统的设计与实现 摘 要 如今社会上各行各业&#xff0c;都喜欢用自己行业的专属软件工作&#xff0c;互联网发展到这个时候&#xff0c;人们已经发现离不开了互联网。新技术的产生&#xff0c;往往能解决一些老技术的弊端问题。因为传统收支信息和销售订单信息管…

C#,图论与图算法,有向图(Direct Graph)广度优先遍历(BFS,Breadth First Search)算法与源程序

1 图的广度优先遍历 图的广度优先遍历(或搜索)类似于树的广度优先遍历(参见本文的方法2)。这里唯一需要注意的是,与树不同,图可能包含循环,因此我们可能再次来到同一个节点。为了避免多次处理节点,我们使用布尔访问数组。为简单起见,假设所有顶点都可以从起始顶点到达…

python的stone音乐播放器的设计与实现flask-django-php-nodejs

该系统利用python语言、MySQL数据库&#xff0c;flask框架&#xff0c;结合目前流行的 B/S架构&#xff0c;将stone音乐播放器的各个方面都集中到数据库中&#xff0c;以便于用户的需要。该系统在确保系统稳定的前提下&#xff0c;能够实现多功能模块的设计和应用。该系统由管理…

python的OA公文发文管理系统flask-django-php-nodejs

采用结构化的分析设计&#xff0c;该方法要求结合一定的图表&#xff0c;在模块化的基础上进行系统的开发工作。在设计中采用“自下而上”的思想&#xff0c;在OA公文发文管理系统实现了用户、公文分类、公文信息、待办提醒等的功能性。系统根据现有的管理模块进行开发和扩展&a…

hyperf 二十八 修改器 一

教程&#xff1a;Hyperf 一 修改器和访问器 根据教程&#xff0c;可设置相关函数,如set属性名Attribute()、get属性名Attribute()&#xff0c;设置和获取属性。这在thinkphp中也常见。 修改器&#xff1a;set属性名Attribute()&#xff1b;访问器&#xff1a;get属性名Attri…

StarRocks-2.5.13部署安装

1、安装jdk11 tar xf jdk-11.0.16.1_linux-x64_bin.tar.gz mv jdk-11.0.16.1 /data/soft/jdk-11 # 配置在/etc/profile中 export JAVA_HOME/data/soft/jdk-11 export CLASSPATH.:/data/soft/jdk-11/lib export PATH/data/soft/jdk-11/bin:$PATH # 验证jdk [rootdb-public-03 s…

基于ArcGIS的2015-2020辽宁省土地利用变化分析

数据准备 栅格转面 运行ArcToolbox&#xff0c;打开【转换工具】&#xff0c;选择【从栅格转出】里面的【栅格转面工具】&#xff0c;调出面板进行参数设置。输入栅格选择裁剪的2015年中国土地利用遥感监测数据&#xff08;…

Jupyter R绘图 汉字显示乱码的解决办法

1.Jupyte中&#xff0c;R绘图&#xff0c;汉字显示乱码 2.如何解决&#xff1f; (1)R中安装showtext 登录linux服务器 #R > install.packages(“showtext”) … 出错 (2)退出R,安装freetype-config #apt install libfreetype6-dev 出错 &#xff08;3&#xff09;进入R&…

嵌入式Linux 内核启动过程详解(第一阶段:汇编语言部分)

目录 概述 1 简述PC Linux 启动过程 2 嵌入式Linux内核启动流程 2.1 嵌入式Linux内核启动分析 2.2 启动Linux内核前的准备 2.3 启动内核第一阶段流程图 3 嵌入式Linux内核启动分析 3.1 链接文件 vmlinux.lds 3.2 启动文件head.S 3.2 启动文件head-common.S 概述 本文…

2024蓝桥杯每日一题(哈希)

备战2024年蓝桥杯 -- 每日一题 Python大学A组 试题一&#xff1a;星空之夜 试题二&#xff1a;模拟散列表 试题三&#xff1a;字符串哈希 试题四&#xff1a;四数方和 试题五&#xff1a;扫雷 试题一&#xff1a;星空之夜 【题目描述】 夜空深…

【JavaScript 漫游】【041】File 对象、FileList 对象、FileReader 对象

文章简介 本篇文章为【JavaScript 漫游】专栏的第 041 篇文章&#xff0c;主要对浏览器模型中 File 对象、FileList 对象和 FileReader 对象的知识点进行了简记。 File 对象 File 对象代表一个文件&#xff0c;用来读写文件信息。它继承了 Blob 对象&#xff0c;或者说是一种…

Springboot+vue的四川美食分享网站+数据库+报告+免费远程调试

项目介绍: Springbootvue的四川美食分享网站。Javaee项目&#xff0c;springboot vue前后端分离项目 本文设计了一个基于Springbootvue的前后端分离的四川美食分享网站&#xff0c;采用M&#xff08;model&#xff09;V&#xff08;view&#xff09;C&#xff08;controller&am…

计算机视觉之三维重建(3)---单视几何

文章目录 一、问题提出二、无穷远点、无穷远线、无穷远平面2.1 2D空间2.2 3D空间 三、影消点和影消线3.1 2D平面上的无穷远点&#xff0c;无穷远线变换3.2 影消点3.3 影消线 四、单视重构 一、问题提出 1. 当摄像机标定后&#xff0c;内部参数 K K K 已知&#xff0c;外部参数…

界面控件DevExpress ASP.NET Ribbon组件 - 完美复刻Office 365体验!

无论用户是喜欢传统工具栏菜单外观、样式&#xff0c;还是想在下一个项目中复制Office 365 web UI&#xff0c;DevExpress ASP.NET都提供了所需要的工具&#xff0c;帮助用户打造更好的应用程序界面。 P.S&#xff1a;DevExpress ASP.NET Web Forms Controls拥有针对Web表单&a…

JavaWeb -- HTTP -- WEB服务器TOMCAT

一.HTTP介绍: HTTP(Hyper Text Protocol) 实际上是一种超文本传输的协议,规定了浏览器跟服务器之间的一些数据传输的规则 例如B/S 对于浏览器的请求,以及相应服务器的响应,都必须依靠这种协议,规范,才能够彼此之间相互 理解 HTTP的协议特点: 1.基于TCP协议: 面向连接 更加安全…

Vscode初建Vue时几个需要注意的问题

首先放图 注意点1.打开文件夹时&#xff0c;可以是VUE2 或者其他&#xff0c;但不能是VUE&#xff0c;会报错 注意点2.终端输入命令“npm init -y" npm init -y -y 的含义&#xff1a;yes的意思&#xff0c;在init的时候省去了敲回车的步骤&#xff0c;生成的默认的packag…

【目标检测】YOLOv9理论解读与代码分析

前言 YOLO这个系列的故事已经很完备了&#xff0c;比如一些Decoupled-Head或者Anchor-Free等大的策略改动已经在YOLOv8固定下来&#xff0c;后面已经估计只有拿一些即插即用的tricks进行小改。 mmdetection框架的作者深度眸也在知乎上对“是否会有YOLOv9”这一观点发表看法&a…