【Easylive】使用Seata解决分布式事务问题
【Easylive】项目常见问题解答(自用&持续更新中…) 汇总版
1. 为什么@Transactional
在跨服务调用时不生效?
技术本质(结合代码示例):
在postComment
方法中:
// 本地数据库操作(可被@Transactional管理)
videoCommentMapper.insert(comment); // 跨服务调用(不受@Transactional控制)
videoClient.updateCountInfo(comment.getVideoId(),...);
问题根源:
- 事务隔离性:
•@Transactional
只能管理当前服务的数据库连接
•videoClient.updateCountInfo()
通过HTTP调用其他服务,属于独立事务 - 两阶段问题:
• 阶段1:本地insert
成功提交
• 阶段2:远程调用失败时,本地事务无法自动回滚 - CAP理论限制:
• 跨服务操作涉及网络分区容忍性,传统事务模型无法保证CP
生活化比喻:
就像你网购时:
- 商家(服务A)确认发货(本地事务提交)
- 但快递(服务B)丢件了(远程调用失败)
- 没有平台(Seata)协调的话,钱货两失!
2. Seata解决方案全流程
第一步:启动Seata服务
- 下载Seata Server(1.6.1+)
- 配置注册中心(修改
conf/registry.conf
):registry {type = "nacos"nacos { serverAddr = "127.0.0.1:8848"namespace = "你的命名空间" # 可选} }
- 启动:
bin/seata-server.sh -p 8091 -h 127.0.0.1
第二步:数据库准备
在每个业务库执行:
-- Seata核心表(用于事务协调)
CREATE TABLE IF NOT EXISTS `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 DEFAULT CHARSET=utf8;-- Seata Server需要的表(在独立数据库执行)
CREATE TABLE `global_table` (...);
CREATE TABLE `branch_table` (...);
CREATE TABLE `lock_table` (...);
第三步:项目配置
-
添加依赖(所有微服务):
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-seata</artifactId><version>2.2.6.RELEASE</version> </dependency>
-
Nacos配置(以
video-service
为例):spring:cloud:alibaba:seata:tx-service-group: video-service-group # 与Seata Server配置一致seata:registry:type: nacosnacos:server-addr: 127.0.0.1:8848config:type: nacos
第四步:代码改造
在postComment
方法上:
@GlobalTransactional(rollbackFor = Exception.class, // 所有异常都回滚timeoutMills = 60000, // 超时时间(毫秒)name = "postCommentTx" // 全局事务名(可查日志)
)
public void postComment(VideoComment comment, Integer replyCommentId) {// 原业务逻辑不变// Seata会自动拦截以下操作:// 1. videoCommentMapper.insert()// 2. videoClient.updateCountInfo()
}
关键机制:
- 事务ID传播:
• Seata通过XID
(全局事务ID)串联各服务
• 自动通过Feign请求头传递seata_xid=123456
- 二阶段提交:
第一阶段:准备阶段(Prepare)
第二阶段:提交/回滚(Commit/Rollback)
- 回滚原理:
• 通过undo_log
表中的回滚日志反向补偿
• 例如:删除已插入的评论记录
- 验证与排查技巧
验证步骤: - 查看Seata控制台:
• 访问http://127.0.0.1:7091
• 检查事务列表是否包含postCommentTx
- 强制触发异常:
• 观察本地// 在updateCountInfo()中模拟失败 if (Math.random() > 0.5) {throw new RuntimeException("模拟远程调用失败"); }
video_comment
表是否回滚
常见问题解决:
问题现象 | 可能原因 | 解决方案 |
---|---|---|
No available service for cluster | 事务组名不匹配 | 检查tx-service-group 一致性 |
Could not register branch | 数据库未建undo_log | 执行建表SQL |
回滚失效 | 方法内捕获异常 | 确保异常抛出到@GlobalTransactional 层 |
生活化总结
Seata就像跨国贸易的支付宝:
- 买家付款(本地事务)→ 资金暂存平台(Phase1)
- 卖家发货(远程调用)→ 平台监控物流(Phase1)
- 确认收货后双方结算(Phase2 Commit)
- 如果卖家不发货,平台退款(Phase2 Rollback)
通过这套机制,postComment
方法就像有了一个全自动保险,无论评论数据保存和计数更新相隔多远,都能保证要么全部成功,要么全部回滚。
代码
使用@GlobalTransactional注解
@Override
@GlobalTransactional(name = "postCommentTx", rollbackFor = Exception.class)
public void postComment(VideoComment comment, Integer replyCommentId) {// 1. 获取视频信息VideoInfo videoInfo = videoClient.getVideoInfoByVideoId(comment.getVideoId());if (videoInfo == null) {throw new BusinessException(ResponseCodeEnum.CODE_600);}// 2. 检查评论是否关闭if (videoInfo.getInteraction() != null && videoInfo.getInteraction().contains(Constants.ZERO.toString())) {throw new BusinessException("UP主已关闭评论区");}// 3. 处理回复评论逻辑if (replyCommentId != null) {VideoComment replyComment = getVideoCommentByCommentId(replyCommentId);if (replyComment == null || !replyComment.getVideoId().equals(comment.getVideoId())) {throw new BusinessException(ResponseCodeEnum.CODE_600);}if (replyComment.getpCommentId() == 0) {comment.setpCommentId(replyComment.getCommentId());} else {comment.setpCommentId(replyComment.getpCommentId());comment.setReplyUserId(replyComment.getUserId());}UserInfo userInfo = videoClient.getUserInfoByUserId(replyComment.getUserId());comment.setReplyNickName(userInfo.getNickName());comment.setReplyAvatar(userInfo.getAvatar());} else {comment.setpCommentId(0);}// 4. 设置评论信息comment.setPostTime(new Date());comment.setVideoUserId(videoInfo.getUserId());// 5. 插入评论this.videoCommentMapper.insert(comment);// 6. 更新评论计数if (comment.getpCommentId() == 0) {this.videoClient.updateCountInfo(comment.getVideoId(), UserActionTypeEnum.VIDEO_COMMENT.getField(), 1);}
}