Seal^_^头像
关注
Seata AT模式脏写问题与解决方案详解封面图

Seata AT模式脏写问题与解决方案详解


🌺The Begin🌺点点关注,收藏不迷路🌺

引言:AT模式的并发挑战

AT模式虽然通过"一阶段提交、二阶段回滚"大幅提升了性能,但也带来了新的问题:在多线程并发访问时,可能出现脏写

并发问题场景

事务1:扣款10元

读取余额:100

事务2:扣款20元

读取余额:100

事务1更新为90并提交

事务2更新为80并提交

事务1需要回滚

根据快照恢复为100

实际余额应为80
导致脏写!


一、脏写问题剖析 🔍

1.1 问题场景复现

假设有一个账户表,初始余额为100:

-- 初始数据
id = 1, money = 100

两个并发事务同时执行

时间事务1(需回滚)事务2(正常提交)实际余额
T1开启全局事务开启全局事务100
T2读取快照:money=100读取快照:money=100100
T3执行SQL:UPDATE account SET money = money - 10 WHERE id = 1等待90(事务1提交)
T4提交本地事务,释放DB锁获取DB锁90
T5-读取快照:money=9090
T6-执行SQL:UPDATE account SET money = money - 20 WHERE id = 170
T7-提交本地事务70
T8事务1需要回滚!-70
T9根据快照将余额恢复为100-100(错误!)

1.2 问题根源

根本原因:事务1提交释放DB锁后,事务2修改了数据,但事务1的快照仍记录着旧值。当事务1需要回滚时,它不知道数据已被事务2修改,导致脏写。


二、解决方案:全局锁 🛡️

2.1 核心思路

解决思路就是引入了全局锁的概念。在释放DB锁之前,先拿到全局锁。避免同一时刻有另外一个事务来操作当前数据。

数据库 全局锁 事务2 事务1 TC 数据库 全局锁 事务2 事务1 TC 1. 申请全局锁 获取成功 2. 执行业务SQL 3. 提交事务(释放DB锁) 4. 申请全局锁 等待(被事务1持有) 5. 二阶段结束 6. 释放全局锁 7. 获取成功 8. 执行业务

2.2 全局锁的实现

Seata通过全局锁表来管理锁:

-- 全局锁表结构
CREATE TABLE `lock_table` (
  `row_key` varchar(128) NOT NULL,        -- 资源ID:如 "account:1"
  `xid` varchar(128) NOT NULL,            -- 持有锁的全局事务ID
  `transaction_id` bigint(20) DEFAULT NULL,
  `branch_id` bigint(20) NOT NULL,
  `resource_id` varchar(256) DEFAULT NULL,
  `table_name` varchar(32) DEFAULT NULL,
  `pk` varchar(36) DEFAULT NULL,
  `status` tinyint(4) NOT NULL DEFAULT '0', -- 0:锁定 1:待定
  `gmt_create` datetime DEFAULT NULL,
  `gmt_modified` datetime DEFAULT NULL,
  PRIMARY KEY (`row_key`),
  KEY `idx_xid` (`xid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.3 全局锁的工作流程

public class GlobalLockManager {
    
    // 事务执行前申请全局锁
    public boolean acquireLock(String xid, String resourceId) {
        String lockKey = buildLockKey(resourceId);
        
        // 插入锁记录,利用数据库唯一索引实现互斥
        try {
            lockDao.insert(lockKey, xid);
            return true; // 获取成功
        } catch (DuplicateKeyException e) {
            // 锁已被其他事务持有
            return false;
        }
    }
    
    // 事务结束后释放锁
    public void releaseLock(String xid) {
        lockDao.deleteByXid(xid);
    }
}

三、极端情况:非Seata事务造成的脏读 ⚠️

3.1 问题场景

但是也可能在一个极端的情况下造成脏读,比如一个非Seata管理的全局事务,在事务1提交事务释放DB锁之后获取了DB锁,从而造成脏写问题。

数据库 非Seata事务 Seata事务1 数据库 非Seata事务 Seata事务1 5. 事务1需要回滚 数据被非Seata事务修改过 恢复后的数据可能错误 1. 执行业务SQL 2. 提交事务(释放DB锁) 3. 获取DB锁 4. 修改数据 6. 根据快照恢复数据

3.2 解决方案

这时事务1会根据快照数据发现异常,发出警告,进行人工介入。

public class DataConsistencyChecker {
    
    public void checkBeforeRollback(UndoLog log, Connection currentConn) {
        // 1. 查询当前数据
        TableRecords currentData = queryCurrentData(log.getResourceId());
        
        // 2. 与后镜像对比
        if (!currentData.equals(log.getAfterImage())) {
            // 数据已被修改,记录警告日志
            logger.error("数据已被其他事务修改,需要人工介入!");
            logger.error("预期后镜像:{}", log.getAfterImage());
            logger.error("实际数据:{}", currentData);
            
            // 3. 发送告警
            alertService.send("数据不一致", log.getXid());
            
            // 4. 不能自动回滚,等待人工处理
            throw new DataConsistencyException("数据已被外部修改");
        }
        
        // 5. 正常回滚
        doRollback(log);
    }
}

四、AT模式优缺点总结 📊

4.1 优点

优点说明
性能好一阶段完成直接提交事务,释放数据库资源
读写隔离利用全局锁实现隔离性
无代码侵入框架自动完成回滚和提交,只需一个注解

4.2 缺点

缺点说明
最终一致两阶段之间属于软状态,不是强一致
快照性能开销框架的快照功能会影响性能,但比XA模式好很多
极端情况需人工与非Seata事务并发时可能需人工介入

五、与其他模式对比 📊

模式一致性性能隔离性人工介入风险
XA强一致全局锁
AT最终一致全局锁中(极端情况)
TCC最终一致业务保证

六、最佳实践建议 💡

6.1 使用建议

public class ATModeAdvice {
    
    public void recommend() {
        // 1. 优先使用AT模式,性能好且无侵入
        
        // 2. 关键业务考虑防悬挂策略
        @GlobalTransactional(lockRetryInterval = 10, lockRetryTimes = 30)
        public void criticalBusiness() {
            // 设置合理的锁重试参数
        }
        
        // 3. 监控警告
        @EventListener
        public void handleDataInconsistency(DataInconsistencyEvent event) {
            // 及时处理人工介入请求
        }
    }
}

6.2 配置优化

seata:
  client:
    rm:
      lock:
        retry-interval: 10      # 重试间隔(ms)
        retry-times: 30         # 重试次数
        retry-policy-branch-rollback-on-conflict: true

七、总结 🎯

7.1 一句话总结

AT模式通过全局锁解决脏写问题,但在与非Seata事务并发时可能产生数据不一致,需要人工介入。这是高性能分布式事务在极端情况下的必要妥协。

7.2 核心要点

要点说明
脏写原因一阶段提交后数据被其他事务修改
解决方案引入全局锁,保证事务间隔离
极端情况非Seata事务绕过全局锁
处理方式快照比对,发现异常后人工介入

7.3 最终建议

public class FinalAdvice {
    
    public String getAdvice() {
        return "1. 绝大多数场景AT模式足够安全\n"
             + "2. 配置合理的锁重试参数\n"
             + "3. 监控数据不一致告警\n"
             + "4. 关键业务考虑使用TCC或XA";
    }
}

(本文为分布式系统事务系列文章,欢迎关注更多架构深度内容)

在这里插入图片描述


🌺The End🌺点点关注,收藏不迷路🌺

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/qq_41840843/article/details/158390861

文章来源crawl

评论

赞0

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--