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

读写分离就这么简单,一个小笔记就够了

时间:2023-03-19 13:40:26 科技观察

前言相信有经验的同学都知道,当db的读写量过高时,我们会备份一个或多个从库进行数据备份然后主库主要负责写功能(也有阅读的需求,但压力不大)。当db分主从库时,我们还需要在项目中自动连接主从库,实现读写。分离效果。实现读写分离并不难,只要在数据库连接池中手动控制对应的db服务地址即可,但那样会侵入业务代码,而且一个项目操作数据库的地方可能很多。如果全部由人工控制,无疑是一项很大的工作量,为此,我们有必要改造一套方便的工具。就Java语言而言,如今的大部分项目都是基于SpringBoot框架来搭建项目结构的。结合Spring本身自带的AOP工具,我们可以很方便的构建出可以达到读写分离效果的注解类。可以达到不侵入业务代码的效果,使用起来更方便。下面就简单带大家写个demo。环境部署数据库:MySql库数量:2个,一主一从。关于mysql的主从环境部署网上有很多文章可以参考,这里就不介绍了。启动项目首先,毫无疑问,开始搭建一个SpringBoot项目,然后在pom文件中引入如下依赖:com.alibabadruid-spring-boot-starter1.1.10org.mybatis.spring.bootmybatis-spring-boot-starter1.3.2tk.mybatismapper-spring-boot-starter2.1.5mysqlmysql-connector-java8.0.16org.springframework.bootspring-boot-starter-jdbc提供org.springframework.bootspring-boot-starter-aop提供org.springframework.bootspring-boot-starter-weborg.projectlomboklomboktruecom.alibabafastjson1.2.4org.springframework.bootspring-boot-starter-test测试org.springframework.bootspring-boot-starter-data-jpa目录结构引入基本依赖后,梳理目录结构,完成的项目骨架为大致如下:建表创建表user,在主库执行sql语句在从库生成对应的表数据DROPTABLEIFEXISTS`user`;CREATETABLE`user`(`user_id`bigint(20)NOTNULLCOMMENT'用户id',`user_name`varchar(255)DEFAULT''COMMENT'用户名',`user_phone`varchar(50)DEFAULT''COMMENT'用户电话',`address`varchar(255)DEFAULT''COMMENT'地址',`weight`int(3)NOTNULLDEFAULT'1'COMMENT'权重大者优先',`created_at`datetimeNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'创建时间',`updated_at`datetimeDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新时间',PRIMARYKEY(`user_id`CH))ENGINE=InnoDBDEFAVALUES('1196978513958141952','测试1','18826334748','广州市海珠区','1','2019-0:11-228:51','2019-11-2214:28:26');INSERTINTO`user`VALUES('1196978513958141953','测试2','18826274230','广州市天河区','2','2019-11-2010:29:37','2019-11-2214:28:14');插入`user`VALUES('1196978513958141954','test3','18826273900','广州市天河区','1','2019-11-2010:30:19','2019-11-2214:28:30');主从数据源配置应用.yml,主要信息为主从数据库服务器的数据源配置:port:8001spring:jackson:date-format:yyyy-MM-ddHH:mm:sstime-zone:GMT+8datasource:type:com.alibaba.druid.pool.DruidDataSourcedriver-class-name:com.mysql.cj.jdbc.Drivermaster:url:jdbc:mysql://127.0.0.1:3307/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&failOverReadOnly=false&useSSL=false&zeroDateTimeBehavi=convertToNull&allowMultiQueries=trueusername:rootpassword:slave:url:jdbc:mysql://127.0.0.1:3308/user?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8OverRead&autofalse&useSSL=false&zeroDateTimeBehavior=convertToNull&allowMultiQueries=trueusername:rootpassword:因为有两个数据源,一主一从,我们用枚举类代替,这样就可以对应@GetterpublicenumDynamicDataSourceEnum{MASTER("master"),SLAVE("slave");优先vateStringdataSourceName;DynamicDataSourceEnum(StringdataSourceName){this.dataSourceName=dataSourceName;}}数据源配置信息类DataSourceConfig,这里配置了两个数据源,masterDb和slaveDb@Configuration@MapperScan(basePackages="com.xjt.proxy.mapper",sqlSessionTemplateRef="sqlTemplate")publicclassDataSourceConfig{//主库@Bean@ConfigurationProperties(prefix="spring.datasource.master")publicDataSourcemasterDb(){returnDruidDataSourceBuilder.create().build();}/***来自库*/@Bean@ConditionalOnProperty(prefix="spring.datasource",name="slave",matchIfMissing=true)@ConfigurationProperties(prefix="spring.datasource.slave")publicDataSourceslaveDb(){returnDruidDataSourceBuilder.create().build();}/***主从动态配置*/@BeanpublicDynamicDataSourcedynamicDb(@Qualifier("masterDb")DataSourcemasterDataSource,@Autowired(required=false)@Qualifier("slaveDb")DataSourceslaveDataSource){DynamicDataSourcedynamicDataSource=newDynamicDataSourcece();MaptargetDataSources=newHashMap<>();targetDataSources.put(DynamicDataSourceEnum.MASTER.getDataSourceName(),masterDataSource);if(slaveDataSource!=null){targetDataSources.put(DynamicDataSourceEnum.SLAVE.getDataSourceName(),slaveDataSource);}dynamicDataSource.setTargetDataSources(targetDataSources);dynamicDataSource.setDefaultTargetDataSource(masterDataSource);returndynamicDataSource;}@BeanpublicSqlSessionFactorysessionFactory(@Qualifier("dynamicDb")DataSourcedynamicDataSource)throwsException{SqlSessionFactoryBeanbean=newSqlSessionFactoryBean();bean.PasetMapperLocations(newSqlSessionFactoryBean();bean.PathMapperLocations(新).getResources("classpath*:mapper/*Mapper.xml"));bean.setDataSource(dynamicDataSource);returnbean.getObject();}@BeanpublicSqlSessionTemplatesqlTemplate(@Qualifier("sessionFactory")SqlSessionFactorysqlSessionFactory){returnnewSqlSessionTemplate(sqlSessionFactory);}@Bean(name="dataSourceTx")publicDataSourceTransactionManagerdataSourceTx(@Qualifier("dynamicDb")DataSourcedynamicDataSource){DataSourceTransactionManagerdataSourceTransactionManager=newDataSourceTransactionManager();dataSourceTransactionManager.setDataSource(dynamicDataSource);returndataSourceTransactionManager;}}设置路由设置路由的目的为了方便查找对应的数据源,我们可以用ThreadLocal将数据源的信息保存到各个线程中,这样我们就可以得到}publicstaticSOOURCE_CONTEXT.set(datasourceType);}publicstaticSOURCE_CONTEXT(){return_DAYNUR();}publicstaticvoidclear(){DYNAMIC_DATASOURCE_CONTEXT.remove();}}获取路由publicclassDynamicDataSourceextendsAbstractRoutingDataSource{@OverrideprotectedObjectdetermineCurrentLookupKey(){returnDataSourceContextHolder.get();}}的作用AbstractRoutingDataSource的目的是维护对应的内部数据源,根据lookup创建了一组目标数据源,并做了routingkey和目标数据源的映射,提供基于key查找数据源的方法数据源的注解为了方便数据源的切换,我们可以写一个注解,里面包含了数据源对应的枚举值,默认是主库,@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.METHOD)@Documentedpublic@interfaceDataSourceSelector{DynamicDataSourceEnumvalue()defaultDynamicDataSourceEnum.MASTER;booleanclear()defaulttrue;}aop在这里切换数据源,aop终于可以登场了。这里我们定义了一个aop类来为带有注解的方法进行切换数据源的操作。代码如下:@Slf4j@Aspect@Order(value=1)@ComponentpublicclassDataSourceContextAop{@Around("@annotation(com.xjt.proxy.dynamicdatasource.DataSourceSelector)")publicObjectsetDynamicDataSource(ProceedingJoinPointpjp)throwsThrowable{booleanclear=true;try{Methodmethod=this.getMethod(pjp);DataSourceSelectordataSourceImport=method.getAnnotation(DataSourceSelector.class);clear=dataSourceImport.clear();DataSourceContextHolder.set(dataSourceImport.value().getDataSourceName());log.info("========切换数据源为:{}",dataSourceImport.value().getDataSourceName());returnpjp.proceed();}finally{if(clear){DataSourceContextHolder.clear();}}}privateMethodgetMethod(JoinPointpjp){MethodSignaturesignature=(MethodSignature)pjp.getSignature();returnsignature.getMethod();}}至此,我们的准备和配置工作就完成了,下面开始测试效果先写Service文件,包括读取和更新两个方法,@ServicepublicclassUserService{@AutowiredprivateUserMapperuserMapper;@DataSourceSelector(value=DynamicDataSourceEnum.MASTER)publicintupdate(LonguserId){Useruser=newUser();user.setUserId(userId);user。setUserName("老雪");returnuserMapper.updateByPrimaryKeySelective(user);}@DataSourceSelector(value=DynamicDataSourceEnum.SLAVE)publicUserfind(LonguserId){Useruser=newUser();user.setUserId(userId);returnuserMapper.selectByPrimaryKey(user);}}根据方法上的注解可以看出,read方法是从库中走的,update方法是从主库中走的。更新的对象是userId为1196978513958141952的数据。然后我们写一个测试类来测试是否可以达到效果,@RunWith(SpringRunner.class)@SpringBootTestclassUserServiceTest{@AutowiredUserServiceuserService;@Testvoidfind(){Useruser=userService.find(1196978513958141952L);System.out.println("id:"+user.getUserId());System.out.println("name:"+user.getUserName());System.out.println("phone:"+user.getUserPhone());}@Testvoidupdate(){LonguserId=1196978513958141952L;userService.update(我们erId);Useruser=userService.find(userId);System.out.println(user.getUserName());}}测试结果:1.读取方法2.执行更新方法后,可以找到主从通过对比数据库中所有的库都有修改过的数据,说明我们的读写分离是成功的。当然update方法可以指向从库,这样只会修改从库的数据,不涉及主库。最后,上面的测试例子虽然比较简单,但是也符合常规的读写分离配置。值得注意的是,读写分离的作用是缓解写库即主库的压力,但必须以数据一致性为原则,即保证之间的数据主从数据库必须保持一致。如果一个方法涉及写逻辑,则该方法中所有的数据库操作都必须到主数据库。假设执行完写操作后,数据可能还没有同步到从库,然后也执行了读操作。如果读取的程序还是从从库走,那么就会出现数据不一致的情况。这是不允许的。