在项目开发中,随着业务的扩展,api可能会操作多个数据库。本博文就学习下spring boot下使用spring-boot-starter-jta-atomikos对mybatis+mysql+PageHelper分页插件的整合。
项目文件结构
例子源码,已上传github
准备两个数据源数据库(如果只有一个数据源,那就新建2个数据库进行测试也是OK的)
pom.xml
org.springframework.boot
spring-boot-starter-parent
1.5.2.RELEASE
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
1.3.0
mysql
mysql-connector-java
org.springframework.boot
spring-boot-starter-jta-atomikos
com.github.pagehelper
pagehelper
4.1.6
准备两个数据源数据库(如果只有一个数据源,那就新建2个数据库进行测试也是OK的)
application.yml
logging:
config: classpath:logback.xml
path: d:/logs
server:
port: 80
session-timeout: 60
spring:
datasource:
db01:
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf-8
username: root
password: root
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
test.maxIdleTime: 60
testQuery: select 1
mapperLocations: classpath:/com/fei/springboot/dao/db01/*.xml
configLocation: classpath:/mybatis-config.xml
db02:
url: jdbc:mysql://192.168.0.213:3306/test?useUnicode=true&characterEncoding=utf-8
username: root
password: root
minPoolSize: 3
maxPoolSize: 25
maxLifetime: 20000
borrowConnectionTimeout: 30
loginTimeout: 30
maintenanceInterval: 60
test.maxIdleTime: 60
testQuery: select 1
mapperLocations: classpath:/com/fei/springboot/dao/db02/*.xml
configLocation: classpath:/mybatis-config.xml
mybatis-config.xml
TestDb01Config.java
package com.fei.springboot.config.dbconfig;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.github.pagehelper.PageHelper;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
/**
* db01 数据库配置
* @author Jfei
*
*/
@ConfigurationProperties(prefix="spring.datasource.db01")
@Configuration
@MapperScan(basePackages="com.fei.springboot.dao.db01", sqlSessionTemplateRef="db01SqlSessionTemplate")
public class TestDb01Config {
private Logger logger = LoggerFactory.getLogger(TestDb01Config.class);
private String url;
private String username;
private String password;
/** min-pool-size 最小连接数 **/
private int minPoolSize;
/** max-pool-size 最大连接数 **/
private int maxPoolSize;
/** max-lifetime 连接最大存活时间 **/
private int maxLifetime;
/** borrow-connection-timeout 获取连接失败重新获等待最大时间,在这个时间内如果有可用连接,将返回 **/
private int borrowConnectionTimeout;
/** login-timeout java数据库连接池,最大可等待获取datasouce的时间 **/
private int loginTimeout;
/** maintenance-interval 连接回收时间 **/
private int maintenanceInterval;
/** max-idle-time 最大闲置时间,超过最小连接池连接的连接将将关闭 **/
private int maxIdleTime;
/** test-query 测试SQL **/
private String testQuery;
// 配置mapper的扫描,找到所有的mapper.xml映射文件
private String mapperLocations;
// 加载全局的配置文件
private String configLocation;
// 配置数据源
@Primary
@Bean(name = "db01DataSource")
public DataSource db01DataSource() throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(url);
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(password);
mysqlXaDataSource.setUser(username);
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("db01DataSource");
xaDataSource.setMinPoolSize(minPoolSize);
xaDataSource.setMaxPoolSize(maxPoolSize);
xaDataSource.setMaxLifetime(maxLifetime);
xaDataSource.setBorrowConnectionTimeout(borrowConnectionTimeout);
xaDataSource.setLoginTimeout(loginTimeout);
xaDataSource.setMaintenanceInterval(maintenanceInterval);
xaDataSource.setMaxIdleTime(maxIdleTime);
xaDataSource.setTestQuery(testQuery);
return xaDataSource;
}
@Bean(name = "db01SqlSessionFactory")
public SqlSessionFactory db01SqlSessionFactory(@Qualifier("db01DataSource") DataSource dataSource)
throws Exception {
try {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
//设置mapper.xml文件所在位置
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);
sessionFactoryBean.setMapperLocations(resources);
//设置mybatis-config.xml配置文件位置
sessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
//添加分页插件、打印sql插件
Interceptor[] plugins = new Interceptor[]{pageHelper(),sqlPrintInterceptor()};
sessionFactoryBean.setPlugins(plugins);
return sessionFactoryBean.getObject();
} catch (IOException e) {
logger.error("mybatis resolver db01 mapper*xml is error",e);
throw e;
} catch (Exception e) {
logger.error("mybatis db01sqlSessionFactoryBean create error",e);
throw e;
}
}
@Bean(name = "db01SqlSessionTemplate")
public SqlSessionTemplate db01SqlSessionTemplate(
@Qualifier("db01SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 分页插件
* @param dataSource
* @return
*/
public PageHelper pageHelper() {
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
p.setProperty("offsetAsPageNum", "true");
p.setProperty("rowBoundsWithCount", "true");
p.setProperty("reasonable", "true");
p.setProperty("returnPageInfo", "check");
p.setProperty("params", "count=countSql");
pageHelper.setProperties(p);
return pageHelper;
}
//将要执行的sql进行日志打印(不想拦截,就把这方法注释掉)
public SqlPrintInterceptor sqlPrintInterceptor(){
return new SqlPrintInterceptor();
}
// --- get set 自行补充
}
第二个数据源
TestDb02Config.java
package com.fei.springboot.config.dbconfig;
import java.io.IOException;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import com.atomikos.jdbc.AtomikosDataSourceBean;
import com.github.pagehelper.PageHelper;
import com.mysql.jdbc.jdbc2.optional.MysqlXADataSource;
/**
* db02 数据库配置
* @author Jfei
*
*/
@ConfigurationProperties(prefix="spring.datasource.db02")
@Configuration
@MapperScan(basePackages="com.fei.springboot.dao.db02",sqlSessionTemplateRef="db02SqlSessionTemplate")
public class TestDb02Config {
private Logger logger = LoggerFactory.getLogger(TestDb02Config.class);
private String url;
private String username;
private String password;
/** min-pool-size 最小连接数 **/
private int minPoolSize;
/** max-pool-size 最大连接数 **/
private int maxPoolSize;
/** max-lifetime 连接最大存活时间 **/
private int maxLifetime;
/** borrow-connection-timeout 获取连接失败重新获等待最大时间,在这个时间内如果有可用连接,将返回 **/
private int borrowConnectionTimeout;
/** login-timeout java数据库连接池,最大可等待获取datasouce的时间 **/
private int loginTimeout;
/** maintenance-interval 连接回收时间 **/
private int maintenanceInterval;
/** max-idle-time 最大闲置时间,超过最小连接池连接的连接将将关闭 **/
private int maxIdleTime;
/** test-query 测试SQL **/
private String testQuery;
// 配置mapper的扫描,找到所有的mapper.xml映射文件
private String mapperLocations;
// 加载全局的配置文件
private String configLocation;
// 配置数据源
// @Primary //db01那边配置使用Primary了,这里不能再用了,否则报错
@Bean(name = "db02DataSource")
public DataSource db02DataSource() throws SQLException {
MysqlXADataSource mysqlXaDataSource = new MysqlXADataSource();
mysqlXaDataSource.setUrl(url);
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
mysqlXaDataSource.setPassword(password);
mysqlXaDataSource.setUser(username);
mysqlXaDataSource.setPinGlobalTxToPhysicalConnection(true);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(mysqlXaDataSource);
xaDataSource.setUniqueResourceName("db02DataSource");
xaDataSource.setMinPoolSize(minPoolSize);
xaDataSource.setMaxPoolSize(maxPoolSize);
xaDataSource.setMaxLifetime(maxLifetime);
xaDataSource.setBorrowConnectionTimeout(borrowConnectionTimeout);
xaDataSource.setLoginTimeout(loginTimeout);
xaDataSource.setMaintenanceInterval(maintenanceInterval);
xaDataSource.setMaxIdleTime(maxIdleTime);
xaDataSource.setTestQuery(testQuery);
return xaDataSource;
}
@Bean(name = "db02SqlSessionFactory")
public SqlSessionFactory db02SqlSessionFactory(@Qualifier("db02DataSource") DataSource dataSource)
throws Exception {
try {
SqlSessionFactoryBean sessionFactoryBean = new SqlSessionFactoryBean();
sessionFactoryBean.setDataSource(dataSource);
//设置mapper.xml文件所在位置
Resource[] resources = new PathMatchingResourcePatternResolver().getResources(mapperLocations);
sessionFactoryBean.setMapperLocations(resources);
//设置mybatis-config.xml配置文件位置
sessionFactoryBean.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
//添加分页插件、打印sql插件
Interceptor[] plugins = new Interceptor[]{pageHelper(),sqlPrintInterceptor()};
sessionFactoryBean.setPlugins(plugins);
return sessionFactoryBean.getObject();
} catch (IOException e) {
logger.error("mybatis resolver db02 mapper*xml is error",e);
throw e;
} catch (Exception e) {
logger.error("mybatis db02sqlSessionFactoryBean create error",e);
throw e;
}
}
@Bean(name = "db02SqlSessionTemplate")
public SqlSessionTemplate db02SqlSessionTemplate(
@Qualifier("db02SqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
/**
* 分页插件
* @param dataSource
* @return
*/
public PageHelper pageHelper() {
PageHelper pageHelper = new PageHelper();
Properties p = new Properties();
p.setProperty("offsetAsPageNum", "true");
p.setProperty("rowBoundsWithCount", "true");
p.setProperty("reasonable", "true");
p.setProperty("returnPageInfo", "check");
p.setProperty("params", "count=countSql");
pageHelper.setProperties(p);
return pageHelper;
}
//将要执行的sql进行日志打印(不想拦截,就把这方法注释掉)
public SqlPrintInterceptor sqlPrintInterceptor(){
return new SqlPrintInterceptor();
}
//-- get set 自行补充
}
@MapperScan(basePackages="com.fei.springboot.dao.db02",sqlSessionTemplateRef="db02SqlSessionTemplate")
要匹配,否则容易出错,不同数据源的mapper接口类和mapper.xml文件最好都分开
事务类的配置TransactionManagerConfig.java
package com.fei.springboot.config.dbconfig;
import javax.transaction.TransactionManager;
import javax.transaction.UserTransaction;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import org.springframework.transaction.jta.JtaTransactionManager;
import com.atomikos.icatch.jta.UserTransactionImp;
import com.atomikos.icatch.jta.UserTransactionManager;
/**
* 事务配置
* @author Jfei
*
*/
@Configuration
@EnableTransactionManagement
public class TransactionManagerConfig {
@Bean(name = "userTransaction")
public UserTransaction userTransaction() throws Throwable {
UserTransactionImp userTransactionImp = new UserTransactionImp();
userTransactionImp.setTransactionTimeout(10000);
return userTransactionImp;
}
@Bean(name = "atomikosTransactionManager", initMethod = "init", destroyMethod = "close")
public TransactionManager atomikosTransactionManager() throws Throwable {
UserTransactionManager userTransactionManager = new UserTransactionManager();
userTransactionManager.setForceShutdown(false);
return userTransactionManager;
}
@Bean(name = "transactionManager")
@DependsOn({ "userTransaction", "atomikosTransactionManager" })
public PlatformTransactionManager transactionManager() throws Throwable {
UserTransaction userTransaction = userTransaction();
JtaTransactionManager manager = new JtaTransactionManager(userTransaction, atomikosTransactionManager());
return manager;
}
}
sql打印的拦截器SqlPrintInterceptor.java
package com.fei.springboot.config.dbconfig;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.ibatis.executor.Executor;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.ParameterMode;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.session.Configuration;
import org.apache.ibatis.session.ResultHandler;
import org.apache.ibatis.session.RowBounds;
import org.apache.ibatis.type.TypeHandlerRegistry;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.regex.Matcher;
/**
* MyBatis 将mybatis要执行的sql拦截打印出来
*
* @since 1.0.0
*/
@Intercepts
({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
public class SqlPrintInterceptor implements Interceptor {
private static Log logger = LogFactory.getLog(SqlPrintInterceptor.class);
private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Override
public Object intercept(Invocation invocation) throws Throwable {
MappedStatement mappedStatement = (MappedStatement) invocation.getArgs()[0];
Object parameterObject = null;
if (invocation.getArgs().length > 1) {
parameterObject = invocation.getArgs()[1];
}
long start = System.currentTimeMillis();
Object result = invocation.proceed();
String statementId = mappedStatement.getId();
BoundSql boundSql = mappedStatement.getBoundSql(parameterObject);
Configuration configuration = mappedStatement.getConfiguration();
String sql = getSql(boundSql, parameterObject, configuration);
long end = System.currentTimeMillis();
long timing = end - start;
if(logger.isInfoEnabled()){
logger.info("执行sql耗时:" + timing + " ms" + " - id:" + statementId + " - Sql:" );
logger.info(" "+sql);
}
return result;
}
@Override
public Object plugin(Object target) {
if (target instanceof Executor) {
return Plugin.wrap(target, this);
}
return target;
}
@Override
public void setProperties(Properties properties) {
}
private String getSql(BoundSql boundSql, Object parameterObject, Configuration configuration) {
String sql = boundSql.getSql().replaceAll("[\\s]+", " ");
List parameterMappings = boundSql.getParameterMappings();
TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry();
if (parameterMappings != null) {
for (int i = 0; i < parameterMappings.size(); i++) {
ParameterMapping parameterMapping = parameterMappings.get(i);
if (parameterMapping.getMode() != ParameterMode.OUT) {
Object value;
String propertyName = parameterMapping.getProperty();
if (boundSql.hasAdditionalParameter(propertyName)) {
value = boundSql.getAdditionalParameter(propertyName);
} else if (parameterObject == null) {
value = null;
} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
value = parameterObject;
} else {
MetaObject metaObject = configuration.newMetaObject(parameterObject);
value = metaObject.getValue(propertyName);
}
sql = replacePlaceholder(sql, value);
}
}
}
return sql;
}
private String replacePlaceholder(String sql, Object propertyValue) {
String result;
if (propertyValue != null) {
if (propertyValue instanceof String) {
result = "'" + propertyValue + "'";
} else if (propertyValue instanceof Date) {
result = "'" + DATE_FORMAT.format(propertyValue) + "'";
} else {
result = propertyValue.toString();
}
} else {
result = "null";
}
return sql.replaceFirst("\\?", Matcher.quoteReplacement(result));
}
}
db01的
package com.fei.springboot.dao.db01;
import java.util.List;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.fei.springboot.domain.User;
@Mapper
public interface UserMapper {
@Insert("insert sys_user(id,user_name) values(#{id},#{userName})")
void insert(User u);
//注:方法名和要UserMapper.xml中的id一致
List query(@Param("userName")String userName);
}
UserMapper.xml
Db02UserMapper.java
package com.fei.springboot.dao.db02;
import java.util.List;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import com.fei.springboot.domain.User;
@Mapper
public interface Db02UserMapper {
@Insert("insert sys_user(id,user_name) values(#{id},#{userName})")
void insert(User u);
//注:方法名和要UserMapper.xml中的id一致
List query(@Param("userName")String userName);
}
Db02UserMapper.xml
package com.fei.springboot.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.fei.springboot.dao.db01.UserMapper;
import com.fei.springboot.dao.db02.Db02UserMapper;
import com.fei.springboot.domain.User;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
@Service
@Transactional(propagation=Propagation.REQUIRED,readOnly=true,rollbackFor=Exception.class)
public class UserService {
@Autowired
private UserMapper userMapper;
@Autowired
private Db02UserMapper db02UserMapper;
//注意:方法的@Transactional会覆盖类上面声明的事务
//Propagation.REQUIRED :有事务就处于当前事务中,没事务就创建一个事务
//isolation=Isolation.DEFAULT:事务数据库的默认隔离级别
@Transactional(propagation=Propagation.REQUIRED,isolation=Isolation.DEFAULT,readOnly=false)
public void insertUser(User u){
this.userMapper.insert(u);
this.db02UserMapper.insert(u);
//如果类上面没有@Transactional,方法上也没有,哪怕throw new RuntimeException,数据库也会成功插入数据
// throw new RuntimeException("测试插入事务");
}
public PageInfo queryPage(String userName,int pageNum,int pageSize){
Page page = PageHelper.startPage(pageNum, pageSize);
//PageHelper会自动拦截到下面这查询sql
this.userMapper.query(userName);
return page.toPageInfo();
}
}
UserController.java
package com.fei.springboot.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import com.fei.springboot.domain.User;
import com.fei.springboot.service.UserService;
import com.github.pagehelper.PageInfo;
@Controller
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/hello")
@ResponseBody
public String hello(){
return "hello";
}
/**
* 测试插入
* @return
*/
@RequestMapping("/add")
@ResponseBody
public String add(String id,String userName){
User u = new User();
u.setId(id);
u.setUserName(userName);
this.userService.insertUser(u);
return u.getId()+" " + u.getUserName();
}
/**
* 测试分页插件
* @return
*/
@RequestMapping("/queryPage")
@ResponseBody
public String queryPage(){
PageInfo page = this.userService.queryPage("tes", 1, 2);
System.out.println("总页数=" + page.getPages());
System.out.println("总记录数=" + page.getTotal()) ;
for(User u : page.getList()){
System.out.println(u.getId() + " \t " + u.getUserName());
}
return "success";
}
}
package com.fei.springboot;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.embedded.ConfigurableEmbeddedServletContainer;
import org.springframework.boot.context.embedded.EmbeddedServletContainerCustomizer;
import org.springframework.boot.web.support.SpringBootServletInitializer;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan(basePackages={"com.fei.springboot"})
@SpringBootApplication
public class Application extends SpringBootServletInitializer implements EmbeddedServletContainerCustomizer{
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Application.class);
}
public static void main(String[] args) throws Exception {
SpringApplication.run(Application.class, args);
}
public void customize(ConfigurableEmbeddedServletContainer configurableEmbeddedServletContainer) {
// configurableEmbeddedServletContainer.setPort(9090);
}
}
到2个数据库中查看,会发现都有数据了。
测试事务,把UserService类中的insertUser方法中的异常注释去掉,然后在浏览器执行
http://127.0.0.1/user/add?id=789&userName=test789
发现抛出了异常,2个数据库中都没数据
注意:由于事务JtaTransactionManager,是二阶提交,有个缺点就是,第一阶段预提交时候发现2个数据库都没问题,但是第2阶段正真提交时候,第一个数据库提交完成,第二个数据库提交的时候失败了(比如刚好宕机了),抛出了异常,但是第一个数据库没法回滚了,所以可以说产生脏数据了。
完整例子源码,已上传github。