广元网站建设公司,沈阳关键词seo,青岛做网站费用,深圳建网站服务商#x1f4a7; S p r i n g A O P 主从数据源切换 读写分离 自定义注解案例实战#xff01; \color{#FF1493}{Spring AOP 主从数据源切换 读写分离 自定义注解 案例实战#xff01;} SpringAOP主从数据源切换读写分离自定义注解案例实战#xff01;#x1f4a7; … S p r i n g A O P 主从数据源切换 读写分离 自定义注解案例实战 \color{#FF1493}{Spring AOP 主从数据源切换 读写分离 自定义注解 案例实战} SpringAOP主从数据源切换读写分离自定义注解案例实战 仰望天空妳我亦是行人.✨ 个人主页——微风撞见云的博客 《数据结构与算法》专栏的文章图文并茂生动形象简单易学欢迎大家来踩踩~ 《Java学习笔记》专栏的文章是本人在Java学习中总结的一些知识点~ 《每天一点小知识》专栏的文章可以丰富你的知识库滴水成河~ 《Redis》专栏的文章是在学习Redis时整理的笔记与记录的思考~ 《RabbitMQ》专栏的文章是在学习尚硅谷课程时整理的笔记方便复习巩固~ 希望本文能够给读者带来一定的帮助~文章粗浅敬请批评指正 文章目录 Spring AOP 自定义注解 数据源 实现主从库切换读写分离 项目实战准备工作项目搭建以及相关依赖书写yml文件数据库准备 config目录各文件介绍定义Spring AOP的切面类 DataSourceAop配置数据源和动态数据源切换创建自定义注解定义数据库读写分离的工具类DBContextHolder定义枚举类DBTypeEnum配置Mybatis指定数据源:SqlSessionFactory和事务管理器自定义数据源路由类MyRoutingDataSourceconfig配置类总结 其他文件说明UserControllerUserEntityUserMapperUserService主启动类DemoApplication 功能演示总结 结语 Spring AOP 自定义注解 数据源 实现主从库切换读写分离 项目实战
在现代的应用程序开发中数据库读写分离是提高应用性能和可伸缩性的重要策略之一。Spring AOP 和自定义注解为我们提供了实现读写分离的有效工具而德鲁伊Druid数据源则为我们提供了高性能的连接池我们用它来实现动态数据源。本篇博客将带领你一步一步实现 Spring AOP 结合自定义注解和动态数据源实现主从数据库切换以及读写分离。 准备工作
项目搭建以及相关依赖
首先我们需要确保已经创建好了一个 Spring Boot 2.x.x 项目并添加了相关依赖。 dependenciesdependencygroupIdcom.alibaba/groupIdartifactIddruid/artifactIdversion1.2.8/version/dependency!--SpringBoot集成Aop起步依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-aop/artifactId/dependency!--SpringBoot集成WEB起步依赖--dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency!--mybatis集成SpringBoot起步依赖--dependencygroupIdorg.mybatis.spring.boot/groupIdartifactIdmybatis-spring-boot-starter/artifactIdversion2.1.3/version/dependency!--MySQL驱动--dependencygroupIdmysql/groupIdartifactIdmysql-connector-java/artifactId/dependency!-- https://mvnrepository.com/artifact/com.alibaba/fastjson --dependencygroupIdcom.alibaba/groupIdartifactIdfastjson/artifactIdversion2.0.12/version/dependencydependencygroupIdjunit/groupIdartifactIdjunit/artifactIdversion4.12/versionscopetest/scope/dependencydependencygroupIdorg.projectlombok/groupIdartifactIdlombok/artifactId/dependency/dependencies项目结构如图所示 书写yml文件
我们在application.yml中配置一下主从数据源
server:port: 8080spring:datasource:#主数据源master:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://host/yoordp?useUnicodetruecharacterEncodingutf-8useSSLtrueserverTimezoneGMT%2B8username: password: #从数据源slave:type: com.alibaba.druid.pool.DruidDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://host/yoordp?useUnicodetruecharacterEncodingutf-8useSSLtrueserverTimezoneGMT%2B8username: password:
数据库准备
准备两个数据库分别作为主从数据库当然这里不用强制实现它们俩直接的主从关系 然后分别建 user2 表然后准备一个可以作为区分的数据。如果有不清楚如果实现mysql主从复制的同学可以看看我的这篇文章docker实现mysql 主从复制
CREATE TABLE user2 (user_id int NOT NULL,account varchar(255) DEFAULT NULL,nickname varchar(255) DEFAULT NULL,password varchar(255) DEFAULT NULL,headimage_url varchar(255) DEFAULT NULL,introduce varchar(255) DEFAULT NULL,PRIMARY KEY (user_id)
) ENGINEInnoDB DEFAULT CHARSETutf8mb3;config目录各文件介绍
定义Spring AOP的切面类 DataSourceAop
DataSourceAop 是一个Spring AOP切面类用于拦截方法调用并根据方法的特定条件来选择数据源类型。它通过Pointcut定义了两个切点表达式分别用于读操作和写操作的方法。在前置通知方法中根据目标方法上是否存在 Master 注解来决定使用主库还是从库。这样通过AOP的切面功能实现了数据库的读写分离。
package com.lxr.demo.config;import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;import java.lang.reflect.Method;/*** 默认情况下所有的查询都走从库插入/修改/删除走主库。我们通过方法名来区分操作类型CRUD* p* 切面不能建立在DAO层事务是在service开启的到dao层再切换数据源那事务就废了*/
Aspect
Component
public class DataSourceAop {/*** 第一个”*“符号 表示返回值的类型任意* com.sample.service.impl AOP所切的服务的包名即我们的业务部分* 包名后面的”..“ 表示当前包及子包* 第二个”*“ 表示类名*即所有类。此处可以自定义下文有举例* .*(..) 表示任何方法名括号表示参数两个点表示任何参数类型* p* 这是一个切点表达式它定义了一个切点该切点在执行以下条件时成立* !annotation(com.lxr.demo.config.Master): 这表示切点会排除那些带有com.lxr.demo.config.Master注解的方法。* execution(* com.lxr.demo.service.*.select*(..)):* 表示切点会包含所有com.lxr.demo.service包下以select开头的方法并且方法参数可以是任意个数、任意类型。* execution(* com.lxr.demo.service..*.find*(..)):* 表示切点会包含所有com.lxr.demo.service包及其子包下以find开头的方法并且方法参数可以是任意个数、任意类型。*/Pointcut(!annotation(com.lxr.demo.config.Master) (execution(* com.lxr.demo.service.*.select*(..)) || execution(* com.lxr.demo.service..*.find*(..)) ) )public void readPointcut() {}Pointcut(annotation(com.lxr.demo.config.Master) || execution(* com.lxr.demo.service..*.save*(..)) || execution(* com.lxr.demo.service..*.add*(..)) || execution(* com.lxr.demo.service..*.insert*(..)) || execution(* com.lxr.demo.service..*.update*(..)) || execution(* com.lxr.demo.service..*.edit*(..)) || execution(* com.lxr.demo..*.delete*(..)) || execution(* com.lxr.demo..*.remove*(..)))public void writePointcut() {}Before(readPointcut())public void read(JoinPoint jp) {
/*** JoinPoint对象封装了SpringAop中切面方法的信息,在切面方法中添加JoinPoint参数,就可以获取到封装了该方法信息的JoinPoint对象.* 常用api:** 方法名 功能* Signature getSignature(); 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息* Object[] getArgs(); 获取传入目标方法的参数对象* Object getTarget(); 获取被代理的对象* Object getThis(); 获取代理对象*///获取当前的方法信息MethodSignature methodSignature (MethodSignature) jp.getSignature();//方法头指定修饰符(例如static)、返回值类型、方法名、和形式参数。Method method methodSignature.getMethod();//判断方法上是否存在注解Masterboolean present method.isAnnotationPresent(Master.class);//判断注解是否存在该元素上如果有则返回true否则falseif (!present) {//如果不存在默认走从库读System.out.println(no);DBContextHolder.slave();} else {//如果存在走主库读System.out.println(yes);DBContextHolder.master();}}Before(writePointcut())public void write() {System.out.println(write);DBContextHolder.master();}/*** 另一种写法if...else... 判断哪些需要读从数据库其余的走主数据库*/
// Before(execution(* com.cjs.example.service.impl.*.*(..)))
// public void before(JoinPoint jp) {
// String methodName jp.getSignature().getName();
//
// if (StringUtils.startsWithAny(methodName, get, select, find)) {
// DBContextHolder.slave();
// }else {
// DBContextHolder.master();
// }
// }}配置数据源和动态数据源切换
我们首先创建一个配置类 DataSourceConfig 来配置德鲁伊数据源和动态数据源切换。这个配置类中使用了Configuration和Bean注解定义了两个数据源主库和从库和一个动态数据源。动态数据源会根据业务需求自动选择主库还是从库从而实现了读写分离的功能。这在多数据库场景下非常有用可以提高数据库的读取性能。
package com.lxr.demo.config;import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;/*** 增加了 DataSourceConfig 这个配置文件之后,需要添加druid连接池单数据源自动装载时不会出这样的问题** Configuration 注解表明这就是一个配置类,指示一个类声明一个或者多个Bean 声明的方法并且由Spring容器统一管理以便在运行时为这些bean生成bean的定义和服务请求的类。*/
Configuration
public class DataSourceConfig {/*** 注入主库数据源*/BeanConfigurationProperties(prefix spring.datasource.master)public DataSource masterDataSource() {return new DruidDataSource();}/*** 注入从库数据源*/BeanConfigurationProperties(prefix spring.datasource.slave)public DataSource slaveDataSource() {return new DruidDataSource();}/*** 配置选择数据源** param masterDataSource* param slaveDataSource* return DataSource*/Beanpublic DataSource myRoutingDataSource(Qualifier(masterDataSource) DataSource masterDataSource, Qualifier(slaveDataSource) DataSource slaveDataSource) {MapObject, Object targetDataSource new HashMap();targetDataSource.put(DBTypeEnum.MASTER, masterDataSource);targetDataSource.put(DBTypeEnum.SLAVE, slaveDataSource);MyRoutingDataSource myRoutingDataSource new MyRoutingDataSource();//找不到用默认数据源myRoutingDataSource.setDefaultTargetDataSource(masterDataSource);//可选择目标数据源myRoutingDataSource.setTargetDataSources(targetDataSource);return myRoutingDataSource;}
}创建自定义注解
接下来我们创建一个自定义注解 Master 来标记我们需要进行主从分离的方法。
package com.lxr.demo.config;import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;/*** 有时候主从延迟需要强制读主库的注解*/
Retention(RetentionPolicy.RUNTIME)
Target(ElementType.METHOD)
public interface Master {
}定义数据库读写分离的工具类DBContextHolder
这里的 DBContextHolder 是一个线程上下文工具类通过 ThreadLocal 来实现不同线程使用不同数据源的功能。在实现数据库读写分离的场景下它可以根据业务需求自动选择主库或从库确保在多线程环境下的数据源正确切换。这种实现方式非常适用于多线程环境下需要使用读写分离的项目。
package com.lxr.demo.config;/*** ThreadLocal 定义数据源切换通过ThreadLocal将数据源绑定到每个线程上下文中* ThreadLocal 用来保存每个线程的是使用读库还是写库。操作结束后清除该数据避免内存泄漏。*/
public class DBContextHolder {/*** ThreadLocal是一个线程内部的数据存储类通过它可以在指定的线程中存储数据对数据存储后只有在当前线程中才可以获取到存储的数据对于其他线程来说是无法获取到数据。* 大致意思就是ThreadLocal提供了线程内存储变量的能力这些变量不同之处在于每一个线程读取的变量是对应的互相独立的通过get和set方法就可以得到当前线程对应的值。*/private static final ThreadLocalDBTypeEnum contextHolder new ThreadLocal();public static void set(DBTypeEnum dbTypeEnum) {contextHolder.set(dbTypeEnum);}public static DBTypeEnum get() {return contextHolder.get();}public static void master() {set(DBTypeEnum.MASTER);System.out.println(--------以下操作为master操作--------);}public static void slave() {set(DBTypeEnum.SLAVE);System.out.println(--------以下操作为slave读操作--------);}public static void clear() {contextHolder.remove();}
}定义枚举类DBTypeEnum
这里的 DBTypeEnum 是一个枚举类用于表示数据库的主库和从库在数据库读写分离的实现中可能会用作标识数据源类型的常量以便在动态数据源切换时选择不同的数据源。这种枚举常量的使用方式有助于代码的可读性和维护性。
package com.lxr.demo.config;public enum DBTypeEnum {MASTER, SLAVE;
}配置Mybatis指定数据源:SqlSessionFactory和事务管理器
这里的MyBatisConfig 是一个Spring配置类用于配置MyBatis的SqlSessionFactory和事务管理器。通过这个配置类MyBatis可以连接到动态数据源并实现数据库的读写分离。同时启用了事务管理功能确保在进行数据库操作时能够进行事务控制。
package com.lxr.demo.config;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.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;import javax.annotation.Resource;
import javax.sql.DataSource;/*** 配置Mybatis指定数据源:SqlSessionFactory和事务管理器*/
Configuration
EnableTransactionManagement
public class MyBatisConfig {/*** 注入自己重写的数据源*/Resource(name myRoutingDataSource)private DataSource myRoutingDataSource;/*** 配置SqlSessionFactory** return SqlSessionFactory* throws Exception*/Beanpublic SqlSessionFactory sqlSessionFactory() throws Exception {SqlSessionFactoryBean sqlSessionFactoryBean new SqlSessionFactoryBean();sqlSessionFactoryBean.setDataSource(myRoutingDataSource);//ResourcePatternResolver(资源查找器)定义了getResources来查找资源//PathMatchingResourcePatternResolver提供了以classpath开头的通配符方式查询,否则会调用ResourceLoader的getResource方法来查找
// ResourcePatternResolver resolver new PathMatchingResourcePatternResolver();
// sqlSessionFactoryBean.setMapperLocations(resolver.getResources(mapperLocation));return sqlSessionFactoryBean.getObject();}/*** 事务管理器不写则事务不生效事务需要知道当前使用的是哪个数据源才能进行事务处理*/Beanpublic PlatformTransactionManager platformTransactionManager() {return new DataSourceTransactionManager(myRoutingDataSource);}// /**
// * 当自定义数据源用户必须覆盖SqlSessionTemplate,开启BATCH处理模式
// *
// * param sqlSessionFactory
// * return
// */
// Bean
// public SqlSessionTemplate sqlSessionTemplate(Qualifier(sqlSessionFactory) SqlSessionFactory sqlSessionFactory) {
// return new SqlSessionTemplate(sqlSessionFactory, ExecutorType.BATCH);
// }}自定义数据源路由类MyRoutingDataSource
这里的MyRoutingDataSource 是一个自定义的数据源路由类继承了 AbstractRoutingDataSource 类。它通过重写 determineCurrentLookupKey() 方法动态决定使用哪个数据源从而实现了数据库的读写分离。这种动态数据源切换的方式非常灵活可以根据业务需求在运行时动态选择不同的数据源。
package com.lxr.demo.config;import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
import org.springframework.lang.Nullable;
/*** 重写 determineCurrentLookupKey 方法获取当前线程上绑定的路由key。Spring 在开始进行数据库操作时会通过这个方法来决定使用哪个数据库源因此我们在这里调用上面 DbContextHolder 类的getDbType()方法获取当前操作类别。** AbstractRoutingDataSource的getConnection() 方法根据查找 lookup key 键对不同目标数据源的调用通常是通过(但不一定)某些线程绑定的事物上下文来实现。** AbstractRoutingDataSource的多数据源动态切换的核心逻辑是在程序运行时把数据源数据源通过 AbstractRoutingDataSource 动态织入到程序中灵活的进行数据源切换。** 基于AbstractRoutingDataSource的多数据源动态切换可以实现读写分离这么做缺点也很明显无法动态的增加数据源。*/
public class MyRoutingDataSource extends AbstractRoutingDataSource {/*** determineCurrentLookupKey()方法决定使用哪个数据源、* 根据Key获取数据源的信息上层抽象函数的钩子*/NullableOverrideprotected Object determineCurrentLookupKey() {return DBContextHolder.get();}
}config配置类总结
上面介绍了config中的各种配置类以及相关工具类现在对它们进行简单梳理 ↓
DBTypeEnum这是一个枚举类定义了两个枚举常量 MASTER 和 SLAVE分别表示数据库的主库和从库。DBContextHolder这是一个工具类使用了 ThreadLocal 来定义数据源切换。它可以将数据源与每个线程的上下文绑定在一起用于在多线程环境下实现不同线程使用不同的数据源。DataSourceConfig这是一个Spring配置类用于配置数据源。它定义了两个 Bean 方法分别用于创建主库数据源和从库数据源。此外还定义了一个 myRoutingDataSource 方法用于创建一个动态数据源根据不同的数据源类型选择相应的数据源。MyRoutingDataSource这是一个自定义的数据源路由类继承了 AbstractRoutingDataSource 类。它重写了 determineCurrentLookupKey() 方法用于动态决定当前使用的数据源根据 DBContextHolder 中存储的数据源类型主库或从库选择相应的数据源。MyBatisConfig这是一个Spring配置类用于配置MyBatis的 SqlSessionFactory 和事务管理器。它通过 Resource 注解将 myRoutingDataSource 自动注入将动态数据源应用到MyBatis框架中。DataSourceAop这是一个切面类用于在使用自定义注解时拦截方法调用。它在 before 方法中根据方法上的自定义注解决定将当前线程的数据源设置为主库或从库从而实现读写分离的功能。
这些类共同实现了一个数据库读写分离的功能。DBTypeEnum 定义了数据源类型DBContextHolder 管理当前线程的数据源类型DataSourceConfig 配置多个数据源和动态数据源切换MyRoutingDataSource 实现数据源的动态路由MyBatisConfig 将动态数据源应用到MyBatis框架中DataSourceAop 切面根据方法上的注解选择数据源类型。这种组合使得我们可以在一个Spring Boot项目中实现数据库读写分离的功能。 其他文件说明
UserController
这个文件是一个Spring Boot的控制器类名为 UserController。它处理来自前端的HTTP请求调用 UserService 中的方法来处理业务逻辑并返回相应的结果。
package com.lxr.demo.controller;import com.lxr.demo.entity.UserEntity;
import com.lxr.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;
import java.util.Random;RestController
public class UserController {AutowiredUserService userService;RequestMapping(/listUser)public ListUserEntity listUser() {ListUserEntity users userService.findAll();return users;}RequestMapping(/insertUser)public void insertUser() {UserEntity userEntity new UserEntity();Random random new Random();userEntity.setUser_id(random.nextInt());userEntity.setAccount(22222);userEntity.setNickname(lxrlxrlxr);userEntity.setPassword(123);userService.insertUser(userEntity);}}UserEntity
一个普通的实体类使用了lombok的Data注解。
package com.lxr.demo.entity;import lombok.Data;Data
public class UserEntity {private Integer user_id;private String account;private String nickname;private String password;private String headimage_url;private String introduce;}UserMapper
一个简单的dao层。
package com.lxr.demo.mapper;import com.lxr.demo.entity.UserEntity;
import org.apache.ibatis.annotations.*;
import java.util.List;/*** Spring通过Mapper注解实现动态代理mybatis会自动创建Dao接口的实现类代理对象注入IOC容器进行管理这样就不用编写Dao层的实现类*/
Mapper
public interface UserMapper {Select(SELECT * FROM user2)ListUserEntity findAll();Insert(insert into user2(user_id,account,nickname,password) values(#{user_id},#{account}, #{nickname}, #{password}))int insert(UserEntity user);// Update(UPDATE user2 SET account#{account},nickname#{nickname} WHERE id #{id})
// void update(UserEntity user);
//
// Delete(DELETE FROM user2 WHERE id #{id})
// void delete(Long id);
}
UserService
一个简单的Service层。
package com.lxr.demo.service;import com.lxr.demo.entity.UserEntity;
import com.lxr.demo.mapper.UserMapper;
import com.lxr.demo.config.Master;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.List;Service
public class UserService {AutowiredUserMapper userMapper;public ListUserEntity findAll() {return userMapper.findAll();}Masterpublic int insertUser(UserEntity user) {return userMapper.insert(user);}// void update(UserEntity user);
//
// void delete(Long id);}主启动类DemoApplication
package com.lxr.demo;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;SpringBootApplication
MapperScan(com.lxr.demo.mapper)
public class DemoApplication {public static void main(String[] args) {SpringApplication.run(DemoApplication.class,args);}
}功能演示
我们启动项目,打开浏览器或者postman等工具
分别访问:
从库的读操作 http://localhost:8080/listUser 主库的写操作 http://localhost:8080/insertUser 总结
通过本篇博客我们学习了如何使用 Spring AOP 结合自定义注解和德鲁伊数据源来实现主从数据库切换和方法的读写分离。通过自定义注解 Master 来区分主库还是从库通过切点来区分读写方法我们成功地将读写操作路由到不同的数据源从而提高了应用程序的性能和可伸缩性。读写分离是一个重要的数据库优化策略在实际的生产环境中非常有用。
希望本篇博客对您有所帮助如果您有任何问题或建议欢迎在评论区留言。谢谢阅读 结语
初学一门技术时总有些许的疑惑别怕它们是我们学习路上的点点繁星帮助我们不断成长。
积少成多滴水成河。文章粗浅希望对大家有帮助