数据库事务的基本概念(ACID)
原子性(Atomicity):操作这些指令时,要么全部执行成功,要么全部不执行。只要其中一个指令执行失败,所有的指令都执行失败,数据进行回滚,回到执行指令前的数据状态。要么执行,要么不执行
一致性(Consistency):事务的执行使数据从一个状态转换为另一个状态,数据库的完整性约束没有被破坏。能量守恒,总量不变
隔离性(Isolation):隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。信息彼此独立,互不干扰
持久性(Durability):当事务正确完成后,它对于数据的改变是永久性的。不会轻易丢失
一、基本介绍
- 什么是分布式事务
指一次大的操作由不同的小操作组成的,这些小的操作分布在不同的服务器上,分布式事务需要保证这些小操作要么全部成功,要么全部失败。从本质上来说,分布式事务就是为了保证不同数据库的数据一致性。
- 为什么要使用分布式事务
在微服务独立数据源的思想,每一个微服务都有一个或者多个数据源,虽然单机单库事务已经非常成熟,但是由于网路延迟和不可靠的客观因素,分布式事务到现在也还没有成熟的方案,对于中大型网站,特别是涉及到交易的网站,一旦将服务拆分微服务,分布式事务一定是绕不开的一个组件,通常解决分布式事务问题。
- seata 分布式事务
Seata
是阿里开源的一款开源的分布式事务解决方案,致力于提供高性能和简单易用的分布式事务服务。
Seata
目标打造一站式的分布事务的解决方案,最终会提供四种事务模式:
AT 模式:参见(《Seata AT 模式》 (opens new window))文档
TCC 模式:参见(《Seata TCC 模式》 (opens new window))文档
Saga 模式:参见(《SEATA Saga 模式》 (opens new window))文档
XA 模式:正在开发中… 目前使用的流行度情况是:AT
> TCC
> Saga
。因此,我们在学习Seata
的时候,可以花更多精力在AT
模式上,最好搞懂背后的实现原理,毕竟分布式事务涉及到数据的正确性,出问题需要快速排查定位并解决。
二、下载方式
- Windows平台安装包下载
可以从https://github.com/seata/seata/releases
下载seata-server-$version.zip
包。
Windows下载解压后(.zip)
直接点击bin/seata-server.bat
就可以了。(我使用的是1.4.0版本)
提示
如果觉得官网下载慢,可以使用我分享的网盘地址: 百度网盘 请输入提取码 提取码: vneh
三、如何使用
1、创建相关测试数据库和表。
# 订单数据库信息 seata_order
DROP 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;
# 产品数据库信息 seata_product
DROP 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);
# 账户数据库信息 seata_account
DROP 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);
其中,每个库中的undo_log
表,是Seata AT
模式必须创建的表,主要用于分支事务的回滚。 另外,考虑到测试方便,我们插入了一条id = 1
的account
记录,和一条id = 1
的product
记录。
2、创建新的test模块
具体参见新建子模块
3、引入依赖
<!-- RuoYi Common Datasource -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-datasource</artifactId>
</dependency>
<!-- RuoYi Common Seata -->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-common-seata</artifactId>
</dependency>
4、服务配置文件
# spring配置
spring:
redis:
host: localhost
port: 6379
password:
datasource:
druid:
stat-view-servlet:
enabled: true
loginUsername: admin
loginPassword: 123456
dynamic:
druid:
initial-size: 5
min-idle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall,slf4j
connectionProperties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
datasource:
# 主库数据源
master:
username: root
password: admin0625
url: jdbc:mysql://localhost:3306/ry-cloud?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
# seata_order数据源
order:
username: root
password: admin0625
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
# seata_account数据源
account:
username: root
password: admin0625
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
# seata_product数据源
product:
username: root
password: admin0625
url: jdbc:mysql://localhost:3306/seata_product?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
driver-class-name: com.mysql.cj.jdbc.Driver
#开启seata代理,开启后默认每个数据源都代理,如果某个不需要代理可单独关闭
seata: true
# seata配置
seata:
enabled: true
# Seata 应用编号,默认为 ${spring.application.name}
application-id: ${
spring.application.name}
# Seata 事务组编号,用于 TC 集群名
tx-service-group: ${
spring.application.name}-group
# 关闭自动代理
enable-auto-data-source-proxy: false
# 服务配置项
service:
# 虚拟组和分组的映射
vgroup-mapping:
ruoyi-system-group: default
config:
type: nacos
nacos:
serverAddr: 127.0.0.1:8848
group: SEATA_GROUP
namespace:
registry:
type: nacos
nacos:
application: seata-server
server-addr: 127.0.0.1:8848
namespace:
# mybatis配置
mybatis:
# 搜索指定包别名
typeAliasesPackage: com.ruoyi.test.domain
# 配置mapper的扫描,找到所有的mapper.xml映射文件
mapperLocations: classpath:mapper/**/*.xml
# swagger配置
swagger:
title: 测试模块接口文档
license: Powered By ruoyi
licenseUrl: https://ruoyi.vip
提示
注意,一定要设置spring.datasource.dynamic.seata
配置项为true
,开启对Seata
的集成,否则会导致Seata
全局事务回滚失败。
5、示例代码
Domain
Account.java
package com.ruoyi.test.domain;
import java.util.Date;
public class Account
{
private Long id;
/**
* 余额
*/
private Double balance;
private Date lastUpdateTime;
public Long getId()
{
return id;
}
public void setId(Long id)
{
this.id = id;
}
public Double getBalance()
{
return balance;
}
public void setBalance(Double balance)
{
this.balance = balance;
}
public Date getLastUpdateTime()
{
return lastUpdateTime;
}
public void setLastUpdateTime(Date lastUpdateTime)
{
this.lastUpdateTime = lastUpdateTime;
}
}
Order.java
package com.ruoyi.test.domain;
public class Order
{
private Integer id;
/**
* 用户ID
*/
private Long userId;
/**
* 商品ID
*/
private Long productId;
/**
* 订单状态
*/
private int status;
/**
* 数量
*/
private Integer amount;
/**
* 总金额
*/
private Double totalPrice;
public Order()
{
}
public Order(Long userId, Long productId, int status, Integer amount)
{
this.userId = userId;
this.productId = productId;
this.status = status;
this.amount = amount;
}
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public Long getUserId()
{
return userId;
}
public void setUserId(Long userId)
{
this.userId = userId;
}
public Long getProductId()
{
return productId;
}
public void setProductId(Long productId)
{
this.productId = productId;
}
public int getStatus()
{
return status;
}
public void setStatus(int status)
{
this.status = status;
}
public Integer getAmount()
{
return amount;
}
public void setAmount(Integer amount)
{
this.amount = amount;
}
public Double getTotalPrice()
{
return totalPrice;
}
public void setTotalPrice(Double totalPrice)
{
this.totalPrice = totalPrice;
}
}
Product.java
package com.ruoyi.test.domain;
import java.util.Date;
public class Product
{
private Integer id;
/**
* 价格
*/
private Double price;
/**
* 库存
*/
private Integer stock;
private Date lastUpdateTime;
public Integer getId()
{
return id;
}
public void setId(Integer id)
{
this.id = id;
}
public Double getPrice()
{
return price;
}
public void setPrice(Double price)
{
this.price = price;
}
public Integer getStock()
{
return stock;
}
public void setStock