演示源码github:链接
1. 本文适合场景
本文事务解决方案只适用于单体架构项目多数据源的场景,就是同一进程中多个数据源要想解决事务问题可以使用本方案。
1.1 场景介绍
比如说我们现在做一个出行项目,类似滴滴出行这样子的,然后呢项目是个单体架构,但是数据却存在了不同的库中了,比如说与司机有关的表都放在了driver_db 库中了,与订单有关的表都存放在了order_db 库中了,项目是使用配置多数据源的形式来操作数据库的。
现在,有个用户a下了一个用车订单,然后系统在order_db 库中的order_info 表中插入一条订单信息,下图:
id_ 是我们的订单编号,driver_id 是接单司机,create_time 就是咱们的创建时间,剩下的字段都省略掉…
接着driver_id为1的司机抢到了这个订单,这时候,需要在司机表中有个当前订单字段中填入 订单号,在订单信息中填入接单司机。
我们来看看这个司机表
id_是司机id,dirver_name 这个这里用不到,curr_order_id 是当前接到的订单,create_time司机注册时间。
这里咱们有这么一个业务要求就是: 司机接到订单的时候,需要订单信息要添加driver_id,司机信息中的curr_order_id要添加订单编号。
这两步要同时执行,保持一致性,要么都成功,要么都失败,否则会对影响业务,比如说driver_id 为1的司机抢到为1的订单,然后只将订单编号更新到driver_info 表的curr_order_id 了,没有将司机信息添加到订单信息中,这时候,系统认为该订单没有派出去,然后又继续通知司机们抢单,会造成一个订单被多个司机师傅接到。相反只将司机信息更新到了订单表中,然后司机表中的curr_order_id没有订单号,这时候司机师傅就不知道自己接到了订单,还会去抢别的订单,造成一个司机同时多个订单。
1.2 场景代码模拟
这里我们就用代码模拟下1.1 节介绍的内容,这里我们使用单体架构多数据源的形式,springboot+mybatis+druid
1.2.1 数据库
这里为了模拟多数据源,我们在同一个实例上面创建了两个db,一个是dirver_db 放的是司机有关的,一个是order_db放的是订单有关的。
我们需要在这两个库中分别创建driver_info 与order_info 表
司机表:
CREATE TABLE `driver_info` (
`id_` int(11) NOT NULL AUTO_INCREMENT COMMENT '司机id',
`driver_name` varchar(20) DEFAULT NULL COMMENT '司机姓名',
`curr_order_id` int(11) DEFAULT NULL COMMENT '当前接到的订单',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id_`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
插入编号1的司机:
insert into driver_info (id_,driver_name,create_time) values(1,'driver01','2020-07-13 00:00:00')
订单表:
CREATE TABLE `order_info` (
`id_` int(11) NOT NULL AUTO_INCREMENT COMMENT '订单id\n',
`driver_id` int(11) DEFAULT NULL COMMENT '接单司机id',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
PRIMARY KEY (`id_`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
创建订单号为1的订单:
insert into order_info (id_,create_time) values(1,'2020-07-13 23:00:00')
1.2.2 单体多数据源项目
1.2.2.1 pom
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.1.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<!--web starter-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
</dependency>
<!--druid 数据源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
</dependency>
<!--mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.1.1</version>
</dependency>
<!--mysql 驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>6.0.6</version>
</dependency>
<!--单元测试-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
1.2.2.2 配置文件application
我们需要在配置文件application.yml中添加多数据源
# 配置多数据源
spring:
datasource:
driver:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.3.36:3306/driver_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
order:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
jdbc-url: jdbc:mysql://192.168.3.36:3306/order_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
application:
name: multi-datasource-distributed-transaction
server:
port: 8080
1.2.2.3 主启动类
这里就是普通的springboot主启动类
1.2.2.4 多数据源配置类
- DriverDatasourceConfiguration
@Configuration
@MapperScan(basePackages = {"com.xuzhaocai.multi.dao.driver"}, sqlSessionTemplateRef = "driverSqlSessionTemplate")
public class DriverDatasourceConfiguration {
@Bean(name = "driverDatasource")
@ConfigurationProperties(prefix = "spring.datasource.driver")
public DataSource driverDruidDatasource() {
return DataSourceBuilder.create().build();
//return xaDataSource;
}
//配置数据源
@Bean(name = "driverSqlSessionFactory")
public SqlSessionFactory driverSqlSessionFactory(@Qualifier("driverDatasource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
//mapper.xml 的位置
Resource[] resources1 = pathMatchingResourcePatternResolver.getResources("classpath*:com/xuzhaocai/multi/dao/driver/*.xml");
factoryBean.setMapperLocations(resources1);
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
// 事务管理器
@Bean(name = "driverTransactionManger")
public DataSourceTransactionManager driverTransactionManger(@Qualifier("driverDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "driverSqlSessionTemplate")
public SqlSessionTemplate driverSqlSessionTemplate(@Qualifier("driverSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
- OrderDatasourceConfiguration
@Configuration
@MapperScan(basePackages = {"com.xuzhaocai.multi.dao.order"}, sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class OrderDatasourceConfiguration {
@Bean(name = "orderDatasource")
@ConfigurationProperties(prefix = "spring.datasource.order")
@Primary
public DataSource orderDatasource() {
return DataSourceBuilder.create().build();
}
//配置数据源
@Bean(name = "orderSqlSessionFactory")
@Primary
public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDatasource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources1 = pathMatchingResourcePatternResolver.getResources("classpath*:com/xuzhaocai/multi/dao/order/*.xml");
factoryBean.setMapperLocations(resources1);
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
// 事务管理器
@Bean(name = "orderTransactionManger")
@Primary
public DataSourceTransactionManager orderTransactionManger(@Qualifier("orderDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean(name = "orderSqlSessionTemplate")
@Primary
public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
1.2.2.5 dao 与mapper
这种多数据源是使用分包来区别的,不同的包下的dao用不同的数据源
DriverInfoDao:
@Repository
public interface DriverInfoDao {
/**
* 修改司机信息表中,当前接单的订单
* @param driverInfo 司机信息
* @return
*/
public int updateReceivedOrder(@Param("driverInfo") DriverInfo driverInfo);
/**
* 通过司机id获取司机信息
* @param id 司机id
* @return 司机信息
*/
public DriverInfo selectById(@Param("dirverId") Integer id);
}
OrderInfoDao:
@Repository
public interface OrderInfoDao {
/**
* 修改订单信息中司机接单信息
* @param orderInfo 订单详情
* @return
*/
public int updateOrderAddDriverInfo(@Param("orderInfo") OrderInfo orderInfo);
//通过订单ID查询订单信息
public OrderInfo selectById(@Param("orderId") Integer oid);
}
同样mapper也是一样的,需要与dao放在同一个目录下:
DriverInfoMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xuzhaocai.multi.dao.driver.DriverInfoDao" >
<update id="updateReceivedOrder">
update driver_info set curr_order_id=#{driverInfo.currOrderId} where id_=#{driverInfo.id}
</update>
<select id="selectById" resultType="com.xuzhaocai.multi.entity.DriverInfo">
select
id_ as id,
driver_name as driverName,
curr_order_id as currOrderId ,
create_time as createTime
from driver_info where id_=#{dirverId}
</select>
</mapper>
OrderInfoMapper.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.xuzhaocai.multi.dao.order.OrderInfoDao" >
<update id="updateOrderAddDriverInfo" >
update order_info set driver_id = #{orderInfo.driverId} where id_=#{orderInfo.id}
</update>
<select id="selectById" resultType="com.xuzhaocai.multi.entity.OrderInfo">
select
id_ as id,
driver_id as driverId,
create_time as createTime
from order_info where id_=#{orderId}
</select>
</mapper>
1.2.2.5 service与impl
service
DirverInfoService:
public interface DirverInfoService {
public String takeOrder(Integer driverId, Integer orderId);
}
OrderInfoService:
public interface OrderInfoService {
}
impl
DriverInfoServiceImpl:
这里我们就根据上面的业务模拟一个司机接单的动作,先是将 司机信息与订单信息拿出来,然后判断司机信息中的当前订单id是否是空,订单信息中的接单司机是否为空,这里还有司机强单问题我们就处理了,我们就当这个司机拿到这个抢单机会,接着就是往订单表中添加司机id,往司机信息中添加订单id,分别更新到库中,当然我们还模拟了出问题。
@Service
public class DriverInfoServiceImpl implements DirverInfoService {
@Autowired
private DriverInfoDao driverInfoDao;
@Autowired
private OrderInfoDao orderInfoDao;
//接单操作。
@Transactional(transactionManager = "driverTransactionManger")
@Override
public String takeOrder(Integer driverId, Integer orderId) {
OrderInfo orderInfo = orderInfoDao.selectById(orderId);
DriverInfo driverInfo = driverInfoDao.selectById(driverId);
// 验证 当前司机没有接单,当前订单没有被司机接走
if (driverInfo==null || driverInfo.getCurrOrderId()!=null || orderInfo==null || orderInfo.getDriverId()!=null ){
return "失败";
}
orderInfo.setDriverId(driverId);
driverInfo.setCurrOrderId(orderId);
int updateDriverFlag = driverInfoDao.updateReceivedOrder(driverInfo);
int updateOrderFlag = orderInfoDao.updateOrderAddDriverInfo(orderInfo);
// 模拟出问题
int i= 1/0;
if (updateDriverFlag+updateOrderFlag==2){
return "成功";
}
return "失败";
}
}
OrderInfoServiceImpl:
这个order的service我们这里没用用到
@Service
public class OrderInfoServiceImpl implements OrderInfoService {
}
1.2.2.6 实体
司机实体(driver_info):
// 司机信息
public class DriverInfo {
private Integer id;
private String driverName;
private Integer currOrderId;
private Date createTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getDriverName() {
return driverName;
}
public void setDriverName(String driverName) {
this.driverName = driverName;
}
public Integer getCurrOrderId() {
return currOrderId;
}
public void setCurrOrderId(Integer currOrderId) {
this.currOrderId = currOrderId;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
订单实体(order_info ):
// 订单信息表
public class OrderInfo {
private Integer id;
private Integer driverId;
private Date createTime;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Integer getDriverId() {
return driverId;
}
public void setDriverId(Integer driverId) {
this.driverId = driverId;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
}
1.2.2.7 controller
这里我们使用controller来触发这个接单操作
@RestController
@RequestMapping("/driver/")
public class DriverInfoController {
@Autowired
private DirverInfoService dirverInfoService;
/**
* 接单方法
* @return
*/
@RequestMapping(value = "/takeOrder",method = RequestMethod.GET)
public String takeOrder(){
Integer driverId =1;
Integer orderId=1;
return dirverInfoService.takeOrder(driverId,orderId);
}
}
1.2.2.8 触发接单,查看效果
我们启动项目,然后请求接口http://127.0.0.1:8080/driver/takeOrder 触发接单。
请求完成后我们可以看到这里出了问题,然后我们看下数据库里面的表:
在driver_info司机表中没有插入订单id,也就是司机端就看不到自己接单了
而在订单表中,则是有了司机id。
这时,可以发现数据出现了不一致的情况,司机没有接到订单,而订单中确是这个司机接到到这个订单。
我们可以看下代码,代码中我们使用是事务管理器是司机那个数据源的,然后报错后,会把司机这个操作回滚。而订单操作的这个事务它是管不到的,所以就没有回滚。
那么我们使用订单的事务管理器可以不,答案是不可以,订单管理器也只能管着订单的回滚,并不能管着司机操作的回滚,同样会出现数据不一致的情况。
2.使用jta+atomikos解决多数据源事务方案
2.1 jta是什么
百度百科是这样说的
JTA,即Java Transaction API,JTA允许应用程序执行分布式事务处理——在两个或多个网络计算机资源上访问并且更新数据。JDBC驱动程序的JTA支持极大地增强了数据访问能力。
JTA与JDBC对比:
JTA事务比JDBC事务更强大。一个JTA事务可以有多个参与者,而一个JDBC事务则被限定在一个单一的数据库连接。下列任一个Java平台的组件都可以参与到一个JTA事务中:JDBC连接、JDO PersistenceManager 对象、JMS 队列、JMS 主题、企业JavaBeans(EJB)、一个用J2EE Connector Architecture 规范编译的资源分配器。
2.2 atomikos是什么
atomikos是一个实现JTA事务管理第三方管理工具,为Java平台提供增值服务的并且开源类事务管理器。
2.3 改造上述案例实现多数据源事务管理
2.3.1 pom
pom文件中添加对atomikos的支持
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jta-atomikos</artifactId>
</dependency>
2.3.2 配置文件application
需要将配置文件中jdbc-url改成url
spring:
datasource:
driver:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.3.36:3306/driver_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
order:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.3.36:3306/order_db?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true
username: root
password: 123456
application:
name: multi-datasource-distributed-transaction
server:
port: 8080
2.3.3 编写数据源配置properties
这里只是将配置文件中的配置信息放到properties中
DriverDatasourceProperties:
@Component
@ConfigurationProperties(prefix = "spring.datasource.driver")
public class DriverDatasourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
OrderDatasourceProperties:
@Component
@ConfigurationProperties(prefix = "spring.datasource.order")
public class OrderDatasourceProperties {
private String driverClassName;
private String url;
private String username;
private String password;
public String getDriverClassName() {
return driverClassName;
}
public void setDriverClassName(String driverClassName) {
this.driverClassName = driverClassName;
}
public String getUrl() {
return url;
}
public void setUrl(String url) {
this.url = url;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2.3.4 修改数据源配置
这里主要是将datasource包上一层atomikos的datasource。
DriverDatasourceConfiguration:
@Configuration
@MapperScan(basePackages = {"com.xuzhaocai.multi.dao.driver"}, sqlSessionTemplateRef = "driverSqlSessionTemplate")
public class DriverDatasourceConfiguration {
@Autowired
private DriverDatasourceProperties driverDatasourceProperties;
@Bean(name = "driverDatasource")
public DataSource driverDruidDatasource() {
DruidXADataSource datasource = new DruidXADataSource();
BeanUtils.copyProperties(driverDatasourceProperties,datasource);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(datasource);
xaDataSource.setUniqueResourceName("driverDatasource");
//return DataSourceBuilder.create().build();
return xaDataSource;
}
//配置数据源
@Bean(name = "driverSqlSessionFactory")
public SqlSessionFactory driverSqlSessionFactory(@Qualifier("driverDatasource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
//mapper.xml 的位置
Resource[] resources1 = pathMatchingResourcePatternResolver.getResources("classpath*:com/xuzhaocai/multi/dao/driver/*.xml");
factoryBean.setMapperLocations(resources1);
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
// 事务管理器
/* @Bean(name = "driverTransactionManger")
public DataSourceTransactionManager driverTransactionManger(@Qualifier("driverDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}*/
@Bean(name = "driverSqlSessionTemplate")
public SqlSessionTemplate driverSqlSessionTemplate(@Qualifier("driverSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
OrderDatasourceConfiguration:
@Configuration
@MapperScan(basePackages = {"com.xuzhaocai.multi.dao.order"}, sqlSessionTemplateRef = "orderSqlSessionTemplate")
public class OrderDatasourceConfiguration {
@Autowired
private OrderDatasourceProperties orderDatasourceProperties;
@Bean(name = "orderDatasource")
@Primary
public DataSource orderDatasource() {
DruidXADataSource datasource = new DruidXADataSource();
BeanUtils.copyProperties(orderDatasourceProperties,datasource);
AtomikosDataSourceBean xaDataSource = new AtomikosDataSourceBean();
xaDataSource.setXaDataSource(datasource);
xaDataSource.setUniqueResourceName("orderDatasource");
return xaDataSource;
}
//配置数据源
@Bean(name = "orderSqlSessionFactory")
@Primary
public SqlSessionFactory orderSqlSessionFactory(@Qualifier("orderDatasource") DataSource dataSource) throws Exception {
SqlSessionFactoryBean factoryBean = new SqlSessionFactoryBean();
PathMatchingResourcePatternResolver pathMatchingResourcePatternResolver = new PathMatchingResourcePatternResolver();
Resource[] resources1 = pathMatchingResourcePatternResolver.getResources("classpath*:com/xuzhaocai/multi/dao/order/*.xml");
factoryBean.setMapperLocations(resources1);
factoryBean.setDataSource(dataSource);
return factoryBean.getObject();
}
/* // 事务管理器
@Bean(name = "orderTransactionManger")
@Primary
public DataSourceTransactionManager orderTransactionManger(@Qualifier("orderDatasource") DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}*/
@Bean(name = "orderSqlSessionTemplate")
@Primary
public SqlSessionTemplate orderSqlSessionTemplate(@Qualifier("orderSqlSessionFactory") SqlSessionFactory sqlSessionFactory) throws Exception {
return new SqlSessionTemplate(sqlSessionFactory);
}
}
2.3.5 修改接单方法
这里就不需要指定事务管理器了,直接使用Transactional注解就好了。
@Transactional()
@Override
public String takeOrder(Integer driverId, Integer orderId) {
OrderInfo orderInfo = orderInfoDao.selectById(orderId);
DriverInfo driverInfo = driverInfoDao.selectById(driverId);
// 验证 当前司机没有接单,当前订单没有被司机接走
if (driverInfo==null || driverInfo.getCurrOrderId()!=null || orderInfo==null || orderInfo.getDriverId()!=null ){
return "失败";
}
orderInfo.setDriverId(driverId);
driverInfo.setCurrOrderId(orderId);
int updateDriverFlag = driverInfoDao.updateReceivedOrder(driverInfo);
int updateOrderFlag = orderInfoDao.updateOrderAddDriverInfo(orderInfo);
// 模拟出问题
int i= 1/0;
if (updateDriverFlag+updateOrderFlag==2){
return "成功";
}
return "失败";
}
2.3.6 测试
首先我们要把数据置为原始样子,然后重启项目,发起接单请求,这时候再看数据情况,发现报错之后都没有提交事务,这就达成了数据的一致性。