【高可用】利用AOP实现数据库读写分离

最近项目中需要做【高可用】数据库读写分离相关的需求,特地整理了下关于读写分离的相关知识。项目中采用4台数据库:1个master,2个slave,1个readOnly,其中master数据库会自动定时同步到readOnly节点。可以通过中间件(ShardingSphere、mycat、mysql-proxy 、TDDL …), 但是我们公司没有专门的中间件团队搭建读写分离基础设施,因此需要开发人员自行实现读写分离(有点离谱~)。

一、实现原理

Spring框架中,Spring-JDBC模块提供了AbstractRoutingDataSource,其内部可以包含了多个DataSource,通过继承该类并覆盖determineCurrentLookupKey方法,可以根据业务需求动态选择数据源。

二、具体实现

1、application.yml配置读和写数据源

server:port: 8080spring:datasource:druid:ds1:url: jdbc:mysql://localhost:3306/db_ds1?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=falseusername: rootpassword: root1234driver-class-name: com.mysql.jdbc.Driverds2:url: jdbc:mysql://localhost:3306/db_ds2?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=falseusername: rootpassword: root1234driver-class-name: com.mysql.jdbc.Driver

2、DynamicDataSource动态数据源,继承AbstractRoutingDataSource,实现determineCurrentLookupKey方法

/*** 动态数据源*/
public class DynamicDataSource extends AbstractRoutingDataSource {/*** ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量。* 也就是说 ThreadLocal 可以为每个线程创建一个【单独的变量副本】,相当于线程的 private static 类型变量。*/private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();/*** 决定使用哪个数据源之前需要把多个数据源的信息以及默认数据源信息配置好** @param targetDataSources       目标数据源* @param defaultTargetDataSource 默认数据*/public DynamicDataSource(Map<Object, Object> targetDataSources, DataSource defaultTargetDataSource) {super.setDefaultTargetDataSource(defaultTargetDataSource);super.setTargetDataSources(targetDataSources);super.afterPropertiesSet();}@Overrideprotected Object determineCurrentLookupKey() {return CONTEXT_HOLDER.get();}public static void setDataSource(String dataSource) {CONTEXT_HOLDER.set(dataSource);}public static void clearDataSource() {CONTEXT_HOLDER.remove();}
}

3、DynamicDataSourceAspect多数据源切面

/*** 多数据源切面*/
@Slf4j
@Component
@Aspect
public class DynamicDataSourceAspect {/*** @annotation:这个表达式的含义是匹配所有带有特定注解的方法。 例如,@annotation(com.xxx.MyAnnotation)将匹配所有带有@MyAnnotation注解的方法。* @within:这个表达式的含义是匹配所有在特定注解的类中的方法,不管这个方法本身有没有这个注解。 例如,@within(com.xxx.MyAnnotation)将匹配所有在带有@MyAnnotation注解的类中的方法。*/@Pointcut("@annotation(com.ds.datasource.DS) " +"|| @within(com.ds.datasource.DS)")public void dataSourcePointCut() {}@Around("dataSourcePointCut()")public Object around(ProceedingJoinPoint point) throws Throwable {MethodSignature signature = (MethodSignature) point.getSignature();Class targetClass = point.getTarget().getClass();Method method = signature.getMethod();DS targetDataSource = (DS) targetClass.getAnnotation(DS.class);DS methodDataSource = method.getAnnotation(DS.class);if (targetDataSource != null || methodDataSource != null) {String value;if (methodDataSource != null) {//优先用方法上的value = methodDataSource.value();} else {//类上的value = targetDataSource.value();}DynamicDataSource.setDataSource(value);log.debug("set datasource is {}", value);}try {return point.proceed();} finally {DynamicDataSource.clearDataSource();log.debug("clean datasource");}}
}

4、DataSourceConfig数据源配置

/*** 数据源配置*/
@Configuration
public class DataSourceConfig {@Bean@ConfigurationProperties("spring.datasource.druid.ds1")public DataSource dataSource1() {// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSourcereturn DruidDataSourceBuilder.create().build();}@Bean@ConfigurationProperties("spring.datasource.druid.ds2")public DataSource dataSource2() {// 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSourcereturn DruidDataSourceBuilder.create().build();}/*** 设设置动态数据源(设置目标数据源)** @param dataSource1 数据源1* @param dataSource2 数据源2* @return 动态数据源*/@Beanpublic DynamicDataSource dynamicDataSource(DataSource dataSource1, DataSource dataSource2) {Map<Object, Object> targetDataSources = new HashMap<>();targetDataSources.put(DsConstant.DS1, dataSource1);targetDataSources.put(DsConstant.DS2, dataSource2);return new DynamicDataSource(targetDataSources, dataSource1);}/*** 当你自定义SqlSessionFactory Bean时,你需要在自定义的SqlSessionFactory中明确地设置别名包和mapper文件的位置。* 否则,application.yml中的配置可能不会生效。* 当你使用MyBatis的Spring Boot Starter自动配置时,它会根据application.yml中的配置来创建SqlSessionFactory。但当你自定义SqlSessionFactory时,Spring Boot Starter的自动配置就不会生效了,因此,你需要在自定义的SqlSessionFactory中设置这些属性。* 所以,虽然你在application.yml中配置了别名包和mapper文件的位置,但是你还是需要在自定义的SqlSessionFactory中设置这些属性,以确保它们被正确地使用。** @param dataSource* @return* @throws Exception*/@Beanpublic SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {// 使用我们的动态数据源来构建SqlSessionFactorySqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();sessionFactory.setDataSource(dataSource);// 设置别名包sessionFactory.setTypeAliasesPackage("com.ds.entity");// 设置mapper文件的位置PathMatchingResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();sessionFactory.setMapperLocations(resolver.getResources("classpath:mapper/*.xml"));return sessionFactory.getObject();}@Beanpublic SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {return new SqlSessionTemplate(sqlSessionFactory);}/*** 注意:虽然Spring会自动管理事务,但是你还需要确保你的DataSourceTransactionManager已经正确配置了你的AbstractRoutingDataSource。* 如果没有正确配置,你可能会遇到无法找到合适的事务管理器的错误。** @param routingDataSource* @return*/@Beanpublic PlatformTransactionManager transactionManager(AbstractRoutingDataSource routingDataSource) {return new DataSourceTransactionManager(routingDataSource);}
}

5、StudentServiceImpl读取数据源1和UserServiceImpl读取数据源2

/*** 学生实现类*/
@Service
public class StudentServiceImpl implements IStudentService {@AutowiredStudentMapper studentMapper;@DS(DsConstant.DS2)@Overridepublic List<Student> findAll() {return studentMapper.selectAll();}@DS(DsConstant.DS2)@Overridepublic void insert() {Student student = new Student();student.setStudentCode("Code-" + RandomUtil.randomNumber());student.setStudentCode("学员-" + RandomUtil.randomNumber());studentMapper.insertStudent(student);// 模拟业务异常,验证事务回滚// int k = 1 / 0;}
}
/*** 用户实现类*/
@Service
public class UserServiceImpl implements IUserService {@AutowiredUserMapper userMapper;@DS(DsConstant.DS1)@Overridepublic List<User> findAll() {return userMapper.selectAll();}@DS(DsConstant.DS1)@Overridepublic void add() {User user = new User();user.setName("张三001-" + RandomUtil.randomInt());user.setAge(RandomUtil.randomInt());user.setSex("男");userMapper.insertUser2(user);}
}

三、测试接口,发现在自定义的业务逻辑上,能够区分数据源,实现读写分离。

在这里插入图片描述
在这里插入图片描述

四、项目结构

在这里插入图片描述
源码下载地址multi-datasource,欢迎Star!

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

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

相关文章

FastAPI(六十九)实战开发《在线课程学习系统》接口开发--修改密码

源码见&#xff1a;"fastapi_study_road-learning_system_online_courses: fastapi框架实战之--在线课程学习系统" 之前我们分享了FastAPI&#xff08;六十八&#xff09;实战开发《在线课程学习系统》接口开发--用户 个人信息接口开发。这次我们去分享实战开发《在线…

Redis集群的主从复制原理-全量复制和增量复制-哨兵机制

Redis集群的主从复制原理-全量复制和增量复制-哨兵机制 作用 数据备份 这一点直观,因为现在有很多节点,每个节点都保存了原始数据的备份. 读写分离 这一点主要是当发生读写的时候&#xff0c;读数据的操作大部分都会进入到从节点&#xff0c;而写数据的操作都会进入到主节点&…

ESP32CAM人工智能教学15

ESP32CAM人工智能教学15 Flask服务器TCP连接 小智利用Flask在计算机中创建一个虚拟的网页服务器服务器&#xff0c;让ESP32Cam通过WiFi连接&#xff0c;把摄像头拍摄到的图片发送到电脑中&#xff0c;并在电脑中保存成图片文件。 Flask是用Python编写的网页服务程序WebServer。…

react18高阶组件

高阶组件的本质上就是函数&#xff0c;接受一个组件作为参数&#xff0c;然后返回一个组件。解决了组件之间如何横向抽离公共逻辑的问题。类组件之间常使用&#xff0c;函数组件中也可以使用&#xff0c;但更多的时候使用自定义hooks。 高阶组件命名一般采用with开头&#xff…

逻辑回归推导

逻辑回归既可以看作是回归算法&#xff0c;也可以看做是分类算法。通常作为分类算法使用&#xff0c;只可以解决二分类问题。 在上述平面中&#xff0c;每个颜色代表一个类别&#xff0c;即有4个类别 将红色的做为一个类别&#xff0c;其他三个类别都统称为其他类别&#xff0…

现代化电商企业在行业竞争中关于数据采集API接口的应用分析||经验分享

及时准确&#xff1a;电商API接口能为品牌提供实时数据&#xff0c;这意味着企业可以即时获取最新的商品价格信息&#xff0c;避免因为信息延迟导致的决策失误。相较于手动采集&#xff0c;接口数据一般更为准确可靠。 效率提升&#xff1a;接口自动化采集大大提高了数据获取效…

ViewPager实现原理分析

ViewPager 是 Android 中用于展示多页面内容的控件&#xff0c;通常被用来实现滑动切换不同页面的功能&#xff0c;比如常见的应用启动引导页、广告轮播图或者多标签页的布局。ViewPager 是一个非常重要的控件&#xff0c;它提供了灵活的滑动效果和页面管理机制。 下面是基于 …

Photoshop(PS) 抠图简单教程

目录 快速选择 魔棒 钢笔 橡皮擦 蒙版 通道 小结 可以发现&#xff0c;ps逐渐成为必备基础的办公软件。本文让ps新手轻松学会抠图。 快速选择 在抠图之前&#xff0c;先了解下选区的概念。ps中大多数的抠图操作都是基于选区的&#xff0c;先选区再Ctrl J提取选区。而快…

【深度】2024AI大模型算力芯片产业深度分析

人工智能算力基础设施成为我国数字经济高质量发展的重要战略部署&#xff0c;具有重大发展意义。 1&#xff09;算力普适普惠化是大趋势&#xff0c;相关服务生态逐步构建。“东数西算”工程的实施&#xff0c;带动数据、算力跨域流动&#xff0c;实现产业跃升和区域平衡发展。…

谷粒商城实战笔记-46-商品服务-API-三级分类-配置网关路由与路径重写

文章目录 一&#xff0c;准备工作1&#xff0c;新增一级菜单2&#xff0c;新增二级菜单 二&#xff0c;前端树形界面开发1&#xff0c;开发分类展示组件 三&#xff0c;远程调用接口获取商品分类数据1&#xff0c;远程调用2&#xff0c;路由配置 错误记录 本节的主要内容&#…

【算法/训练】:动态规划

一、路径类 1. 字母收集 思路&#xff1a; 1、预处理 对输入的字符矩阵我们按照要求将其转换为数字分数&#xff0c;由于只能往下和往右走&#xff0c;因此走到&#xff08;i&#xff0c;j&#xff09;的位置要就是从&#xff08;i - 1&#xff0c; j&#xff09;往下走&#x…

MySQL 约束 (constraint)

文章目录 约束&#xff08;constraint)列级约束和表级约束给约束起名字&#xff08;constraint)非空约束&#xff08;no null)检查约束&#xff08;check)唯一性约束 (unique)主键约束 (primary key)主键分类单一主键复合主键主键自增 &#xff08;auto_increment) 外键约束外什…

C# 中用 TopShelf服务创建和延迟运行

TopShelf 是一个开源项目&#xff0c;它简化了在 .NET 环境中创建和管理 Windows 服务的流程。以下是一个简单的示例&#xff0c;展示了如何使用 TopShelf 创建一个服务&#xff0c;并在服务启动后延迟执行某些操作。 首先&#xff0c;确保你已经安装了 TopShelf。你可以通过 …

C++树形结构(总)

目录 一.基础&#xff1a; 1.概念&#xff1a; 2.定义&#xff1a; Ⅰ.树的相关基础术语&#xff1a; Ⅱ.树的层次&#xff1a; 3.树的性质&#xff1a; 二.存储思路&#xff1a; 1.结构体存储&#xff1a; 2.数组存储&#xff1a; 三.树的遍历模板&#xff1a; 四.…

学习笔记-系统框图简化求传递函数公式例题

简化系统结构图求系统传递函数例题 基础知识回顾 第四讲 控制系统的方框图 (zhihu.com) 「自控原理」2.3 方框图的绘制及化简_方框图化简-CSDN博客 自动控制原理笔记-结构图及其等效变换_结构图等效变换-CSDN博客 例子一 「自控原理」2.3 方框图的绘制及化简_方框图化简-CS…

7-20FPGA调试日志

1. 在代码里面定义的ILA的变量名称与波形抓取界面的不一致 问题描述 ::: 2. 直接从其他的播放声音的平台放音乐没问题&#xff0c;但是从AU里面生成的2kHz的正弦波放不出声音 演示视频链接 好像和ILA的例化信号有关&#xff0c;例化ILA信号的驱动时钟信号频率没有内部的其他…

MySQL字段设置的varchar长度小于数据长度自动截取丢弃超出的长度而不是报错?

MySQL字段设置的varchar长度小于数据长度自动截取丢弃超出的长度而不是报错&#xff1f; 事情是这样的&#xff0c;我们一个订单表存放了商品的快照信息其中快照信息存储的是json格式商品信息&#xff0c;当查看订单是报错了&#xff0c;发现我们后端服务查询到订单的快照信息…

dockerfile部署wordpress

1.将容器直接提交成镜像 [rootlocalhost ~]# docker commit 8ecc7f6b9c12 nginx:1.1 sha256:9a2bb94ba6d8d952527df616febf3fbc8f842b3b9e28b7011b50c743cd7b233b [rootlocalhost ~]# docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx …

C语言 数据存储之结构类型 万字讲解#数据类型详细介绍 #整型在内存中的存储 #大小端字节序 #浮点型在内存的存储解析

文章目录 目录 前言 一、数据类型详细介绍 类型的意义&#xff1a; 1、类型的基本归类 a.整型家族 b.浮点型家族 c.构造类型 数组类型 结构体类型 struct 枚举类型 enum 联合类型 union d.指针类型 e.空类型 二、整型在内存中的存储 三、大小端字节序 什么是…

MySql性能调优05-[sql实战演练]

sql实战演练 行列转换行列式转换第一题【列转行】第二题【列转行】 having的使用找到表中&#xff0c;名字重复的项有数据表employee&#xff0c;包含如下字段id、name、department、age&#xff0c;编写SQL&#xff0c;找到不与其他人同龄的年纪最大的员工的年龄有数据表emplo…