mybatis操作多数据源实现的方法
本文讲解"mybatis操作多数据源实现的方法",希望能够解决相关问题。
现在有一个mysql数据源和一个postgresql数据源,使用mybatis对两个数据源进行操作:
1. 注入多数据源
可以对两个数据源分别实现其service层和mapper层,以及mybatis的配置类:
@configuration // 这里需要配置扫描包路径,以及sqlsessiontemplateref @mapperscan(basepackages = "com.example.mybatisdemo.mapper.mysql", sqlsessiontemplateref = "mysqlsqlsessiontemplate") public class mysqlmybatisconfigurer { /** * 注入mysql数据源 */ @bean @configurationproperties(prefix = "spring.datasource.mysql") public datasource mysqldatasource() { return new druiddatasource(); } /** * 注入mysqlsqlsessionfactory */ @bean public sqlsessionfactory mysqlsqlsessionfactory(datasource mysqldatasource) throws exception { sqlsessionfactorybean factorybean = new sqlsessionfactorybean(); factorybean.setdatasource(mysqldatasource); // 设置对应的mapper文件 factorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:" + "/mappers/mysqlmapper.xml")); return factorybean.getobject(); } /** * 注入mysqlsqlsessiontemplate */ @bean public sqlsessiontemplate mysqlsqlsessiontemplate(sqlsessionfactory mysqlsqlsessionfactory) { return new sqlsessiontemplate(mysqlsqlsessionfactory); } /** * 注入mysqltransactionalmanager */ @bean public datasourcetransactionmanager mysqltransactionalmanager(datasource mysqldatasource) { return new datasourcetransactionmanager(mysqldatasource); } }
@configuration // 这里需要配置扫描包路径,以及sqlsessiontemplateref @mapperscan(basepackages = "com.example.mybatisdemo.mapper.postgresql", sqlsessiontemplateref = "postgresqlsqlsessiontemplate") public class postgresqlmybatisconfigurer { /** * 注入postgresql数据源 */ @bean @configurationproperties(prefix = "spring.datasource.postgresql") public datasource postgresqldatasource() { return new druiddatasource(); } /** * 注入postgresqlsqlsessionfactory */ @bean public sqlsessionfactory postgresqlsqlsessionfactory(datasource postgresqldatasource) throws exception { sqlsessionfactorybean factorybean = new sqlsessionfactorybean(); factorybean.setdatasource(postgresqldatasource); // 设置对应的mapper文件 factorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath:" + "/mappers/postgresqlmapper.xml")); return factorybean.getobject(); } /** * 注入postgresqlsqlsessiontemplate */ @bean public sqlsessiontemplate postgresqlsqlsessiontemplate(sqlsessionfactory postgresqlsqlsessionfactory) { return new sqlsessiontemplate(postgresqlsqlsessionfactory); } /** * 注入postgresqltransactionalmanager */ @bean public datasourcetransactionmanager postgresqltransactionalmanager(datasource postgresqldatasource) { return new datasourcetransactionmanager(postgresqldatasource); } }
在配置类中,分别注入了一个事务管理器transactionmanager,这个和事务管理是相关的。在使用@transactional注解时,需要配置其value属性指定对应的事务管理器。
2. 动态数据源
spring中提供了abstractroutingdatasource抽象类,可以用于动态地选择数据源。
public abstract class abstractroutingdatasource extends abstractdatasource implements initializingbean { @nullable private map targetdatasources; @nullable private object defaulttargetdatasource; private boolean lenientfallback = true; private datasourcelookup datasourcelookup = new jndidatasourcelookup(); @nullable private map resolveddatasources; @nullable private datasource resolveddefaultdatasource; // 略 }
通过源码可以看到,该抽象类实现了initializingbean接口,并在其afterpropertiesset方法中将数据源以的形式放入一个map中。
public void afterpropertiesset() { if (this.targetdatasources == null) { throw new illegalargumentexception("property 'targetdatasources' is required"); } else { this.resolveddatasources = collectionutils.newhashmap(this.targetdatasources.size()); this.targetdatasources.foreach((key, value) -> { object lookupkey = this.resolvespecifiedlookupkey(key); datasource datasource = this.resolvespecifieddatasource(value); // 将数据源以的形式放入map中 this.resolveddatasources.put(lookupkey, datasource); }); if (this.defaulttargetdatasource != null) { this.resolveddefaultdatasource = this.resolvespecifieddatasource(this.defaulttargetdatasource); } } }
该类中还有一个determinetargetdatasource方法,是根据lookupkey从map中获取对应的数据源,如果没有获取到,则使用默认的数据源。
protected datasource determinetargetdatasource() { assert.notnull(this.resolveddatasources, "datasource router not initialized"); object lookupkey = this.determinecurrentlookupkey(); // 根据lookupkey从map中获取对应的数据源 datasource datasource = (datasource)this.resolveddatasources.get(lookupkey); if (datasource == null && (this.lenientfallback || lookupkey == null)) { datasource = this.resolveddefaultdatasource; } if (datasource == null) { throw new illegalstateexception("cannot determine target datasource for lookup key [" + lookupkey + "]"); } else { return datasource; } }
lookupkey是通过determinetargetdatasource方法获取到的,而它是一个抽象方法,我们要做的就是通过实现这个方法,来控制获取到的数据源。
@nullable protected abstract object determinecurrentlookupkey();
(1) 创建并注入动态数据源
创建abstractroutingdatasource的子类,实现determinecurrentlookupkey方法
public class routingdatasource extends abstractroutingdatasource { @override protected object determinecurrentlookupkey() { return datasourcecontextholder.get(); } }
这里的datasourcecontextholder是一个操作threadlocal对象的工具类
public class datasourcecontextholder { /** * 数据源上下文 */ private static final threadlocal contextholder = new threadlocal<>(); /** * 设置数据源类型 */ public static void set(datasourcetype type) { contextholder.set(type); } /** * 获取数据源类型 * * @return datasourcetype */ public static datasourcetype get() { return contextholder.get(); } /** * 使用mysql数据源 */ public static void mysql() { set(datasourcetype.mysql); } /** * 使用postgresql数据源 */ public static void postgresql() { set(datasourcetype.postgresql); } public static void remove() { contextholder.remove(); } }
通过调用datasourcecontextholder.mysql()或者datasourcecontextholder.postgresql()就能修改contextholder的值,从而在动态数据源的determinetargetdatasource方法中就能获取到对应的数据源。
在数据源配置类中,将mysql和postgresql的数据源设置到动态数据源的map中,并注入容器。
@configuration public class datasourceconfigurer { @bean @configurationproperties(prefix = "spring.datasource.mysql") public datasource mysqldatasource() { return new druiddatasource(); } @bean @configurationproperties(prefix = "spring.datasource.postgresql") public datasource postgresqldatasource() { return new druiddatasource(); } @bean public routingdatasource routingdatasource(datasource mysqldatasource, datasource postgresqldatasource) { map datasources = new hashmap<>(); datasources.put(datasourcetype.mysql, mysqldatasource); datasources.put(datasourcetype.postgresql, postgresqldatasource); routingdatasource routingdatasource = new routingdatasource(); routingdatasource.setdefaulttargetdatasource(mysqldatasource); // 设置数据源 routingdatasource.settargetdatasources(datasources); return routingdatasource; } }
(2) mybatis配置类
由于使用了动态数据源,所以只需要编写一个配置类即可。
@configuration @mapperscan(basepackages = "com.example.mybatisdemo.mapper", sqlsessiontemplateref = "sqlsessiontemplate") public class mybatisconfigurer { // 注入动态数据源 @resource private routingdatasource routingdatasource; @bean public sqlsessionfactory sqlsessionfactory() throws exception { sqlsessionfactorybean sqlsessionfactorybean = new sqlsessionfactorybean(); sqlsessionfactorybean.setdatasource(routingdatasource); // 这里可以直接设置所有的mapper.xml文件 sqlsessionfactorybean.setmapperlocations(new pathmatchingresourcepatternresolver().getresources("classpath" + ":mappers/*.xml")); return sqlsessionfactorybean.getobject(); } @bean public sqlsessiontemplate sqlsessiontemplate(sqlsessionfactory sqlsessionfactory) { return new sqlsessiontemplate(sqlsessionfactory); } @bean public datasourcetransactionmanager transactionalmanager(datasource mysqldatasource) { return new datasourcetransactionmanager(mysqldatasource); } }
(3) 使用注解简化数据源切换
我们虽然可以使用datasourcecontextholder类中的方法进行动态数据源切换,但是这种方式有些繁琐,不够优雅。可以考虑使用注解的形式简化数据源切换。
我们先定义两个注解,表示使用mysql数据源或postgresql数据源:
@target({elementtype.type, elementtype.method}) @retention(retentionpolicy.runtime) public @interface mysql { }
@target({elementtype.type, elementtype.method}) @retention(retentionpolicy.runtime) public @interface postgresql { }
再定义一个切面,当使用了注解时,会先调用切换数据源的方法,再执行后续逻辑。
@component @aspect public class datasourceaspect { @pointcut("@within(com.example.mybatisdemo.aop.mysql) || @annotation(com.example.mybatisdemo.aop.mysql)") public void mysqlpointcut() { } @pointcut("@within(com.example.mybatisdemo.aop.postgresql) || @annotation(com.example.mybatisdemo.aop.postgresql)") public void postgresqlpointcut() { } @before("mysqlpointcut()") public void mysql() { datasourcecontextholder.mysql(); } @before("postgresqlpointcut()") public void postgresql() { datasourcecontextholder.postgresql(); } }
在使用动态数据源的事务操作时有两个需要注意的问题:
问题一 同一个事务操作两个数据源
mybatis使用executor执行sql时需要获取连接,baseexecutor类中的getconnection方法调用了springmanagedtransaction中的getconnection方法,这里优先从connection字段获取连接,如果connection为空,才会调用openconnection方法,并把连接赋给connection字段。
也就是说,如果你使用的是同一个事务来操作两个数据源,那拿到的都是同一个连接,会导致数据源切换失败。
protected connection getconnection(log statementlog) throws sqlexception { connection connection = this.transaction.getconnection(); return statementlog.isdebugenabled() ? connectionlogger.newinstance(connection, statementlog, this.querystack) : connection; }
public connection getconnection() throws sqlexception { if (this.connection == null) { this.openconnection(); } return this.connection; }
private void openconnection() throws sqlexception { this.connection = datasourceutils.getconnection(this.datasource); this.autocommit = this.connection.getautocommit(); this.isconnectiontransactional = datasourceutils.isconnectiontransactional(this.connection, this.datasource); logger.debug(() -> { return "jdbc connection [" + this.connection + "] will" + (this.isconnectiontransactional ? " " : " not ") + "be managed by spring"; }); }
问题二 两个独立事务分别操作两个数据源
(1) 在开启事务的时候,datasourcetransactionmanager中的dobegin方法会先获取connection,并保存到connectionholder中,将数据源和connectionholder的对应关系绑定到transactionsynchronizationmanager中。
protected void dobegin(object transaction, transactiondefinition definition) { datasourcetransactionmanager.datasourcetransactionobject txobject = (datasourcetransactionmanager.datasourcetransactionobject)transaction; connection con = null; try { if (!txobject.hasconnectionholder() || txobject.getconnectionholder().issynchronizedwithtransaction()) { // 获取连接 connection newcon = this.obtaindatasource().getconnection(); if (this.logger.isdebugenabled()) { this.logger.debug("acquired connection [" + newcon + "] for jdbc transaction"); } // 保存到connectionholder中 txobject.setconnectionholder(new connectionholder(newcon), true); } txobject.getconnectionholder().setsynchronizedwithtransaction(true); // 从connectionholder获取连接 con = txobject.getconnectionholder().getconnection(); // 略 // 将数据源和connectionholder的关系绑定到transactionsynchronizationmanager中 if (txobject.isnewconnectionholder()) { transactionsynchronizationmanager.bindresource(this.obtaindatasource(), txobject.getconnectionholder()); } // 略 }
(2) transactionsynchronizationmanager的bindresource方法将数据源和connectionholder的对应关系存入线程变量resources中。
public abstract class transactionsynchronizationmanager { // 线程变量 private static final threadlocal