一、先白话白话分布式事务多重要
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
场景:用户下单买烩面
- 创建订单(order-service)
- 扣库存(product-service)
- 扣余额(account-service)
问题:
- 订单创建成功了,库存没扣 -> 超卖了
- 库存扣了,余额不够 -> 库存白扣了
- 余额扣了,订单没创建 -> 钱没了,订单没有
这就是分布式事务问题:多个服务操作数据库,要保证要么一起成功,要么一起失败。
本地事务 vs 分布式事务
- 本地事务:一个数据库里,用
@Transactional搞定 - 分布式事务:多个数据库/服务,
@Transactional管不了跨服务
二、分布式事务的几种方案
方案1:2PC(两阶段提交) - 老村长模式
- 阶段一:村长问大家“能结婚不?”(准备阶段)
- 阶段二:大家都说“能”,村长说“结!”(提交阶段)
- 缺点:同步阻塞,性能差,协调者单点故障
方案2:TCC(Try-Confirm-Cancel) - 预订模式
- Try:预订(冻结资源)
- Confirm:确认(使用资源)
- Cancel:取消(释放资源)
- 优点:性能好
- 缺点:代码复杂,要写三个接口
方案3:本地消息表 - 写信模式
- 本地事务+消息
- 可靠消息最终一致性
- 优点:实现简单
- 缺点:消息可能重复消费
方案4:Seata的AT模式 - 自动模式(推荐)
- 自动回滚
- 不用改业务代码
- Spring Cloud Alibaba集成好
三、Seata是啥?
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
Seata(Simple Extensible Autonomous Transaction Architecture)是阿里巴巴开源的分布式事务解决方案。
三个角色:
-
TC(Transaction Coordinator):事务协调者(村长)
- 管理全局事务状态
- 协调分支事务提交/回滚
-
TM(Transaction Manager):事务管理器(婚礼司仪)
- 开启全局事务
- 提交/回滚全局事务
-
RM(Resource Manager):资源管理器(各个家庭)
- 管理分支事务
- 向TC注册分支事务
- 报告分支事务状态
工作流程(以结婚为例):
- TM说:“开始结婚事务!”(@GlobalTransactional)
- 新郎家RM:准备结婚(执行SQL,生成UNDO_LOG)
- 新娘家RM:准备结婚(执行SQL,生成UNDO_LOG)
- TC记录事务状态
- 都准备好了,TM说:“提交!”
- TC通知各家删除UNDO_LOG
如果出问题:
- 任何一家说“不行”
- TM说:“回滚!”
- TC通知各家根据UNDO_LOG回滚
四、搭Seata环境
步骤1:下载Seata Server
- 官网:seata.io
- 下载1.5.0以上版本
- 解压到
D:\seata
步骤2:改配置
1. 改存储模式(用file就行,生产用db)
seata\conf\file.conf:
## transaction log store
store {
## store mode: file、db
mode = "file" # 先用file,简单
## file store property
file {
dir = "sessionStore"
}
}
2. 改注册中心(用nacos)
seata\conf\registry.conf:
registry {
type = "nacos" # 用nacos
nacos {
application = "seata-server"
serverAddr = "127.0.0.1:8848"
group = "SEATA_GROUP"
namespace = ""
cluster = "default"
username = "nacos"
password = "nacos"
}
}
config {
type = "nacos"
nacos {
serverAddr = "127.0.0.1:8848"
namespace = ""
group = "SEATA_GROUP"
username = "nacos"
password = "nacos"
}
}
步骤3:启动Seata Server
cd D:\seata\bin
# Windows
seata-server.bat
# Linux/Mac
sh seata-server.sh
启动后,看到:
Server started at port: 8091
五、创建示例项目:分布式下单
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
项目结构:
distributed-order-demo/
├── order-service/ # 订单服务
├── product-service/ # 商品服务(库存)
├── account-service/ # 账户服务(余额)
└── seata-server/ # Seata Server
步骤1:建数据库
-- 订单库
CREATE DATABASE `order_db`;
USE `order_db`;
CREATE TABLE `order` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`product_id` bigint NOT NULL,
`count` int NOT NULL COMMENT '购买数量',
`money` decimal(10,2) NOT NULL COMMENT '金额',
`status` int DEFAULT '0' COMMENT '状态:0-创建中,1-已完成',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 商品库
CREATE DATABASE `product_db`;
USE `product_db`;
CREATE TABLE `product` (
`id` bigint NOT NULL AUTO_INCREMENT,
`product_name` varchar(100) NOT NULL,
`stock` int NOT NULL COMMENT '库存',
`price` decimal(10,2) NOT NULL COMMENT '单价',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `product` VALUES (1, '河南烩面', 100, 15.00);
-- 账户库
CREATE DATABASE `account_db`;
USE `account_db`;
CREATE TABLE `account` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`balance` decimal(10,2) NOT NULL COMMENT '余额',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT INTO `account` VALUES (1, 1, 1000.00);
-- UNDO_LOG表(每个库都要建)
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int 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 DEFAULT CHARSET=utf8mb4;
步骤2:创建订单服务
1. 加依赖
order-service/pom.xml:
<!-- Seata -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
2. 配置文件
bootstrap.yml:
spring:
application:
name: order-service
cloud:
nacos:
discovery:
server-addr: localhost:8848
# Seata配置
alibaba:
seata:
tx-service-group: my_tx_group # 事务组,要跟Seata Server配置一致
# Seata配置
seata:
enabled: true
application-id: order-service
tx-service-group: my_tx_group
enable-auto-data-source-proxy: true # 自动代理数据源
config:
type: nacos
nacos:
server-addr: localhost:8848
group: SEATA_GROUP
namespace: ""
registry:
type: nacos
nacos:
application: seata-server
server-addr: localhost:8848
namespace: ""
group: SEATA_GROUP
application.yml:
server:
port: 8081
spring:
datasource:
url: jdbc:mysql://localhost:3306/order_db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
3. 业务代码
Order实体:
@Data
@TableName("order")
public class Order {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private Long productId;
private Integer count;
private BigDecimal money;
private Integer status; // 0-创建中,1-已完成
}
OrderMapper:
@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}
OrderService:
@Service
public class OrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private RestTemplate restTemplate;
// 创建订单(分布式事务入口)
@GlobalTransactional // 重点!开启全局事务
public Order createOrder(Long userId, Long productId, Integer count) {
// 1. 计算金额(调用商品服务获取价格)
BigDecimal price = getProductPrice(productId);
BigDecimal money = price.multiply(new BigDecimal(count));
// 2. 创建订单(本地事务)
Order order = new Order();
order.setUserId(userId);
order.setProductId(productId);
order.setCount(count);
order.setMoney(money);
order.setStatus(0); // 创建中
orderMapper.insert(order);
// 3. 扣库存(远程调用)
boolean stockSuccess = decreaseStock(productId, count);
if (!stockSuccess) {
throw new RuntimeException("库存不足");
}
// 4. 扣余额(远程调用)
boolean accountSuccess = decreaseAccount(userId, money);
if (!accountSuccess) {
throw new RuntimeException("余额不足");
}
// 5. 更新订单状态
order.setStatus(1); // 已完成
orderMapper.updateById(order);
return order;
}
private BigDecimal getProductPrice(Long productId) {
// 调用商品服务获取价格
String url = "http://product-service/product/price/" + productId;
return restTemplate.getForObject(url, BigDecimal.class);
}
private boolean decreaseStock(Long productId, Integer count) {
// 调用商品服务扣库存
String url = "http://product-service/product/decreaseStock";
Map<String, Object> params = new HashMap<>();
params.put("productId", productId);
params.put("count", count);
Boolean result = restTemplate.postForObject(
url, params, Boolean.class);
return Boolean.TRUE.equals(result);
}
private boolean decreaseAccount(Long userId, BigDecimal money) {
// 调用账户服务扣余额
String url = "http://account-service/account/decrease";
Map<String, Object> params = new HashMap<>();
params.put("userId", userId);
params.put("money", money);
Boolean result = restTemplate.postForObject(
url, params, Boolean.class);
return Boolean.TRUE.equals(result);
}
}
OrderController:
@RestController
@RequestMapping("/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/create")
public String createOrder(@RequestParam Long userId,
@RequestParam Long productId,
@RequestParam Integer count) {
try {
Order order = orderService.createOrder(userId, productId, count);
return "订单创建成功,订单ID:" + order.getId();
} catch (Exception e) {
return "订单创建失败:" + e.getMessage();
}
}
}
步骤3:创建商品服务
ProductService:
@Service
public class ProductService {
@Autowired
private ProductMapper productMapper;
@Transactional // 本地事务注解
public boolean decreaseStock(Long productId, Integer count) {
// 查询商品
Product product = productMapper.selectById(productId);
if (product == null) {
throw new RuntimeException("商品不存在");
}
// 检查库存
if (product.getStock() < count) {
throw new RuntimeException("库存不足");
}
// 扣库存
product.setStock(product.getStock() - count);
productMapper.updateById(product);
return true;
}
public BigDecimal getPrice(Long productId) {
Product product = productMapper.selectById(productId);
return product != null ? product.getPrice() : BigDecimal.ZERO;
}
}
ProductController:
@RestController
@RequestMapping("/product")
public class ProductController {
@Autowired
private ProductService productService;
@PostMapping("/decreaseStock")
public boolean decreaseStock(@RequestBody Map<String, Object> params) {
Long productId = Long.valueOf(params.get("productId").toString());
Integer count = Integer.valueOf(params.get("count").toString());
return productService.decreaseStock(productId, count);
}
@GetMapping("/price/{productId}")
public BigDecimal getPrice(@PathVariable Long productId) {
return productService.getPrice(productId);
}
}
步骤4:创建账户服务
AccountService:
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
@Transactional
public boolean decrease(Long userId, BigDecimal money) {
// 查询账户
QueryWrapper<Account> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
Account account = accountMapper.selectOne(wrapper);
if (account == null) {
throw new RuntimeException("账户不存在");
}
// 检查余额
if (account.getBalance().compareTo(money) < 0) {
throw new RuntimeException("余额不足");
}
// 扣余额
account.setBalance(account.getBalance().subtract(money));
accountMapper.updateById(account);
return true;
}
}
六、测试分布式事务
测试1:正常流程
# 1. 启动所有服务
# 2. 调用下单接口
POST http://localhost:8081/order/create?userId=1&productId=1&count=2
# 预期结果:
# 订单创建成功
# 库存减少2
# 余额减少30(15*2)
测试2:余额不足
// 在AccountService的decrease方法里模拟异常
@Transactional
public boolean decrease(Long userId, BigDecimal money) {
// ... 正常逻辑
// 模拟余额不足
if (account.getBalance().compareTo(new BigDecimal("10000")) < 0) {
throw new RuntimeException("模拟异常:余额不足");
}
// ...
}
调用下单接口:
- 订单创建了(会回滚)
- 库存扣了(会回滚)
- 余额没扣(因为异常)
- 最终:全部回滚,数据一致
测试3:查看Seata事务
- 访问Seata控制台:
http://localhost:7091 - 用户名:seata
- 密码:seata
- 查看事务列表,能看到事务状态
七、Seata AT模式原理
第一阶段:提交
-- 业务SQL
UPDATE product SET stock = stock - 1 WHERE id = 1;
-- Seata自动做:
-- 1. 查询前镜像(before image)
SELECT * FROM product WHERE id = 1;
-- 2. 执行业务SQL
UPDATE product SET stock = stock - 1 WHERE id = 1;
-- 3. 查询后镜像(after image)
SELECT * FROM product WHERE id = 1;
-- 4. 生成UNDO_LOG
INSERT INTO undo_log (xid, branch_id, rollback_info, log_status)
VALUES ('全局事务ID', '分支事务ID', '{"beforeImage":..., "afterImage":...}', 1);
第二阶段:
- 提交:删除UNDO_LOG
- 回滚:用UNDO_LOG恢复数据
八、实际开发注意事项
1. 全局事务ID传递
// Feign拦截器传递XID
@Component
public class SeataFeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String xid = RootContext.getXID();
if (StringUtils.isNotBlank(xid)) {
template.header(RootContext.KEY_XID, xid);
}
}
}
2. 避免分布式事务大事务
// 不好的写法:事务太大
@GlobalTransactional
public void bigTransaction() {
// 1. 复杂业务逻辑(30秒)
// 2. RPC调用(5秒)
// 3. 数据库操作(10秒)
// 总时长45秒,容易超时
}
// 好的写法:拆分事务
public void business() {
// 1. 本地准备
prepare();
// 2. 分布式事务只包含必要操作
distributedTransaction();
// 3. 后续处理
postProcess();
}
@GlobalTransactional(timeoutMills = 10000) // 设置超时时间
public void distributedTransaction() {
// 只包含必须一起成功/失败的操作
rpcCall1();
rpcCall2();
}
3. 事务隔离级别问题
Seata AT模式默认是读未提交,需要业务上注意:
// 问题:脏读
// 事务A减库存,还没提交
// 事务B查库存,看到减少了,但其实可能回滚
// 解决方案1:使用SELECT FOR UPDATE(全局锁)
@GlobalTransactional
public void updateStock() {
// 先加锁
productMapper.selectForUpdate(productId);
// 再操作
// ...
}
// 解决方案2:使用SELECT ... FOR UPDATE
@GlobalLock // Seata提供的注解
@GlobalTransactional
public void updateStock() {
// ...
}
4. 幂等性设计
@Service
public class OrderService {
@GlobalTransactional
public Order createOrder(Long userId, Long productId, Integer count, String requestId) {
// 1. 检查是否已处理(幂等)
Order existOrder = orderMapper.selectByRequestId(requestId);
if (existOrder != null) {
return existOrder; // 直接返回已创建的订单
}
// 2. 创建订单
Order order = new Order();
order.setRequestId(requestId); // 唯一请求ID
// ...
}
}
九、Seata其他模式
TCC模式(适合复杂业务)
// Try接口
public interface AccountTccService {
@TwoPhaseBusinessAction(name = "decrease", commitMethod = "confirm", rollbackMethod = "cancel")
boolean prepareDecrease(@BusinessActionContextParameter(paramName = "userId") Long userId,
@BusinessActionContextParameter(paramName = "money") BigDecimal money);
// Confirm接口
boolean confirm(BusinessActionContext context);
// Cancel接口
boolean cancel(BusinessActionContext context);
}
// 实现:冻结金额,而不是直接扣
@Service
public class AccountTccServiceImpl implements AccountTccService {
@Transactional
public boolean prepareDecrease(Long userId, BigDecimal money) {
// 冻结金额
update account set balance = balance - #{money},
frozen = frozen + #{money}
where user_id = #{userId};
return true;
}
@Transactional
public boolean confirm(BusinessActionContext context) {
// 确认:删除冻结金额
update account set frozen = frozen - #{money}
where user_id = #{userId};
return true;
}
@Transactional
public boolean cancel(BusinessActionContext context) {
// 取消:恢复金额
update account set balance = balance + #{money},
frozen = frozen - #{money}
where user_id = #{userId};
return true;
}
}
Saga模式(长事务)
- 每个步骤都有补偿操作
- 适合业务流程长的场景
- 如:订机票 -> 订酒店 -> 租车
十、常见问题
1. Seata连接失败
io.seata.common.exception.FrameworkException: can not connect to services-server.
解决:
- 检查Seata Server启动了没
- 检查Nacos里有没有Seata Server
- 检查配置的
tx-service-group对不对
2. 全局事务不生效
可能原因:
- 没加
@GlobalTransactional - 异常被捕获了(要抛出RuntimeException)
- 嵌套事务问题
- 超时了
3. 数据源代理失败
No qualifying bean of type 'javax.sql.DataSource'
解决:
seata:
enable-auto-data-source-proxy: false # 关闭自动代理
@Configuration
public class DataSourceConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Primary
@Bean("dataSource")
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource); // 手动代理
}
}
4. UNDO_LOG表没数据
可能原因:
- 表结构不对
- 不是自动代理的数据源
- SQL不支持(如DDL语句)
十一、生产环境建议
1. Seata Server高可用
# 集群部署
seata:
service:
vgroup-mapping:
my_tx_group: default # 事务组映射到集群
registry:
type: nacos
nacos:
cluster: seata-cluster # 集群名
2. 数据库模式存储
# file.conf
store {
mode = "db"
db {
datasource = "druid"
db-type = "mysql"
driver-class-name = "com.mysql.cj.jdbc.Driver"
url = "jdbc:mysql://127.0.0.1:3306/seata?useSSL=false"
user = "root"
password = "123456"
}
}
3. 监控告警
// 监控事务成功率
@RestController
@RequestMapping("/seata")
public class SeataMonitorController {
@Autowired
private DefaultCoordinator coordinator;
@GetMapping("/metrics")
public Map<String, Object> getMetrics() {
Map<String, Object> metrics = new HashMap<>();
// 获取全局会话统计
SessionManager sessionManager = coordinator.getSessionManager();
metrics.put("activeSessions", sessionManager.getSessions().size());
metrics.put("committed", coordinator.getMetrics().getCommited());
metrics.put("rollbacked", coordinator.getMetrics().getRollbacked());
return metrics;
}
}
十二、今儿个总结
学会了啥?
- ✅ 分布式事务为啥重要
- ✅ Seata AT模式原理
- ✅ 搭建Seata环境
- ✅ @GlobalTransactional使用
- ✅ 异常回滚测试
- ✅ 生产环境注意事项
关键点
- @GlobalTransactional开全局事务
- UNDO_LOG表要创建
- 异常要抛出才能回滚
- 避免大事务,设置超时
- 幂等性设计防止重复
十三、明儿个学啥?
零基础全栈开发Java微服务版本实战-后端-前端-运维-实战企业级三个实战项目
资源获取:关注公众号: 小坏说Java ,获取本文所有示例代码、配置模板及导出工具。
明天咱学消息驱动!
- 服务之间除了HTTP调用,还能用消息
- 削峰填谷:双11订单不直接写数据库,先放消息队列
- 异步解耦:下单成功发短信,不用等短信发完
- 用Spring Cloud Stream操作消息队列
明天咱让服务之间写信(发消息),不用打电话(HTTP调用)!📨