前段时间在做会员中心和中间件零碎开发时,屡次碰到多数据源配置问题,次要用到分包形式、参数化切换、注解+AOP、动静增加 这四种形式。这里做一下总结,分享下应用心得以及踩过的坑。

分包形式

数据源配置文件

在yml中,配置两个数据源,id别离为master和s1。

spring:  datasource:    master:      jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db1?......... username: xxx password: xxx driverClassName: com.mysql.cj.jdbc.Driver s1: jdbcUrl: jdbc:mysql://192.168.xxx.xxx:xxxx/db2?........ username: xxx password: xxx driverClassName: com.mysql.cj.jdbc.Driver

数据源配置类

master数据源配置类

留神点:

须要用@Primary注解指定默认数据源,否则spring不晓得哪个是主数据源;

@Configuration@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.master", sqlSessionFactoryRef = "masterSqlSessionFactory")public class MasterDataSourceConfig {    //默认数据源    @Bean(name = "masterDataSource")    @Primary    @ConfigurationProperties(prefix = "spring.datasource.master")    public HikariDataSource masterDataSource() {        return DataSourceBuilder.create().type(HikariDataSource.class).build();    }    @Bean(name = "masterSqlSessionFactory")    @Primary    public SqlSessionFactory masterSqlSessionFactory(@Qualifier("masterDataSource") DataSource datasource, PaginationInterceptor paginationInterceptor)            throws Exception {        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();        bean.setDataSource(datasource);        bean.setMapperLocations(                // 设置mybatis的xml所在位置                new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/master/**/**.xml"));        bean.setPlugins(new Interceptor[]{paginationInterceptor});        return bean.getObject();    }    @Bean(name = "masterSqlSessionTemplate")    @Primary    public SqlSessionTemplate masterSqlSessionTemplate(            @Qualifier("masterSqlSessionFactory") SqlSessionFactory sessionfactory) {        return new SqlSessionTemplate(sessionfactory);    }}

s1数据源配置类

@Configuration@MapperScan(basePackages = "com.hosjoy.xxx.xxx.xxx.xxx.mapper.s1", sqlSessionFactoryRef = "s1SqlSessionFactory")public class S1DataSourceConfig {    @Bean(name = "s1DataSource")    @ConfigurationProperties(prefix = "spring.datasource.s1")    public HikariDataSource s1DataSource() {        return DataSourceBuilder.create().type(HikariDataSource.class).build();    }    @Bean(name = "s1SqlSessionFactory")    public SqlSessionFactory s1SqlSessionFactory(@Qualifier("s1DataSource") DataSource datasource            , PaginationInterceptor paginationInterceptor)            throws Exception {        MybatisSqlSessionFactoryBean bean = new MybatisSqlSessionFactoryBean();        bean.setDataSource(datasource);        bean.setMapperLocations(                // 设置mybatis的xml所在位置 new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/s1/**/**.xml")); bean.setPlugins(new Interceptor[]{paginationInterceptor}); return bean.getObject(); } @Bean(name = "s1SqlSessionTemplate") public SqlSessionTemplate s1SqlSessionTemplate( @Qualifier("s1SqlSessionFactory") SqlSessionFactory sessionfactory) { return new SqlSessionTemplate(sessionfactory); }}

应用

能够看出,mapper接口、xml文件,须要依照不同的数据源分包。在操作数据库时,依据须要在service类中注入dao层。

特点剖析

长处

实现起来简略,只须要编写数据源配置文件和配置类,mapper接口和xml文件留神分包即可。

毛病

很显著,如果前面要减少或删除数据源,不仅要批改数据源配置文件,还须要批改配置类。

例如减少一个数据源,同时还须要新写一个该数据源的配置类,同时还要思考新建mapper接口包、xml包等,没有实现 “热插拔” 成果。

参数化切换形式

思维

参数化切换数据源,意思是,业务侧须要依据以后业务参数,动静的切换到不同的数据源。

这与分包思维不同。分包的前提是在编写代码的时候,就曾经晓得以后须要用哪个数据源,而参数化切换数据源须要依据业务参数决定用哪个数据源。

例如,申请参数userType值为1时,须要切换到数据源slave1;申请参数userType值为2时,须要切换到数据源slave2。

/**伪代码**/int userType = reqUser.getType();if (userType == 1){ //切换到数据源slave1 //数据库操作} else if(userType == 2){ //切换到数据源slave2 //数据库操作}

设计思路

数据源注册

数据源配置类创立datasource时,从yml配置文件中读取所有数据源配置,主动创立每个数据源,并注册至bean工厂和AbstractRoutingDatasource(前面聊聊这个),同时返回默认的数据源master。

数据源切换

(1)通过线程池解决申请,每个申请独占一个线程,这样每个线程切换数据源时互不影响。

(2)依据业务参数获取应切换的数据源ID,依据ID从数据源缓存池获取数据源bean;

(3)生成以后线程数据源key;

(4)将key设置到threadLocal;

(5)将key和数据源bean放入数据源缓存池;

(6)在执行mapper办法前,spring会调用determineCurrentLookupKey办法获取key,而后依据key去数据源缓存池取出数据源,而后getConnection获取该数据源连贯;

(7)应用该数据源执行数据库操作;

(8)开释以后线程数据源。

AbstractRoutingDataSource源码剖析

spring为咱们提供了AbstractRoutingDataSource抽象类,该类就是实现动静切换数据源的要害。

咱们看下该类的类图,其实现了DataSource接口。

咱们看下它的getConnection办法的逻辑,其首先调用determineTargetDataSource来获取数据源,再获取数据库连贯。很容易猜想到就是这里来决定具体应用哪个数据源的。

进入到determineTargetDataSource办法,咱们能够看到它先是调用determineCurrentLookupKey获取到一个lookupKey,而后依据这个key去resolvedDataSources里去找相应的数据源。

看下该类定义的几个对象,defaultTargetDataSource是默认数据源,resolvedDataSources是一个map对象,存储所有主从数据源。

所以,要害就是这个lookupKey的获取逻辑,决定了以后获取的是哪个数据源,而后执行getConnection等一系列操作。determineCurrentLookupKey是AbstractRoutingDataSource类中的一个形象办法,而它的返回值是你所要用的数据源dataSource的key值,有了这个key值,resolvedDataSource(这是个map,由配置文件中设置好后存入的)就从中取出对应的DataSource,如果找不到,就用配置默认的数据源。

所以,通过扩大AbstractRoutingDataSource类,并重写其中的determineCurrentLookupKey()办法,能够实现数据源的切换。

代码实现

上面贴出要害代码实现。

数据源配置文件

这里配了3个数据源,其中主数据源是MySQL,两个从数据源是sqlserver。

spring:  datasource:    master:      jdbcUrl: jdbc:mysql://192.168.xx.xxx:xxx/db1?........ username: xxx password: xxx driverClassName: com.mysql.cj.jdbc.Driver slave1: jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db2 username: xxx password: xxx driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver slave2: jdbcUrl: jdbc:sqlserver://192.168.xx.xxx:xxx;DatabaseName=db3 username: xxx password: xxx driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver

定义动静数据源

次要是继承AbstractRoutingDataSource,实现determineCurrentLookupKey办法。

public class DynamicDataSource extends AbstractRoutingDataSource {    /*存储所有数据源*/    private Map<Object, Object> backupTargetDataSources;    public Map<Object, Object> getBackupTargetDataSources() {        return backupTargetDataSources;    }    /*defaultDataSource为默认数据源*/    public DynamicDataSource(DataSource defaultDataSource, Map<Object, Object> targetDataSource) {        backupTargetDataSources = targetDataSource;        super.setDefaultTargetDataSource(defaultDataSource);        super.setTargetDataSources(backupTargetDataSources);        super.afterPropertiesSet();    }    public void addDataSource(String key, DataSource dataSource) {        this.backupTargetDataSources.put(key, dataSource);        super.setTargetDataSources(this.backupTargetDataSources);        super.afterPropertiesSet();    }    /*返回以后线程的数据源的key*/    @Override    protected Object determineCurrentLookupKey() {        return DynamicDataSourceContextHolder.getContextKey();    }}

定义数据源key线程变量持有

定义一个ThreadLocal动态变量,该变量持有了线程和线程的数据源key之间的关系。当咱们要切换数据源时,首先要本人生成一个key,将这个key存入threadLocal线程变量中;同时还应该从DynamicDataSource对象中的backupTargetDataSources属性中获取到数据源对象, 而后将key和数据源对象再put到backupTargetDataSources中。 这样,spring就能依据determineCurrentLookupKey办法返回的key,从backupTargetDataSources中取出咱们刚刚设置的数据源对象,进行getConnection等一系列操作了。

public class DynamicDataSourceContextHolder {    /** * 存储线程和数据源key的映射关系 */    private static final ThreadLocal<String> DATASOURCE_CONTEXT_KEY_HOLDER = new ThreadLocal<>();    /*** * 设置以后线程数据源key */    public static void setContextKey(String key) {        DATASOURCE_CONTEXT_KEY_HOLDER.set(key);    }    /*** * 获取以后线程数据源key */    public static String getContextKey() {        String key = DATASOURCE_CONTEXT_KEY_HOLDER.get();        return key == null ? DataSourceConstants.DS_KEY_MASTER : key;    }    /*** * 删除以后线程数据源key */    public static void removeContextKey() {        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class);        String currentKey = DATASOURCE_CONTEXT_KEY_HOLDER.get();        if (StringUtils.isNotBlank(currentKey) && !"master".equals(currentKey)) {            dynamicDataSource.getBackupTargetDataSources().remove(currentKey);        }        DATASOURCE_CONTEXT_KEY_HOLDER.remove();    }}

多数据源主动配置类

这里通过读取yml配置文件中所有数据源的配置,主动为每个数据源创立datasource 对象并注册至bean工厂。同时将这些数据源对象,设置到AbstractRoutingDataSource中。

通过这种形式,前面如果须要增加或批改数据源,都无需新增或批改java配置类,只需去配置核心批改yml文件即可。

@Configuration@MapperScan(basePackages = "com.hosjoy.xxx.xxx.modules.xxx.mapper")public class DynamicDataSourceConfig {    @Autowired    private BeanFactory beanFactory;    @Autowired    private DynamicDataSourceProperty dynamicDataSourceProperty;    /** * 性能形容: <br> * 〈动静数据源bean 主动配置注册所有数据源〉 * * @param * @return javax.sql.DataSource * @Author li.he * @Date 2020/6/4 16:47 * @Modifier */    @Bean    @Primary    public DataSource dynamicDataSource() {        DefaultListableBeanFactory listableBeanFactory = (DefaultListableBeanFactory) beanFactory;        /*获取yml所有数据源配置*/        Map<String, Object> datasource = dynamicDataSourceProperty.getDatasource();        Map<Object, Object> dataSourceMap = new HashMap<>(5);        Optional.ofNullable(datasource).ifPresent(map -> {            for (Map.Entry<String, Object> entry : map.entrySet()) {                //创立数据源对象 HikariDataSource dataSource = (HikariDataSource) DataSourceBuilder.create().build(); String dataSourceId = entry.getKey(); configeDataSource(entry, dataSource); /*bean工厂注册每个数据源bean*/ listableBeanFactory.registerSingleton(dataSourceId, dataSource); dataSourceMap.put(dataSourceId, dataSource); } }); //AbstractRoutingDataSource设置主从数据源 return new DynamicDataSource(beanFactory.getBean("master", DataSource.class),          dataSourceMap); } private void configeDataSource(Map.Entry<String, Object> entry, HikariDataSource dataSource) { Map<String, Object> dataSourceConfig = (Map<String, Object>) entry.getValue(); dataSource.setJdbcUrl(MapUtils.getString(dataSourceConfig, "jdbcUrl")); dataSource.setDriverClassName(MapUtils.getString(dataSourceConfig, "driverClassName")); dataSource.setUsername(MapUtils.getString(dataSourceConfig, "username")); dataSource.setPassword(MapUtils.getString(dataSourceConfig, "password")); }}

数据源切换工具类

切换逻辑:

(1)生成以后线程数据源key

(2)依据业务条件,获取应切换的数据源ID;

(3)依据ID从数据源缓存池中获取数据源对象,并再次增加到backupTargetDataSources缓存池中;

(4)threadLocal设置以后线程对应的数据源key;

(5)在执行数据库操作前,spring会调用determineCurrentLookupKey办法获取key,而后依据key去数据源缓存池取出数据源,而后getConnection获取该数据源连贯;

(6)应用该数据源执行数据库操作;

(7)开释缓存:threadLocal清理以后线程数据源信息、数据源缓存池清理以后线程数据源key和数据源对象,目标是避免内存透露。

@Slf4j@Componentpublic class DataSourceUtil {    @Autowired    private DataSourceConfiger dataSourceConfiger;        /*依据业务条件切换数据源*/    public void switchDataSource(String key, Predicate<? super Map<String, Object>> predicate) {        try {            //生成以后线程数据源key String newDsKey = System.currentTimeMillis() + ""; List<Map<String, Object>> configValues = dataSourceConfiger.getConfigValues(key); Map<String, Object> db = configValues.stream().filter(predicate) .findFirst().get(); String id = MapUtils.getString(db, "id"); //依据ID从数据源缓存池中获取数据源对象,并再次增加到backupTargetDataSources addDataSource(newDsKey, id); //设置以后线程对应的数据源key DynamicDataSourceContextHolder.setContextKey(newDsKey); log.info("以后线程数据源切换胜利,以后数据源ID:{}", id); } catch (Exception e) { log.error("切换数据源失败,请查看数据源配置文件。key:{}, 条件:{}", key, predicate.toString()); throw new ClientException("切换数据源失败,请查看数据源配置", e); } }  /*将数据源增加至少数据源缓存池中*/ public static void addDataSource(String key, String dataSourceId) { DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean(DynamicDataSource.class); DataSource dataSource = (DataSource) dynamicDataSource.getBackupTargetDataSources().get(dataSourceId); dynamicDataSource.addDataSource(key, dataSource); }}

应用

public void doExecute(ReqTestParams reqTestParams){    //结构条件    Predicate<? super Map<String, Object>> predicate =.........;    //切换数据源    dataSourceUtil.switchDataSource("testKey", predicate);    //数据库操作    mapper.testQuery();    //清理缓存,防止内存透露    DynamicDataSourceContextHolder.removeContextKey();}

每次数据源应用后,都要调用removeContextKey办法清理缓存,防止内存透露,这里能够思考用AOP拦挡特定办法,利用后置告诉为执行办法代理执行缓存清理工作。

@Aspect@Component@Slf4jpublic class RequestHandleMethodAspect {    @After("xxxxxxxxxxxxxxExecution表达式xxxxxxxxxxxxxxxxxx")    public void afterRunning(JoinPoint joinPoint){        String name = joinPoint.getSignature().toString();        long id = Thread.currentThread().getId();        log.info("办法执行结束,开始清空以后线程数据源,线程id:{},代理办法:{}",id,name);        DynamicDataSourceContextHolder.removeContextKey();        log.info("以后线程数据源清空结束,已返回至默认数据源:{}",id);    }}

特点剖析

(1)参数化切换数据源形式,出发点和分包形式不一样,适宜于在运行时能力确定用哪个数据源。

(2)须要手动执行切换数据源操作;

(3)无需分包,mapper和xml门路自在定义;

(4)减少数据源,无需批改java配置类,只需批改数据源配置文件即可。

注解形式

思维

该形式利用注解+AOP思维,为须要切换数据源的办法标记自定义注解,注解属性指定数据源ID,而后利用AOP切面拦挡注解标记的办法,在办法执行前,切换至相应数据源;在办法执行完结后,切换至默认数据源。

须要留神的是,自定义切面的优先级须要高于@Transactional注解对应切面的优先级。

否则,在自定义注解和@Transactional同时应用时,@Transactional切面会优先执行,切面在调用getConnection办法时,会去调用AbstractRoutingDataSource.determineCurrentLookupKey办法,此时获取到的是默认数据源master。这时@UsingDataSource对应的切面即便再设置以后线程的数据源key,前面也不会再去调用determineCurrentLookupKey办法来切换数据源了。

设计思路

数据源注册

同上。

数据源切换

利用切面,拦挡所有@UsingDataSource注解标记的办法,依据dataSourceId属性,在办法执行前,切换至相应数据源;在办法执行完结后,清理缓存并切换至默认数据源。

代码实现

数据源配置文件

同上。

定义动静数据源

同上。

定义数据源key线程变量持有

同上。

多数据源主动配置类

同上。

数据源切换工具类

切换逻辑:

(1)生成以后线程数据源key

(3)依据ID从数据源缓存池中获取数据源对象,并再次增加到backupTargetDataSources缓存池中;

(4)threadLocal设置以后线程对应的数据源key;

(5)在执行数据库操作前,spring会调用determineCurrentLookupKey办法获取key,而后依据key去数据源缓存池取出数据源,而后getConnection获取该数据源连贯;

(6)应用该数据源执行数据库操作;

(7)开释缓存:threadLocal清理以后线程数据源信息、数据源缓存池清理以后线程数据源key和数据源对象。

public static void switchDataSource(String dataSourceId) {    if (StringUtils.isBlank(dataSourceId)) {        throw new ClientException("切换数据源失败,数据源ID不能为空");    }    try {        String threadDataSourceKey = UUID.randomUUID().toString();        DataSourceUtil.addDataSource(threadDataSourceKey, dataSourceId);        DynamicDataSourceContextHolder.setContextKey(threadDataSourceKey);    }    catch (Exception e) {        log.error("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:{}", dataSourceId);        throw new ClientException("切换数据源失败,未找到指定的数据源,请确保所指定的数据源ID已在配置文件中配置。dataSourceId:" + dataSourceId, e);    }}

自定义注解

自定义注解标记以后办法所应用的数据源,默认为主数据源。

@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface UsingDataSource {    String dataSourceId() default "master";}

切面

次要是定义前置告诉和后置告诉,拦挡UsingDataSource注解标记的办法,办法执行前切换数据源,办法执行后清理数据源缓存。

须要标记切面的优先级比@Transaction注解对应切面的优先级要高。否则,在自定义注解和@Transactional同时应用时,@Transactional切面会优先执行,切面在调用getConnection办法时,会去调用AbstractRoutingDataSource.determineCurrentLookupKey办法,此时获取到的是默认数据源master。这时@UsingDataSource对应的切面即便再设置以后线程的数据源key,前面也不会再去调用determineCurrentLookupKey办法来切换数据源了。

@Aspect@Component@Slf4j@Order(value = 1)public class DynamicDataSourceAspect {    //拦挡UsingDataSource注解标记的办法,办法执行前切换数据源 @Before(value = "@annotation(usingDataSource)") public void before(JoinPoint joinPoint, UsingDataSource usingDataSource) { String dataSourceId = usingDataSource.dataSourceId(); log.info("执行指标办法前开始切换数据源,指标办法:{}, dataSourceId:{}", joinPoint.getSignature().toString(), dataSourceId); try { DataSourceUtil.switchDataSource(dataSourceId); } catch (Exception e) { log.error("切换数据源失败!数据源可能未配置或不可用,数据源ID:{}", dataSourceId, e); throw new ClientException("切换数据源失败!数据源可能未配置或不可用,数据源ID:" + dataSourceId, e); } log.info("指标办法:{} , 已切换至数据源:{}", joinPoint.getSignature().toString(), dataSourceId); } //拦挡UsingDataSource注解标记的办法,办法执行后清理数据源,避免内存透露 @After(value = "@annotation(com.hosjoy.hbp.dts.common.annotation.UsingDataSource)") public void after(JoinPoint joinPoint) { log.info("指标办法执行结束,执行清理,切换至默认数据源,指标办法:{}", joinPoint.getSignature().toString()); try { DynamicDataSourceContextHolder.removeContextKey(); } catch (Exception e) { log.error("清理数据源失败", e); throw new ClientException("清理数据源失败", e); } log.info("指标办法:{} , 数据源清理结束,已返回默认数据源", joinPoint.getSignature().toString()); }}

应用

@UsingDataSource(dataSourceId = "slave1")@Transactionalpublic void test(){    AddressPo po = new AddressPo();    po.setMemberCode("asldgjlk");    po.setName("lihe");    po.setPhone("13544986666");    po.setProvince("asdgjwlkgj");    addressMapper.insert(po);    int i = 1 / 0;}

动静增加形式(十分用)

业务场景形容

这种业务场景不是很常见,但必定是有人遇到过的。

我的项目外面只配置了1个默认的数据源,而具体运行时须要动静的增加新的数据源,非已配置好的动态的多数据源。例如须要去服务器实时读取数据源配置信息(非配置在本地),而后再执行数据库操作。

这种业务场景,以上3种形式就都不实用了,因为上述的数据源都是提前在yml文件配置好的。

实现思路

除了第6步外,利用之前写好的代码就能够实现。

思路是:

(1)创立新数据源;

(2)DynamicDataSource注册新数据源;

(3)切换:设置以后线程数据源key;增加长期数据源;

(4)数据库操作(必须在另一个service实现,否则无法控制事务);

(5)清理以后线程数据源key、清理长期数据源;

(6)清理刚刚注册的数据源;

(7)此时已返回至默认数据源。

代码

代码写的比拟简陋,然而模板大略就是这样子,次要想表白实现的形式。

Service A:

public String testUsingNewDataSource(){        DynamicDataSource dynamicDataSource = RequestHandleMethodRegistry.getContext().getBean("dynamicDataSource", DynamicDataSource.class);        try {            //模仿从服务器读取数据源信息            //.......................... //....................  //创立新数据源 HikariDataSource dataSource = (HikariDataSource)                   DataSourceBuilder.create().build(); dataSource.setJdbcUrl("jdbc:mysql://192.168.xxx.xxx:xxxx/xxxxx?......"); dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver"); dataSource.setUsername("xxx"); dataSource.setPassword("xxx");  //DynamicDataSource注册新数据源 dynamicDataSource.addDataSource("test_ds_id", dataSource); //设置以后线程数据源key、增加长期数据源 DataSourceUtil.switchDataSource("test_ds_id"); //数据库操作(必须在另一个service实现,否则无法控制事务) serviceB.testInsert(); } finally { //清理以后线程数据源key DynamicDataSourceContextHolder.removeContextKey(); //清理刚刚注册的数据源 dynamicDataSource.removeDataSource("test_ds_id"); } return "aa"; }

Service B:

@Transactional(rollbackFor = Exception.class)    public void testInsert() {        AddressPo po = new AddressPo();        po.setMemberCode("555555555");        po.setName("李郃");        po.setPhone("16651694996");        po.setProvince("江苏省");        po.setCity("南京市");        po.setArea("浦口区");        po.setAddress("南京市浦口区宁六路219号");        po.setDef(false);        po.setCreateBy("23958");        addressMapper.insert(po);        //测试事务回滚 int i = 1 / 0; }

DynamicDataSource: 减少removeDataSource办法, 清理注册的新数据源。

public class DynamicDataSource extends AbstractRoutingDataSource {                .................            .................            .................    public void removeDataSource(String key){        this.backupTargetDataSources.remove(key);        super.setTargetDataSources(this.backupTargetDataSources);        super.afterPropertiesSet();    }                .................            .................            .................}

四种形式比照

事务问题

应用上述数据源配置形式,可实现单个数据源事务管制。

例如在一个service办法中,须要操作多个数据源执行CUD时,是能够实现单个数据源事务管制的。形式如下,别离将须要事务管制的办法独自抽取到另一个service,可实现单个事务办法的事务管制。

ServiceA:

public void updateMuilty(){     serviceB.updateDb1();     serviceB.updateDb2();}

ServiceB:

@UsingDataSource(dataSourceId = "slave1")@Transactionalpublic void updateDb1(){    //业务逻辑......}@UsingDataSource(dataSourceId = "slave2")@Transactionalpublic void updateDb2(){ //业务逻辑......}

然而在同一个办法里管制多个数据源的事务就不是这么简略了,这就属于分布式事务的范畴,能够思考应用atomikos开源我的项目实现JTA分布式事务处理或者阿里的Fescar框架。

因为波及到分布式事务管制,实现比较复杂,这里只是引出这个问题,前面抽时间把这块补上来。