|
一、读写分离介绍当使用SpringBoot开发数据库应用时,读写分离是一种常见的优化策略。读写分离将读操作和写操作分别分配给不同的数据库实例,以提高系统的吞吐量和性能。读写分离实现主要是通过动态数据源功能实现的,动态数据源是一种通过在运行时动态切换数据库连接的机制。它允许应用程序根据不同的条件或配置选择不同的数据源,以实现更灵活和可扩展的数据库访问。二、实现读写分离-基础1.配置主数据库和从数据库的连接信息#主库配置spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.datasource.master.driver-class-name=com.mysql.jdbc.Driver#从库配置spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.slave.username=slavespring.datasource.slave.password=123456spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driver2.创建主数据库和从数据库的数据源配置类通过不同的条件限制和配置文件前缀可以完成不同数据源的创建工作,不止是主从也可以是多个不同的数据库主库数据源配置@Configuration@ConditionalOnProperty("spring.datasource.master.jdbc-url")publicclassMasterDataSourceConfiguration{@Bean("masterDataSource")@ConfigurationProperties(prefix="spring.datasource.master")publicDataSourcemasterDataSource(){returnDataSourceBuilder.create().build();}}从库数据源配置@Configuration@ConditionalOnProperty("spring.datasource.slave.jdbc-url")publicclassSlaveDataSourceConfiguration{@Bean("slaveDataSource")@ConfigurationProperties(prefix="spring.datasource.slave")publicDataSourceslaveDataSource(){returnDataSourceBuilder.create().build();}}3.创建主从数据源枚举publicenumDataSourceTypeEnum{/***主库*/MASTER,/***从库*/SLAVE,;}4.创建动态路由数据源这儿做了一个开关,可以控制读写分离的开启和关闭工作,可以将操作全部切换到主库进行。然后根据上下文中的数据源类型来返回不同的数据源类型枚举@Slf4jpublicclassDynamicRoutingDataSourceextendsAbstractRoutingDataSource{@Value("${DB_RW_SEPARATE_SWITCH:false}")privatebooleandbRwSeparateSwitch;@OverrideprotectedObjectdetermineCurrentLookupKey(){if(dbRwSeparateSwitch&DataSourceTypeEnum.SLAVE.equals(DataSourceContextHolder.getDataSourceType())){log.info("DynamicRoutingDataSource切换数据源到从库");returnDataSourceTypeEnum.SLAVE;}log.info("DynamicRoutingDataSource切换数据源到主库");//根据需要指定当前使用的数据源,这里可以使用ThreadLocal或其他方式来决定使用主库还是从库returnDataSourceTypeEnum.MASTER;}}5.创建动态数据源配置类将主数据库和从数据库的数据源添加到动态数据源中,并可以通过枚举创建一个数据源map,这样就可以通过上面的路由返回的枚举来切换数据源@Configuration@ConditionalOnProperty("spring.datasource.master.jdbc-url")publicclassDynamicDataSourceConfiguration{@Bean("dataSource")@PrimarypublicDataSourcedynamicDataSource(DataSourcemasterDataSource,DataSourceslaveDataSource){MaptargetDataSources=newHashMap();targetDataSources.put(DataSourceTypeEnum.MASTER,masterDataSource);targetDataSources.put(DataSourceTypeEnum.SLAVE,slaveDataSource);DynamicRoutingDataSourcedynamicDataSource=newDynamicRoutingDataSource();dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource);returndynamicDataSource;}}6.创建DatasourceContextHolder类使用ThreadLocal存储当前线程的数据源类型注意这儿有个潜在风险就是创建新的线程时会导致ThreadLocal中的数据无法正确读取,如果涉及到在开启新线程可以使用TransmittableThreadLocal来进行父子线程数据的同步,git地址:https://github.com/alibaba/transmittable-thread-localpublicclassDataSourceContextHolder{privatestaticfinalThreadLocalcontextHolder=newThreadLocal();publicstaticvoidsetDataSourceType(DataSourceTypeEnumdataSourceType){contextHolder.set(dataSourceType);}publicstaticDataSourceTypeEnumgetDataSourceType(){returncontextHolder.get();}publicstaticvoidclearDataSourceType(){contextHolder.remove();}}7.创建自定义注解,用于标记主和从数据源@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceMasterDataSource{}@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)public@interfaceSlaveDataSource{}8.创建切面类,拦截数据库操作,并根据注解设置切换数据源参数@Aspect@ComponentpublicclassDataSourceAspect{@Before("@annotation(xxx.MasterDataSource)")publicvoidsetMasterDataSource(JoinPointjoinPoint){DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);}@Before("@annotation(xxx.SlaveDataSource)")publicvoidsetSlaveDataSource(JoinPointjoinPoint){DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.SLAVE);}@After("@annotation(xxx.MasterDataSource)||@annotation(xxx.SlaveDataSource)")publicvoidclearDataSource(JoinPointjoinPoint){DataSourceContextHolder.clearDataSourceType();}}9.在Service层的方法上使用自定义注解标记查询数据源@ServicepublicclassTestService{@AutowiredprivateTestDaotestDao;@SlaveDataSourcepublicTesttest(){returntestDao.queryByPrimaryKey(11L);}}10.排除掉数据源自动配置类如果不排除自动配置类会导致初始化多个dataSource对象导致出现问题SpringBootApplication(exclude={DataSourceAutoConfiguration.class})三、实现读写分离-进阶1.使用链接池,以Hikari为例修改链接配置,加入链接池相关配置即可#主库配置spring.datasource.master.jdbc-url=jdbc:mysql://ip:port/master?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.master.username=masterspring.datasource.master.password=123456spring.datasource.master.driver-class-name=com.mysql.jdbc.Driverspring.datasource.master.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.master.hikari.name=masterspring.datasource.master.hikari.minimum-idle=5spring.datasource.master.hikari.idle-timeout=30spring.datasource.master.hikari.maximum-pool-size=10spring.datasource.master.hikari.auto-commit=truespring.datasource.master.hikari.pool-name=DatebookHikariCPspring.datasource.master.hikari.max-lifetime=1800000spring.datasource.master.hikari.connection-timeout=30000spring.datasource.master.hikari.connection-test-query=SELECT1#从库配置spring.datasource.slave.jdbc-url=jdbc:mysql://ip:port/slave?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai&useSSL=falsespring.datasource.slave.username=rootspring.datasource.slave.password=123456spring.datasource.slave.driver-class-name=com.mysql.jdbc.Driverspring.datasource.slave.type=com.zaxxer.hikari.HikariDataSourcespring.datasource.slave.hikari.name=masterspring.datasource.slave.hikari.minimum-idle=5spring.datasource.slave.hikari.idle-timeout=30spring.datasource.slave.hikari.maximum-pool-size=10spring.datasource.slave.hikari.auto-commit=truespring.datasource.slave.hikari.pool-name=DatebookHikariCPspring.datasource.slave.hikari.max-lifetime=1800000spring.datasource.slave.hikari.connection-timeout=30000spring.datasource.slave.hikari.connection-test-query=SELECT12.集成mybatis并在写入时强制切换到主库不需要做任何配置,正常集成mybatis即可使用读写分离功能可以通过mybatis的拦截器在写入操作时强制切换到主库@Intercepts({@Signature(type=Executor.class,method="update",args={MappedStatement.class,Object.class}),})@ComponentpublicclassWriteInterceptorimplementsInterceptor{@OverridepublicObjectintercept(Invocationinvocation)throwsThrowable{//获取SQL类型DataSourceTypeEnumdataSourceType=DataSourceContextHolder.getDataSourceType();if(DataSourceTypeEnum.SLAVE.equals(dataSourceType)){DataSourceContextHolder.setDataSourceType(DataSourceTypeEnum.MASTER);}try{//执行SQLreturninvocation.proceed();}finally{//恢复数据源考虑到写入后可能会反查,后续都走主库//DataSourceContextHolder.setDataSourceType(dataSourceType);}}}-end-
|
|