当前位置: 首页 > 科技观察

SpringBoot实现MySQL读写分离技术

时间:2023-03-20 22:32:36 科技观察

前言首先想一个问题:在高并发场景下,数据库有哪些优化方式?常用的实现方式有以下几种:读写分离、加缓存、主从架构集群、分库分表等。在互联网应用中,大多是读多写少的场景。设置了两个库,主库和读库。主库负责写,从库主要负责读。可以建立读库集群,减少读写冲突,减轻数据库负载,通过隔离数据源上的读写功能来保护数据库。实际使用中凡是涉及写的部分直接切换到主库,涉及到读的部分直接切换到读库。这是典型的读写分离技术。这篇博文将重点关注读写分离,并讨论如何实现它。图片主从同步的局限性:这里分为主库和从库。主库和从库保持相同的数据库结构。主库负责写入。写入数据时,会自动同步数据到从库;从库负责读,当有读请求到来时,直接从读库中读取数据,主库会自动将数据复制到从库。不过本篇博客不介绍这部分配置的知识,因为更多的是运维方面的工作。这里涉及到一个问题:主从复制的延迟问题。在写master数据库的时候突然进来一个读请求,这个时候数据还没有完全同步,读请求的数据读不到或者读到的数据小于原来的值。最简单的具体方案就是把读请求暂时指向主库,但同时也失去了一部分主从分离的意义。也就是说,在严格意义上的数据一致性场景下,读写分离并不完全适用。注意更新的及时性是读写分离的缺点。好吧,这部分只是为了理解。接下来看看如何通过java代码实现读写分离:项目需要引入如下依赖:springBoot、spring-aop、spring-jdbc、aspectjweaver等:主从数据源配置我们需要配置主从数据库,主从数据库的配置一般写在配置文件中。通过@ConfigurationProperties注解,可以将配置文件(一般命名为:application.Properties)中的属性映射到具体的类属性,从而读取写入的值注入到具体的代码配置中。按照习惯,大于约定的原则,我们总是把主库标为master,从库标为slave。本项目使用阿里的druid数据库连接池,使用构建器模式创建DataSource对象。DataSource是代码层面抽象出来的数据源,接下来需要配置sessionFactory、sqlTemplate、事务管理器等/***主从配置**@authorwyq*/@Configuration@MapperScan(basePackages="com.wyq.mysqlreadwriteseparate.mapper",sqlSessionTemplateRef="sqlTemplate")publicclassDataSourceConfig{/***主库*/@Bean@ConfigurationProperties(prefix="spring.datasource.master")publicDataSourcemaster(){returnDruidDataSourceBuilder.create().build();}/***来自图书馆*/@Bean@ConfigurationProperties(prefix="spring.datasource.slave")publicDataSourceslaver(){returnDruidDataSourceBuilder.create()。build();}/***实例化数据源路由*/@BeanpublicDataSourceRouterdynamicDB(@Qualifier("master")DataSourcemasterDataSource,@Autowired(required=false)@Qualifier("slaver")DataSourceslaveDataSource){DataSourceRouterdynamicDataSource=newDataSourceRouter();MaptargetDataSources=newHashMap<>();targetDataSources.put(DataSourceEnum.MASTER.getDataSourceName(),masterDataSource);if(slaveDataSource!=null){targetDataSources.put(DataSourceEnum.SLAVE.getDataSourceName(),slaveDataSource);}dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource);returndynamicDataSource;}/***配置sessionFactory*@paramdynamicDataSource*@return*@throwsException*/@BeanpublicSqlSessionFactorysessionFactory(@Qualifier("dynamicDB")DataSourcedynamicDataSource)throwsException{SqlSessionFactoryBeanbean=newSqlSessionFactoryBean();bean.setMapperLocations(newPathMatchingResourcePatternResolver().classgetResources(:mapper/*Mapper.xml"));bean.setDataSource(dynamicDataSource);returnbean.getObject();}/***创建sqlTemplate*@paramsqlSessionFactory*@return*/@BeanpublicSqlSessionTemplatesqlTemplate(@Qualifier("sessionFactory")SqlSessionFactorysqlSessionFactory){returnnewSqlSessionTemplate(sqlSessionFactory);}/***事务配置**@paramdynamicDataSource*@return*/@Bean(name="dataSourceTx")publicDataSourceTransactionManagerdataSourceTransactionManager(@Qualifier("dynamicDB")DataSourcedynamicDataSource){DataSourceTransactionManagerdataSourceTransactionManager=newDataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dynamicDataSource);returndataSourceTransactionManager;}}二:数据源路径由的配置路由在主从分离中非常重要。它基本上是读写切换的核心。Spring提供了AbstractRoutingDataSource,可以根据用户定义的规则选择当前数据源。作用是在执行查询前设置使用的数据源,实现动态路由。数据源,在每次数据库查询操作前执行其抽象方法determineCurrentLookupKey()来决定使用哪个数据源。为了拥有一个全局的数据源管理器,我们需要引入DataSourceContextHolder这个数据库上下文管理器,可以理解为一个全局变量,可以随时访问(具体介绍见下文)。它的主要功能是保存当前数据源;publicclassDataSourceRouterextendsAbstractRoutingDataSource{/***最终determineCurrentLookupKey返回是从DataSourceContextHolder中获取的,所以动态切换数据源时注解*要设置DataSourceContextHolder值**@return*/@OverrideprotectedObjectdetermineCurrentLookupKey(){returnDataSourceContextHolder.get();}}三:数据源上下文数据源上下文保存器方便程序随时获取当前数据源。它主要使用了ThreadLocal封装,因为ThreadLocal是线程隔离的,天然具有线程安全的优势。set、get、clear方法都暴露在这里。set方法用于赋值当前数据源名称,get方法用于获取当前数据源名称,clear方法用于清除ThreadLocal中的内容,因为ThreadLocal的key是weakReference,如果有有内存泄漏的风险,使用remove方法防止内存泄漏;/***使用ThreadLocal包保存数据源的在线context上下文*/publicclassDataSourceContextHolder{privatestaticfinalThreadLocalcontext=newThreadLocal<>();/***赋值**@paramdatasourceType*/publicstaticvoidset(StringdatasourceType){context.set(datasourceType);}/***getvalue*@return*/publicstaticStringget(){returncontext.get();}publicstaticvoidclear(){context.remove();}}四:开关注解和Aop配置首先,我们定义一个@DataSourceSwitcher注解,它有两个属性①当前数据源②是否清除当前数据源,并且只能放在方法上,(不能放在类上,没必要放在class,因为我们切换数据源的时候,肯定是方法操作),这个注解的主要作用是切换数据源,在dao层操作数据库的时候,可以在method上指明是哪些数据当前使用的来源;@DataSourceSwitcher注解定义:@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic@interfaceDataSourceSwitcher{/***默认数据源*@return*/DataSourceEnumvalue()defaultDataSourceEnum.MASTER;/***clear*@return*/booleanclear()defaulttrue;}DataSourceAop配置为了让@DataSourceSwitcher注解具备切换数据源的能力,我们需要使用AOP,然后使用@Aroud注解在方法上找到带有@DataSourceSwitcher.class的方法,然后取数据源的值在注释上配置并将其设置为DataSourceContextHolder。实现将当前方法上配置的数据源注入全局作用域;@Slf4j@Aspect@Order(value=1)@ComponentpublicclassDataSourceContextAop{@Around("@annotation(com.wyq.mysqlreadwriteseparate.annotation.DataSourceSwitcher)")publicObjectsetDynamicDataSource(ProceedingJoinPointpjp)throwsThrowable{booleanclear=false;try{Methodmethod=this.getMethod(pjp);DataSourceSwitcherdataSourceSwitcher=method.getAnnotation(DataSourceSwitcher.class);clear=dataSourceSwitcher.clear(Holder.Source.Sourcerdata)(Context.getDataSourceName());log.info("DataSourceSwitchto:{}",dataSourceSwitcher.value().getDataSourceName());returnpjp.proceed();}finally{if(clear){DataSourceContextHolder.clear();}}}privateMethodgetMethod(JoinPointpjp){MethodSignaturesignature=(MethodSignature)pjp.getSignature();returnsignature.getMethod();}}五:用法并测试配置读写分离后,就可以它在代码中使用。一般来说,我们在service层或者dao层使用,在需要查询的方法中加上@DataSourceSwitcher(DataSourceEnum.SLAVE),表示该方法下的所有操作都是读取库;在需要update或者insert的时候,使用@DataSourceSwitcher(DataSourceEnum.MASTER)表示接下来要写库。其实还有一种更自动化的写法。可以配置AOP根据方法的前缀自动切换数据源,比如update、insert、fresh等前缀的方法名自动设置为writelibraries,select、get、query等前缀的方法名都配置为读取库。这是一种比较自动化的配置编写方式。缺点是需要严格按照aop配置定义方法名,否则会失败publicListgetOrderorder.setOrderId(orderId);returnorderMapper.saveOrder(order);}}六:总结上图是基本流程的简化图。本篇博客介绍如何实现数据库读写分离。注意,读写分离的核心是数据路由。继承AbstractRoutingDataSource,重写其determineCurrentLookupKey方法。同时需要关注全局上下文管理器DataSourceContextHolder。它是保存数据源上下文的主要类。也是路由方法中找到的数据源的值,相当于数据源的中转站。结合jdbc-Template底层用于创建和管理数据源、事务等,完美实现了我们的数据库读写分离。