Mybatis操作多数据源实现的方法

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                
相关文章