Skip to content

Commit

Permalink
feat:增加多数据源事务同步机制&多数据源事务单元测试 (#581)
Browse files Browse the repository at this point in the history
* feat:增加事务同步机制

---------

Co-authored-by: zhangpeng <[email protected]>
  • Loading branch information
ZPZP1 and zhangpeng authored Oct 23, 2023
1 parent d9d3b58 commit b4a0ee2
Show file tree
Hide file tree
Showing 30 changed files with 1,467 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@
import com.baomidou.dynamic.datasource.provider.DynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.provider.YmlDynamicDataSourceProvider;
import com.baomidou.dynamic.datasource.strategy.DynamicDataSourceStrategy;
import com.baomidou.dynamic.datasource.tx.DsTxEventListenerFactory;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
Expand Down Expand Up @@ -73,4 +75,13 @@ public DefaultDataSourceCreator dataSourceCreator(List<DataSourceCreator> dataSo
creator.setSeataMode(properties.getSeataMode());
return creator;
}

@Configuration
static class DsTxEventListenerFactoryConfiguration {
@Bean
@ConditionalOnMissingBean
public DsTxEventListenerFactory dsTxEventListenerFactory() {
return new DsTxEventListenerFactory();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.fixture.v1;

import com.baomidou.dynamic.datasource.DynamicRoutingDataSource;
import com.baomidou.dynamic.datasource.creator.DataSourceProperty;
import com.baomidou.dynamic.datasource.creator.DefaultDataSourceCreator;
import com.baomidou.dynamic.datasource.fixture.v1.service.tx.*;
import com.baomidou.dynamic.datasource.tx.TransactionContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.support.TransactionSynchronization;

import javax.sql.DataSource;
import java.util.Arrays;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;


@SpringBootTest(classes = DsTransactionalApplication.class, webEnvironment = RANDOM_PORT)
public class DsTransactionalTest {
@Autowired
private OrderService orderService;

@Autowired
private AccountService accountService;

@Autowired
private ProductService productService;

@Autowired
DataSource dataSource;

@Autowired
DefaultDataSourceCreator dataSourceCreator;
private DynamicRoutingDataSource ds;

@Test
public void testDsTransactional() {
DataSourceProperty orderDataSourceProperty = createDataSourceProperty("order");
DataSourceProperty productDataSourceProperty = createDataSourceProperty("product");
DataSourceProperty accountDataSourceProperty = createDataSourceProperty("account");
ds = (DynamicRoutingDataSource) dataSource;
ds.addDataSource(orderDataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(orderDataSourceProperty));
ds.addDataSource(productDataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(productDataSourceProperty));
ds.addDataSource(accountDataSourceProperty.getPoolName(), dataSourceCreator.createDataSource(accountDataSourceProperty));
PlaceOrderRequest placeOrderRequest = new PlaceOrderRequest(1, 1, 22, OrderStatus.INIT);

//商品不足
TransactionContext.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
placeOrderRequest.setOrderStatus(OrderStatus.FAIL);
}
}
});
assertThrows(RuntimeException.class, () -> orderService.placeOrder(placeOrderRequest));
assertThat(placeOrderRequest.getOrderStatus()).isEqualTo(OrderStatus.FAIL);
assertThat(orderService.selectOrders()).isEmpty();
assertThat(accountService.selectAccount()).isEqualTo(new Account(1, 50.0));
assertThat(productService.selectProduct()).isEqualTo(new Product(1, 10.0, 20));

//账户不足
TransactionContext.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCompletion(int status) {
if (status == STATUS_ROLLED_BACK) {
placeOrderRequest.setOrderStatus(OrderStatus.FAIL);
}
}
});
placeOrderRequest.setAmount(6);
placeOrderRequest.setOrderStatus(OrderStatus.INIT);
assertThrows(RuntimeException.class, () -> orderService.placeOrder(placeOrderRequest));
assertThat(placeOrderRequest.getOrderStatus()).isEqualTo(OrderStatus.FAIL);
assertThat(orderService.selectOrders()).isEmpty();
assertThat(accountService.selectAccount()).isEqualTo(new Account(1, 50.0));
assertThat(productService.selectProduct()).isEqualTo(new Product(1, 10.0, 20));

//正常下单
TransactionContext.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
placeOrderRequest.setOrderStatus(OrderStatus.SUCCESS);
}
});
placeOrderRequest.setAmount(5);
placeOrderRequest.setOrderStatus(OrderStatus.INIT);
assertThat(orderService.placeOrder(placeOrderRequest)).isEqualTo(OrderStatus.INIT);
assertThat(placeOrderRequest.getOrderStatus()).isEqualTo(OrderStatus.SUCCESS);
assertThat(orderService.selectOrders()).isEqualTo(Arrays.asList(new Order(3, 1, 1, 5, 50.0)));
assertThat(accountService.selectAccount()).isEqualTo(new Account(1, 0.0));
assertThat(productService.selectProduct()).isEqualTo(new Product(1, 10.0, 15));
}

private DataSourceProperty createDataSourceProperty(String poolName) {
DataSourceProperty result = new DataSourceProperty();
result.setPoolName(poolName);
result.setDriverClassName("org.h2.Driver");
result.setUrl("jdbc:h2:mem:" + poolName + ";INIT=RUNSCRIPT FROM 'classpath:db/ds-with-transactional.sql'");
result.setUsername("sa");
result.setPassword("");
return result;
}
}

@SpringBootApplication
class DsTransactionalApplication {
public static void main(String[] args) {
SpringApplication.run(DsTransactionalApplication.class, args);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,4 @@ class NestApplication {
public static void main(String[] args) {
SpringApplication.run(NestApplication.class, args);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.fixture.v1.service.tx;

import lombok.AllArgsConstructor;
import lombok.Data;


@Data
@AllArgsConstructor
public class Account {
private Integer id;

/**
* 余额
*/
private Double balance;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.fixture.v1.service.tx;

import com.baomidou.dynamic.datasource.annotation.DS;
import org.springframework.stereotype.Service;
import org.springframework.util.Assert;

import javax.sql.DataSource;
import java.sql.*;


@Service
@DS("account")
public class AccountService {
private final DataSource dataSource;

public AccountService(DataSource dataSource) {
this.dataSource = dataSource;
}

public void reduceBalance(Integer userId, Double price) {
try (Connection connection = dataSource.getConnection()) {
PreparedStatement preparedStatement = connection.prepareStatement("select * from account where id=?");
preparedStatement.setInt(1, userId);
ResultSet resultSet = preparedStatement.executeQuery();
Account account = null;
if (resultSet.next()) {
Integer id = resultSet.getObject(1, Integer.class);
Double balance = resultSet.getObject(2, Double.class);
account = new Account(id, balance);
}
Assert.notNull(account, "用户不存在");
Double balance = account.getBalance();
if (balance < price) {
throw new RuntimeException("余额不足");
}
double currentBalance = account.getBalance() - price;
String sql = "update account set balance=? where id=?";
PreparedStatement updateStatement = connection.prepareStatement(sql);
updateStatement.setDouble(1, currentBalance);
updateStatement.setInt(2, userId);
updateStatement.executeUpdate();
} catch (SQLException e) {
throw new RuntimeException(e);
}
}

public Account selectAccount() {
try (Connection connection = dataSource.getConnection(); Statement statement = connection.createStatement()) {
ResultSet resultSet = statement.executeQuery("SELECT * FROM account where id=1");
Account account = null;
if (resultSet.next()) {
Integer id = resultSet.getObject(1, Integer.class);
Double balance = resultSet.getObject(2, Double.class);
account = new Account(id, balance);
}
return account;
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright © 2018 organization baomidou
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.baomidou.dynamic.datasource.fixture.v1.service.tx;

import lombok.AllArgsConstructor;
import lombok.Data;


@Data
@AllArgsConstructor
public class Order {
private Integer id;

/**
* 用户ID
*/
private Integer userId;
/**
* 商品ID
*/
private Integer productId;
/**
* 数量
*/
private Integer amount;

/**
* 总金额
*/
private Double totalPrice;
}
Loading

0 comments on commit b4a0ee2

Please sign in to comment.