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

news2024/9/24 0:10:28

最近项目中需要做【高可用】数据库读写分离相关的需求,特地整理了下关于读写分离的相关知识。项目中采用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: 8080

spring:
  datasource:
    druid:
      ds1:
        url: jdbc:mysql://localhost:3306/db_ds1?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: root1234
        driver-class-name: com.mysql.jdbc.Driver
      ds2:
        url: jdbc:mysql://localhost:3306/db_ds2?serverTimeZone=UTC&useUnicode=true&characterEncoding=UTF-8&useSSL=false
        username: root
        password: root1234
        driver-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();
    }

    @Override
    protected 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中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    @Bean
    @ConfigurationProperties("spring.datasource.druid.ds2")
    public DataSource dataSource2() {
        // 底层会自动拿到spring.datasource中的配置, 创建一个DruidDataSource
        return DruidDataSourceBuilder.create().build();
    }

    /**
     * 设设置动态数据源(设置目标数据源)
     *
     * @param dataSource1 数据源1
     * @param dataSource2 数据源2
     * @return 动态数据源
     */
    @Bean
    public 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
     */
    @Bean
    public SqlSessionFactory sqlSessionFactory(@Qualifier("dynamicDataSource") DataSource dataSource) throws Exception {
        // 使用我们的动态数据源来构建SqlSessionFactory
        SqlSessionFactoryBean 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();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }

    /**
     * 注意:虽然Spring会自动管理事务,但是你还需要确保你的DataSourceTransactionManager已经正确配置了你的AbstractRoutingDataSource。
     * 如果没有正确配置,你可能会遇到无法找到合适的事务管理器的错误。
     *
     * @param routingDataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager transactionManager(AbstractRoutingDataSource routingDataSource) {
        return new DataSourceTransactionManager(routingDataSource);
    }
}

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

/**
 * 学生实现类
 */
@Service
public class StudentServiceImpl implements IStudentService {

    @Autowired
    StudentMapper studentMapper;

    @DS(DsConstant.DS2)
    @Override
    public List<Student> findAll() {
        return studentMapper.selectAll();
    }

    @DS(DsConstant.DS2)
    @Override
    public 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 {

    @Autowired
    UserMapper userMapper;

    @DS(DsConstant.DS1)
    @Override
    public List<User> findAll() {
        return userMapper.selectAll();
    }

    @DS(DsConstant.DS1)
    @Override
    public 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.coloradmin.cn/o/1946083.html

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

相关文章

Mysql —— 事务

目录 什么是事务&#xff1f; 两种方式实现事务&#xff1a; 方法一 方法二&#xff1a; 事务四大特性(简称ACID) 并发事务问题&#xff08;面试题&#xff09; 事务隔离级别 什么是事务&#xff1f; 事务是一组操作的集合&#xff0c;它是一个不可分割的工作单位&#xff…

跨域浏览器解决前端跨域问题

1.问题背景 这是一种属于非主流的解决跨域的方案&#xff0c;但是也是可以正常使用而且比较简单的。如果需要使用主流的解决前端跨域方案&#xff0c;请参考这篇文章。 我这边其实是优先建议大家使用主流的跨域方案&#xff0c;如果主流的实在不行&#xff0c;那么就使用跨域…

【2024最新华为OD-C/D卷试题汇总】[支持在线评测] 图像物体的边界(200分) - 三语言AC题解(Python/Java/Cpp)

🍭 大家好这里是清隆学长 ,一枚热爱算法的程序员 ✨ 本系列打算持续跟新华为OD-C/D卷的三语言AC题解 💻 ACM银牌🥈| 多次AK大厂笔试 | 编程一对一辅导 👏 感谢大家的订阅➕ 和 喜欢💗 🍿 最新华为OD机试D卷目录,全、新、准,题目覆盖率达 95% 以上,支持题目在线…

scratch绘制十个圆 2024年6月中国电子学会图形化编程 少儿编程 scratch编程等级考试三级真题和答案解析

目录 scratch绘制十个圆 一、题目要求 1、准备工作 2、功能实现 二、案例分析 1、角色分析 2、背景分析 3、前期准备 三、实现流程 1、案例分析 2、详细过程 四、程序编写 五、考点分析 六、推荐资料 1、入门基础 2、蓝桥杯比赛 3、考级资料 4、视频课程 5、…

css气泡背景特效

css气泡背景特效https://www.bootstrapmb.com/item/14879 要创建一个CSS气泡背景特效&#xff0c;你可以使用CSS的伪元素&#xff08;:before 和 :after&#xff09;、border-radius 属性来创建圆形或椭圆形的“气泡”&#xff0c;以及background 和 animation 属性来设置背景…

mysql 先子查询过滤再联合 和 先联合再过滤 执行效率问题

执行顺序 先子查询过滤再联合 SELECT XXX FROM(select * from edw_data_dyd.overrun_offsite_info WHERELENGTH( VEHICLE_ID ) > 12 AND CREATED_TIME > DATE_ADD(NOW(),INTERVAL -1 HOUR)AND CREATED_TIME < NOW()AND VEHICLE_ID not like %无车牌%AND VEHICLE_I…

华为ICT大赛之ensp软件BGP原理与配置

BGP基础 1.用于不同自治系统AS(autonomous system)之间动态交换路由信息&#xff1b; BGP取代EGP(exterior gateway protocol)外部网关协议&#xff0c;BGP在其发布路由信息基础上可以进行路由优选&#xff0c;高效处理路由信息&#xff1b; AS:同一组织管理下&#xff0c;使…

全面解析 SnowNLP:中文文本处理、情感分析

1 前言 SnowNLP 是一个专门用于处理中文文本的 Python库。功能包括&#xff1a; 分词情感分析关键词提取文本分类拼音转换繁体转简体词相似度计算等 snownlp0.12.3测试环境&#xff1a;Python3.10.9 2 分词 中文分词&#xff08;Character-Based Generative Model&#xf…

WEB渗透Web突破篇-前端突破

逻辑漏洞 1.任意用户注册 2.用户名枚举 3.爆破用户名&#xff0c;密码 4.用户名注入 5.万能密码 6.用户名Xss 7.修改返回包信息&#xff0c;登入他人账户 8.修改cookie中的参数&#xff0c;如user,adminid等 9.HTML源码、JS等查看信息搜集那一章 10.后台登录参数修改…

MSPM0G3507学习笔记1:开发环境_引脚认识与点灯

今日速通一款Ti的单片机用于电赛&#xff1a;MSPM0G3507 这里默认已经安装好了Keil5_MDK 首先声明一下: 因为是速成&#xff0c;所以需要一定单片机学习基础&#xff0c;然后我写的也不会详细&#xff0c;这个专栏的笔记也就是自己能看懂就行的目标~~~ 文章提供测试代码解…

Go: IM系统分布式架构方案 (6)

分布式部署可能遇到的问题 常见 nginx 反向代理方案 假设按照上述架构方案来A用户接入后connA(ws客户端) 由节点1来维护B用户接入后connA(ws客户端) 由节点2来维护流程: A->B 发信息: A -> connA -> 分析处理 -> connB -> B实际上&#xff0c;上述流程是没有办法…

C++学习笔记03-对象和类(问题-解答自查版)

前言 以下问题以Q&A形式记录&#xff0c;基本上都是笔者在初学一轮后&#xff0c;掌握不牢或者频繁忘记的点 Q&A的形式有助于学习过程中时刻关注自己的输入与输出关系&#xff0c;也适合做查漏补缺和复盘。 本文对读者可以用作自查&#xff0c;答案在后面&#xff0…

1 深度学习网络DNN

代码来自B站up爆肝杰哥 测试版本 import torch import torchvisiondef print_hi(name):print(fHi, {name}) if __name__ __main__:print_hi(陀思妥耶夫斯基)print("HELLO pytorch {}".format(torch.__version__))print("torchvision.version:", torchvi…

C++(week13): C++基础: 标准模板库 STL

文章目录 零、标准模板库 STL一、容器 (Container)1.序列式容器(1)vector2.五种遍历10.vector的迭代器失效问题 (2)deque(3)list 2.关联式容器(1)set4.set的查找(2)find() 8.set中存储自定义类型&#xff1a;三种方法 (2)multiset7.multiset的特殊操作&#xff1a;bound系列函数…

LeetCode/NowCoder-二叉树OJ练习

励志冰檗&#xff1a;形容在清苦的生活环境中激励自己的意志。&#x1f493;&#x1f493;&#x1f493; 目录 说在前面 题目一&#xff1a;单值二叉树 题目二&#xff1a;相同的树 题目三&#xff1a;对称二叉树 题目四&#xff1a;二叉树的前序遍历 题目五&#xff1a;另…

Python | Leetcode Python题解之第275题H指数II

题目&#xff1a; 题解&#xff1a; class Solution:def hIndex(self, citations: List[int]) -> int:n len(citations)left 0; right n - 1while left < right:mid left (right - left) // 2if citations[mid] > n - mid:right mid - 1else:left mid 1retur…

C语言之2048小游戏理解分析

目录 游戏程序思维导图&#xff1a; ​编辑 功能介绍&#xff1a; 代码管理&#xff1a; 主函数&#xff1a; 头文件&#xff1a; 游戏程序思维导图&#xff1a; 功能介绍&#xff1a; 按键W --------------- 向上 按键A --------------- 向左 按键S --------------- 向…

科技云报道:算网筑基AI注智,中国联通如何讲出AI时代的“新故事”?

科技云报道原创。 AI从未停止进化&#xff0c;也从未停止给人类带来惊喜。 从ChatGPT代表的文生文、Dall-E代表的文生图&#xff0c;到Sora代表的文生视频&#xff0c;Suno为代表的文生音乐&#xff0c;生成式AI的“暴力美学”持续突破内容生产的天花板&#xff0c;大模型技术…

【黑马java基础】特殊文件,日志

目录 特殊文件&#xff1a;Properties属性文件特点、作用使用Properties读取属性文件里的键值对数据使用properties把键值对数据写到属性文件中去案例 特殊文件&#xff1a;XML文件概述读取XML文件中的数据把数据写出到XML文件中去补充知识&#xff1a;约束XML文件的编写[了解]…

打卡第21天------二叉树

我现在每天都是在与时间赛跑&#xff0c;分秒必争&#xff0c;不想浪费一点我自己的时间。 希望通过算法训练营可以把我自己的逻辑思维建立起来&#xff0c;把自己的算法能力给提上去。 一、修剪二叉搜索树 题目链接&#xff1a;669. 修剪二叉搜索树 题目描述&#xff1a; 给…