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

SpringBoot多数据源如何处理事务?教你一招!

时间:2023-03-12 01:04:01 科技观察

首先申明,本文纯属技术探讨。从实际应用的角度来说,我不推荐这样玩分布式事务,也不推荐这样玩多数据源。毕竟分布式事务主要还是用在微服务场景下。好了废话不多说,开始吧。一、思路梳理首先,我们来梳理一下思路。上一篇我们是一个微服务,在A中分别调用B和C,当B或C其中一个执行失败时,我们回滚。B和C都调用了远程服务。所谓回滚,并不是传统意义上的数据库回滚,而是一种“反向补偿”,即使用一条更新SQL来恢复更新后的数据。在这个例子中,B和C都是远程服务,他们操作的是不同的数据库。我们的多个数据源不就是这样吗!在微服务中,一个服务实际上代表了一个数据源。在我们的多个数据源的情况下,一个注释可以标记一个数据源。以此类推,你会发现使用分布式事务来解决多数据源中的事务问题其实是非常容易的。而且这不是微服务项目,而是单体项目,更简单!但是,有一些细节需要注意。2.代码实践接下来结合代码说说。2.1案例准备首先,多数据源的案例我就不赘述了。我们之前已经写过一篇,这里不再赘述。文章开头也有相关链接。没看过的朋友可以先看看。也可以在公众号后台直接回复dynamic_datasource获取相关案例。2.2开始整理因为上一篇我主要给大家分享了seata的AT模式,所以这篇文章也是一样,先用AT模式。朋友知道我们多数据源的情况下,我们使用了test08和test09这两个库,现在还在使用这两个库,但是现在因为我们使用的是AT模式,所以需要使用这两个库创建undolog表in,用来记录我们对表的更新操作。当事务提交时,撤销日志表中的数据将被清除。undolog,undolog表的脚本如下:CREATETABLE`undo_log`(`id`bigint(20)NOTNULLAUTO_INCREMENT,`branch_id`bigint(20)NOTNULL,`xid`varchar(100)NOTNULL,`context`varchar(128)NOTNULL,`rollback_info`longblobNOTNULL,`log_status`int(11)NOTNULL,`log_created`datetimeNOTNULL,`log_modified`datetimeNOTNULL,PRIMARYKEY(`id`),UNIQUEKEY`ux_undo_log`(`xid`,`branch_id`))ENGINE=InnoDBAUTO_INCREMENT=1DEFAULTCHARSET=utf8;数据库准备好后,下一步就是准备依赖。Seata有两个依赖,一个是seata-all,还有一个是微服务版本。我们这里直接使用上一篇文章中使用的微服务版本。对于服务版本,依赖如下:com.alibaba.cloudspring-cloud-starter-alibaba-seata2.2.2.RELEASE配置完成后,接下来提供file.conf和regsigry.conf两个配置文件。这两个配置文件和上一篇介绍的完全一样,这里不再赘述。接下来配置application.yaml如下:这里看一下几个配置:tx-service-group:这个是事务组的名字,相关的名字在file.conf中配置。allow-circular-references:这是为了允许循环依赖。可能有的朋友已经知道循环依赖在最新版的SpringBoot中已经被禁止了,但是循环依赖在这个seata中好像还在用,所以需要开启。enable-auto-data-source-proxy:由于seata会自动代理数据源,但是我们当前的数据源是自己加载的,所以关闭这个数据源的自动代理,以后使用我们自己的。application-id:给我们的应用程序一个名字。好了,这个文件配置好了。其次是数据源问题。刚才说了,seata会自动代理数据源。使用的代理对象是DataSourceProxy,而我们在之前的自定义数据源加载中并没有使用到这个DataSourceProxy对象,所以这里需要稍微修改一下,一共改动两处,如下:class)publicclassLoadDataSource{@AutowiredDruidPropertiesdruidProperties;publicMaploadAllDataSource(){Mapmap=newHashMap<>();Map>ds=druidProperties.getDs();尝试{SetkeySet=ds.keySet();for(Stringkey:keySet){DataSourcedataSource=druidProperties.dataSource((DruidDataSource)DruidDataSourceFactory.createDataSource(ds.get(key)));DataSourceProxyproxyDs=newDataSourceProxy(dataSource);map.put(key,proxyDs);}}catch(Exceptione){e.printStackTrace();}返回地图;}}其实这里的变化就是把之前的DataSource用DataSourceProxy重新包装,然后保存得到的DataSourceProxy,最后修改动态数据源:@ComponentpublicclassDynamicDataSourceextendsAbstractRoutingDataSource{publicDynamicDataSource(LoadDataSourceloadDataSource){//1.设置所有数据源MapallDs=loadDataSource.loadAllDataSource();super.setTargetDataSources(newHashMap<>(allDs));//2。设置默认数据源//以后,并不是所有的方法都用@DataSource注解,那些没有@DataSource注解的方法应该使用哪个数据源呢?super.setDefaultTargetDataSource(allDs.get(DataSourceType.DEFAULT_DS_NAME));//3super.afterPropertiesSet();}/***此方法用于返回数据源的名称。当系统需要获取数据源时,会自动调用获取数据源名称的方法*@return*/@OverrideprotectedObjectdetermineCurrentLookupKey(){returnDynamicDataSourceContextHolder.getDataSourceType();}}Map中的值类型变为DataSourceProxy,其他不变。还有一个地方需要修改,就是解析@DataSource注解的切面。在前面的解析中,我们捕获了异常,现在我们要将其抛出,如下:@Around("pc()")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{//获取上面的有效注解DataSource方法dataSource=getDataSource(pjp);if(dataSource!=null){//获取注解中数据源的名称Stringvalue=dataSource.value();DynamicDataSourceContextHolder.setDataSourceType(value);}尝试{返回pjp.proceed();}最后{DynamicDataSourceContextHolder.clearDataSourceType();}}扔掉的原因也很简单,因为这是一个切面方法,所有服务层的方法都是在这里执行的。如果捕获到异常,以后服务层方法就不会抛出异常,交易也不会生效。好,现在准备工作已经就绪。接下来我们来写一个简单的多数据源事务案例。首先我们创建一个MasterService,专门用来操作主数据源:@ServicepublicclassMasterService{@AutowiredMasterMappermasterMapper;@DataSource("master")publicvoidaddUser(Stringusername,Integerage){masterMapper.addUser(username,age);你不需要看映射器,它只是一个普通的添加。您可以在文末下载案例。另一个SlaveService操作slave数据源:@ServicepublicclassSlaveService{@AutowiredSlaveMapperslaveMapper;@DataSource("slave")publicvoidaddAccount(Stringname,Doublebalance){inti=1/0;slaveMapper.addAccount(名称,余额);}}从属数据源的某个方法出现异常。最后我们在UserService中分别调用这两个方法:@ServicepublicclassUserService{@AutowiredMasterServicemasterService;@AutowiredSlaveServiceslaveService;@GlobalTransactional(rollbackFor=Exception.class)publicvoidtest(){masterService.addUser("javaboy.org",99);slaveService.addAccount("javaboy.org",99.0);}}请注意,测试方法上有一个全局事务注释。好吧,让我们一起努力吧!现在让我们执行测试方法。因为slaveService#addAccount中的方法会抛出异常,会导致整个事务回滚。最后的结果是master没有添加数据。3.总结一下,结合前面的文章,相信你应该可以熟练使用seata分布式事务中的at模式了!