This is the 20th day of my participation in the First Challenge 2022
- Microservices series: Introduction to Seata for Distributed Transactions Spring Cloud Alibaba
In the previous introductory article, we had an overview of Seata and set up the SeATa-Server server, the TC coordinator, as well as integrating Nacos.
In this article we are going to use Seata in the real world. After all, that’s what you learn to use Seata in the real world.
Seata is simple to use, mainly using the @GlobalTransactional annotation, but the scaffolding process is a little more complicated.
Without further ado, let’s begin today’s lesson.
One, foreword
- This example uses
Seata
Ats model - Due to limited space, only the core code and configuration are posted in this article. The full code address is cloud-seata: seata demo-gitee.com
- This example uses Nacos as the registry and Nacos as the configuration center
1. Version description
MySQL 8.0.27
Nacos Server 2.0.3
Seata Server 1.4.2
Spring Boot 2.3.7. RELEASE
Spring Cloud Hoxton.SR9
Spring Cloud Alibaba 2.2.5. RELEASE
2. Case objective
This case will create three services, namely order service, inventory service and account service, and the call process among the services is as follows:
- 1) When the user places an order, the order service is called to create an order, and then the inventory service is called remotely (OpenFeign) to deduct the inventory of the ordered item
- 2) The order service then makes a remote call to the Account service (OpenFeign) to deduct the balance in the user account
- 3) Finally change the order status to completed in the order service
The above operation spans three databases and has two remote calls. It is obvious that there are distributed transaction problems. The overall structure of the project is as follows:
├─ Cloud-Seata ├─ ├─ seata Product # ├─ seata ├─ ├─ seata ├─ seata Product # ├─ seata ├─ Seata │ ├─ Seata ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │ ├─ Seata │Copy the code
Second, the code
1. Account service establishment
Create the Seata-Account service module
1.1. Create a database
SeatA_AccountDROP DATABASE IF EXISTS seata_account;
CREATE DATABASE seata_account;
DROP TABLE IF EXISTS seata_account.account;
CREATE TABLE seata_account.account
(
id INT(11) NOT NULL AUTO_INCREMENT,
balance DOUBLE DEFAULT NULL,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS seata_account.undo_log;
CREATE TABLE seata_account.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL.PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
INSERT INTO seata_account.account (id, balance)
VALUES (1.50);
Copy the code
The undo_log table in the library, which is required by Seata AT mode, is mainly used for rollback of branch transactions. Also, for testing convenience, we insert an account record with ID = 1.
1.2. Add dependencies
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
<exclusions>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
</exclusion>
<exclusion>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-all</artifactId>
<version>1.4.2</version>
</dependency>
<dependency>
<groupId>io.seata</groupId>
<artifactId>seata-spring-boot-starter</artifactId>
<version>1.4.2</version>
</dependency>
Copy the code
Since the SpringCloud Alibaba dependency version used is 2.2.5.RELEASE, the built-in SEATA version is 1.3.0, but the version used by our SEATA server is 1.4.2, so we need to remove the original dependency and add the dependency of 1.4.2 again.
If the dependent versions are different, the following error is reported after startup
no available service ‘null’ found, please make sure registry config correct
Note: The seata client version must be the same as the server version.
1.3. Service Profile
server:
port: 9201
# spring configuration
spring:
application:
name: seata-account
datasource:
druid:
username: root
password: root
url: jdbc:mysql://localhost:3306/seata_account? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0. 01.: 8848
# seata configuration
seata:
# Whether to enable seATA. Default is true
enabled: true
${spring.application. Name}
application-id: ${spring.application.name}
# Seata transaction group number, used for TC cluster name, must be the same as config.tx(nacOS) configured
tx-service-group: ${spring.application.name}-group
# Service configuration item
service:
# Mapping virtual groups and groups
vgroup-mapping:
ruoyi-system-group: default
Group and Seata service mapping
grouplist:
default: 127.0. 01.: 8091
config:
type: nacos
nacos:
The configuration on the server side (registry. Config) needs to be consistent
namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
server-addr: localhost:8848
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
The name must be the same as the name of the seata server. The default is seata-server
application: seata-server
Need to be consistent with the configuration on the server side (registry.config)
group: SEATA_GROUP
namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
server-addr: localhost:8848
username: nacos
password: nacos
# mybatis configuration
mybatis:
Search for the specified package alias
typeAliasesPackage: com.ezhang.account.mapper
Configure mapper scan to find all mapper. XML mapping files
mapperLocations: classpath:mapper/xml/*.xml
Copy the code
Note:
- The nacOS configuration in the client seATA must be the same as that on the server, such as the address, namespace, and group…….
tx-service-group
Note that this attribute must be consistent with the configuration on the server. Otherwise, it does not take effect. For example, in the configuration above, add a new configuration to nacOSservice.vgroupMapping.seata-account-group=default
, as shown below:
Value is the default
Note: Remember to add the configuration prefix service. VgroupMapping.
4. Core code
@Service
public class AccountServiceImpl implements AccountService
{
private static final Logger log = LoggerFactory.getLogger(AccountServiceImpl.class);
@Resource
private AccountMapper accountMapper;
/** * Transaction propagation feature set to REQUIRES_NEW Start new transaction important !!!! Be sure to use REQUIRES_NEW */
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void reduceBalance(Long userId, Double price)
{
log.info("=============ACCOUNT START=================");
log.info("XID: {}", RootContext.getXID());
Account account = accountMapper.selectById(userId);
Double balance = account.getBalance();
log.info("Order user {} balance is {}, total commodity price is {}", userId, balance, price);
if (balance < price)
{
log.warn("User {} balance is insufficient, current balance :{}", userId, balance);
throw new RuntimeException("Insufficient balance");
}
log.info("Start deducting user {} balance", userId);
double currentBalance = account.getBalance() - price;
account.setBalance(currentBalance);
accountMapper.updateById(account);
log.info("Successful deduction of user {} balance, user account balance after deduction is {}", userId, currentBalance);
log.info("=============ACCOUNT END================="); }}Copy the code
Note the @transactional (propagation = Propagation.REQUIRES_NEW) annotation
@RestController
@RequestMapping("/account")
public class AccountController {
@Autowired
private AccountService accountService;
@PostMapping("/reduceBalance")
public Map<String, Object> reduceBalance(Long userId, Double price){
accountService.reduceBalance(userId, price);
Map<String, Object> map = new HashMap<>();
map.put("code"."success");
returnmap; }}Copy the code
This is mainly used for remote calls to Feign, the rest of the code is not posted, the full code address will be at the end of the article.
2. Warehouse service construction
Create a seata-Product service module. Don’t bother with the name.
2.1. Create a database
SeatA_productDROP DATABASE IF EXISTS seata_product;
CREATE DATABASE seata_product;
DROP TABLE IF EXISTS seata_product.product;
CREATE TABLE seata_product.product
(
id INT(11) NOT NULL AUTO_INCREMENT,
price DOUBLE DEFAULT NULL,
stock INT(11) DEFAULT NULL,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS seata_product.undo_log;
CREATE TABLE seata_product.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL.PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
INSERT INTO seata_product.product (id, price, stock)
VALUES (1.10.20);
Copy the code
There is also an undo_log table with a test data inserted
2.2. Add dependencies
This and account service is the same, will not stick
2.3. Service profile
The account service is basically the same as the account service, except for the port number, spring.application.name, database connection, etc
server:
port: 9221
# spring configuration
spring:
application:
name: seata-product
Copy the code
Note tx-service-group again
# Seata transaction group number used for TC cluster name
tx-service-group: ${spring.application.name}-group
Copy the code
Add a service in Nacos console. VgroupMapping. Seata – product – group
2.4. Core code
@Service
public class ProductServiceImpl implements ProductService
{
private static final Logger log = LoggerFactory.getLogger(ProductServiceImpl.class);
@Resource
private ProductMapper productMapper;
/** * Transaction propagation feature set to REQUIRES_NEW Start new transaction important !!!! Be sure to use REQUIRES_NEW */
@Transactional(propagation = Propagation.REQUIRES_NEW)
@Override
public Double reduceStock(Long productId, Integer amount)
{
log.info("=============PRODUCT START=================");
log.info("XID: {}", RootContext.getXID());
// Check inventory
Product product = productMapper.selectById(productId);
Integer stock = product.getStock();
log.info("The inventory with item number {} is {} and the quantity of the order is {}", productId, stock, amount);
if (stock < amount)
{
log.warn("Item number {} out of stock, current stock :{}", productId, stock);
throw new RuntimeException("Out of stock");
}
log.info("Start deducting inventory with item number {} and unit price {}", productId, product.getPrice());
// Deduct inventory
int currentStock = stock - amount;
product.setStock(currentStock);
productMapper.updateById(product);
double totalPrice = product.getPrice() * amount;
log.info("Inventory {} is deducted successfully, inventory {} is deducted, and the total price of {} items is {}", productId, currentStock, amount, totalPrice);
log.info("=============PRODUCT END=================");
returntotalPrice; }}Copy the code
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/reduceStock")
public Map<String, Object> reduceStock(Long productId, Integer amount){
Double totalPrice = productService.reduceStock(productId, amount);
Map<String, Object> map = new HashMap<>();
map.put("code"."success");
map.put("totalPrice", totalPrice);
returnmap; }}Copy the code
3. Order service establishment
3.1. Create a database
Order database information seatA_ORDERDROP DATABASE IF EXISTS seata_order;
CREATE DATABASE seata_order;
DROP TABLE IF EXISTS seata_order.p_order;
CREATE TABLE seata_order.p_order
(
id INT(11) NOT NULL AUTO_INCREMENT,
user_id INT(11) DEFAULT NULL,
product_id INT(11) DEFAULT NULL,
amount INT(11) DEFAULT NULL,
total_price DOUBLE DEFAULT NULL,
status VARCHAR(100) DEFAULT NULL,
add_time DATETIME DEFAULT CURRENT_TIMESTAMP,
last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP.PRIMARY KEY (id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
DROP TABLE IF EXISTS seata_order.undo_log;
CREATE TABLE seata_order.undo_log
(
id BIGINT(20) NOT NULL AUTO_INCREMENT,
branch_id BIGINT(20) NOT NULL,
xid VARCHAR(100) NOT NULL,
context VARCHAR(128) NOT NULL,
rollback_info LONGBLOB NOT NULL,
log_status INT(11) NOT NULL,
log_created DATETIME NOT NULL,
log_modified DATETIME NOT NULL.PRIMARY KEY (id),
UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8mb4;
Copy the code
3.2. Add dependencies
In addition to the same dependencies as the two services above, the order service also requires an OpenFeign dependency
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
Copy the code
3.3. Service configuration files
server:
port: 9211
# spring configuration
spring:
application:
name: seata-order
datasource:
druid:
username: root
password: root
url: jdbc:mysql://localhost:3306/seata_order? useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
cloud:
nacos:
discovery:
server-addr: 127.0. 01.: 8848
feign:
hystrix:
enabled: true
# seata configuration
seata:
enabled: true
${spring.application. Name}
application-id: ${spring.application.name}
# Seata transaction group number used for TC cluster name
tx-service-group: ${spring.application.name}-group
Disable automatic proxy
enable-auto-data-source-proxy: false
# Service configuration item
service:
# Mapping virtual groups and groups
vgroup-mapping:
ruoyi-system-group: default
Group and Seata service mapping
grouplist:
default: 127.0. 01.: 8091
config:
type: nacos
nacos:
namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
server-addr: localhost:8848
group: SEATA_GROUP
username: nacos
password: nacos
registry:
type: nacos
nacos:
application: seata-server
group: SEATA_GROUP
namespace: c18b9158-bcf3-4d5a-b78b-f02bc8a19353
server-addr: localhost:8848
username: nacos
password: nacos
# mybatis configuration
mybatis:
Search for the specified package alias
typeAliasesPackage: com.ezhang.order.mapper
Configure mapper scan to find all mapper. XML mapping files
mapperLocations: classpath:mapper/**/*.xml
Copy the code
3.4. Core code
The test interface
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/placeOrder")
public String placeOrder(@Validated @RequestBody PlaceOrderRequest request) {
orderService.placeOrder(request);
return "Order successful"; }}Copy the code
The specific implementation is through Feign to remotely call the other two services to deduct inventory, deduct the balance of the interface
@Service
public class OrderServiceImpl implements OrderService
{
private static final Logger log = LoggerFactory.getLogger(OrderServiceImpl.class);
@Resource
private OrderMapper orderMapper;
@Autowired
private RemoteAccountService accountService;
@Autowired
private RemoteProductService productService;
@Override
@Transactional
@GlobalTransactional The seATA global transaction annotation needs to be added if the transaction is started first
public void placeOrder(PlaceOrderRequest request)
{
log.info("=============ORDER START=================");
Long userId = request.getUserId();
Long productId = request.getProductId();
Integer amount = request.getAmount();
log.info("Order request received, User :{}, item :{}, Quantity :{}", userId, productId, amount);
log.info("XID: {}", RootContext.getXID());
Order order = new Order(userId, productId, 0, amount);
orderMapper.insert(order);
log.info("The order is generated in the first stage, waiting for the payment of the inventory.");
// Deduct the inventory and calculate the total price
Map<String, Object> reduceStockMap = productService.reduceStock(productId, amount);
Double totalPrice = Double.valueOf(reduceStockMap.get("totalPrice").toString());
// Deduct the balance
accountService.reduceBalance(userId, totalPrice);
order.setStatus(1);
order.setTotalPrice(totalPrice);
orderMapper.updateById(order);
log.info("Order has been placed successfully.");
log.info("=============ORDER END================="); }}Copy the code
Note: There is a @GlobalTransactional annotation, which is provided by Seata to start global transactions.
RemoteAccountService and RemoteProductService
@FeignClient(contextId = "remoteAccountService", value = "seata-account")
public interface RemoteAccountService {
@PostMapping(value = "/account/reduceBalance")
Map<String, Object> reduceBalance(@RequestParam("userId") Long userId, @RequestParam("price") Double price);
}
@FeignClient(contextId = "remoteProductService", value = "seata-product")
public interface RemoteProductService {
@PostMapping(value = "/product/reduceStock")
Map<String, Object> reduceStock(@RequestParam("productId") Long productId, @RequestParam("amount") Integer amount);
}
Copy the code
Since we are only testing Seata, the degraded fallbackFactory is not added.
Don’t forget the @enableFeignClients annotation on the startup class.
Now that our code is basically written, let’s test it out.
Three, test,
The premise of the test is that the Nacos, Seata and MySQL used in the test are successfully started, and then the three services built above are successfully started
The price of the item with ID 1 in the Product table in the SEATA_Product repository is 10 and the inventory is 20
The balance of user 1 in the account table of the seatA_account account library is 50
1. Order normally
To simulate the normal order, buy a commodity at http://localhost:9211/order/placeOrder
Parameters:
{
"userId": 1,
"productId": 1,
"amount": 1
}
Copy the code
View console logs:
2. Inventory is insufficient
Simulation of inventory shortage, transaction rollback http://localhost:9211/order/placeOrder
Content-Type/application/json
{
"userId": 1,
"productId": 1,
"amount": 21
}
Copy the code
Request exception, console log:
Order records added to p_order table are rolled back
3. The user balance is insufficient
Lack of simulated user balance, transaction rollback http://localhost:9211/order/placeOrder
Content-Type/application/json
{
"userId": 1,
"productId": 1,
"amount": 6
}
Copy the code
Request exception, console log:
P_order, product, account all records are rolled back.
At this point, the test is complete and this article is closed.
Cloud-seata: seata Demo-gitee.com
Give it a thumbs up, Daniel!