告别数据不一致:TiKV如何用两阶段提交保障事务原子性

告别数据不一致:TiKV如何用两阶段提交保障事务原子性

【免费下载链接】tikv TiKV 是一个分布式键值存储系统,用于存储大规模数据。 * 提供高性能、可扩展的分布式存储功能,支持事务和分布式锁,适用于大数据存储和分布式系统场景。 * 有什么特点:高性能、可扩展、支持事务和分布式锁、易于集成。 【免费下载链接】tikv 项目地址: https://gitcode.com/GitHub_Trending/ti/tikv

你是否曾遇到过这样的问题:转账时钱从A账户扣了,B账户却没到账?或者订单创建了,库存却没减少?这些都是分布式系统中事务原子性(Atomicity)保障失败的典型案例。TiKV作为一款高性能分布式键值存储,通过精妙的两阶段提交(Two-Phase Commit, 2PC)机制,确保即使在节点故障、网络分区的情况下,事务也能要么完全成功,要么完全失败。本文将用通俗的语言,结合TiKV源码解析,带你理解这一机制的工作原理。

读完本文你将掌握:

  • TiKV事务原子性的三大核心保障手段
  • 预写(Prewrite)阶段如何锁定资源并检测冲突
  • 提交(Commit)阶段如何确保数据一致性
  • 事务异常时的自动恢复机制

TiKV事务模型概览

TiKV的事务系统基于多版本并发控制(MVCC)两阶段提交构建,主要实现代码位于src/storage/txn/mod.rs。其核心设计目标是满足ACID特性中的原子性(Atomicity)和隔离性(Isolation),同时保持分布式环境下的高性能。

TiKV事务处理流程

事务在TiKV中经历以下生命周期:

  1. 开始阶段:客户端获取事务ID(start_ts)
  2. 预写阶段:锁定所有要修改的数据,并检查冲突
  3. 提交阶段:确认所有节点准备就绪,永久化修改
  4. 清理阶段:释放锁资源,删除临时数据

其中预写和提交两个阶段是保障原子性的关键,接下来我们将深入剖析这两个阶段的实现细节。

预写阶段:为事务"占座"

预写(Prewrite)就像你去餐厅吃饭时的"占座"行为——在正式用餐(提交)前,先确认座位(数据资源)是否可用,并暂时锁定它们。这一阶段的核心代码在src/storage/txn/actions/prewrite.rs中实现。

预写阶段的三大任务

  1. 冲突检测:检查要修改的数据是否已被其他事务锁定
  2. 锁定资源:为当前事务创建锁记录,防止并发修改
  3. 准备数据:将修改后的数据写入临时存储

下面是预写阶段的核心函数调用流程:

// 预写单个修改操作
pub fn prewrite<S: Snapshot>(
    txn: &mut MvccTxn,
    reader: &mut SnapshotReader<S>,
    txn_props: &TransactionProperties<'_>,
    mutation: Mutation,
    secondary_keys: &Option<Vec<Vec<u8>>>,
    pessimistic_action: PrewriteRequestPessimisticAction,
    expected_for_update_ts: Option<TimeStamp>,
) -> Result<(TimeStamp, OldValue)> {
    // ...实现细节...
}

锁机制:TiKV的"占位符"

TiKV使用锁记录(Lock) 来实现资源锁定,每个锁包含以下关键信息:

  • 事务ID(start_ts):标识锁的所有者
  • 主键(primary):事务的主锁所在位置
  • 过期时间(ttl):防止锁永久阻塞资源
  • 锁类型(lock_type):区分读写锁

锁的创建过程在write_lock方法中实现:

fn write_lock(
    self,
    lock_status: LockStatus,
    txn: &mut MvccTxn,
    is_new_lock: bool,
    generation: u64,
) -> Result<TimeStamp> {
    let mut lock = Lock::new(
        self.lock_type.unwrap(),
        self.txn_props.primary.to_vec(),
        self.txn_props.start_ts,
        self.lock_ttl,
        None,
        for_update_ts_to_write,
        self.txn_props.txn_size,
        self.min_commit_ts,
        false,
    )
    // ...设置锁的其他属性...
    
    txn.put_lock(self.key, &lock, is_new_lock);
    // ...
}

冲突解决策略

当检测到数据已被其他事务锁定时,TiKV会根据冲突类型采取不同策略:

  • 乐观锁冲突:直接中止当前事务,返回冲突错误
  • 悲观锁冲突:根据锁的过期时间决定等待或中止
  • 死锁检测:通过锁依赖图检测死锁,主动中止其中一个事务

冲突检测的核心逻辑在check_lock方法中实现:

fn check_lock(
    &mut self,
    lock: Lock,
    pessimistic_action: PrewriteRequestPessimisticAction,
    expected_for_update_ts: Option<TimeStamp>,
    generation_to_write: u64,
) -> Result<LockStatus> {
    if lock.ts != self.txn_props.start_ts {
        // 发现其他事务的锁,处理冲突
        if matches!(pessimistic_action, DoPessimisticCheck) {
            return Err(ErrorInner::PessimisticLockNotFound {
                start_ts: self.txn_props.start_ts,
                key: self.key.to_raw()?,
                reason: PessimisticLockNotFoundReason::LockTsMismatch,
            }.into());
        }
    }
    // ...其他冲突处理逻辑...
}

提交阶段:事务的"最终确认"

预写成功后,事务进入提交阶段。这一阶段就像餐厅确认所有食材都已备齐,可以开始烹饪了。提交阶段的核心代码在src/storage/txn/actions/commit.rs中实现。

两阶段提交的精妙之处

TiKV的两阶段提交包含以下关键步骤:

  1. 提交主键:首先提交事务的主键(Primary Key)
  2. 提交次级键:然后异步提交所有次级键(Secondary Keys)
  3. 写入提交记录:在MVCC版本链中写入提交记录

这种设计的优势在于:只要主键提交成功,即使部分次级键提交失败,系统也能通过重试完成剩余提交,从而保障事务的最终一致性。

TiKV两阶段提交流程

提交阶段的核心实现

提交函数的核心逻辑如下:

pub fn commit<S: Snapshot>(
    txn: &mut MvccTxn,
    reader: &mut SnapshotReader<S>,
    key: Key,
    commit_ts: TimeStamp,
    commit_role: Option<CommitRole>,
) -> MvccResult<Option<ReleasedLock>> {
    // 检查锁是否存在且属于当前事务
    let (mut lock, commit) = match reader.load_lock(&key)? {
        Some(lock) if lock.ts == reader.start_ts => {
            // 检查提交时间戳是否有效
            if commit_ts < lock.min_commit_ts {
                return Err(ErrorInner::CommitTsExpired {
                    start_ts: reader.start_ts,
                    commit_ts,
                    key: key.into_raw()?,
                    min_commit_ts: lock.min_commit_ts,
                    mvcc_info,
                }.into());
            }
            (lock, true)
        }
        _ => {
            // 处理锁不存在的情况
            return match reader.get_txn_commit_record(&key)?.info() {
                // ...冲突处理逻辑...
            };
        }
    };
    
    // 写入提交记录
    txn.put_write(key.clone(), commit_ts, write.as_ref().to_bytes());
    // 释放锁
    Ok(txn.unlock_key(key, lock.is_pessimistic_txn(), commit_ts))
}

异常处理:事务的"安全网"

即使有了两阶段提交,分布式系统仍可能出现各种异常情况。TiKV通过以下机制确保事务原子性:

  1. 超时机制:锁记录设置TTL,超时后自动释放
  2. 后台清理:定期扫描并清理过期锁和未完成事务
  3. 乐观重试:客户端检测到冲突后自动重试事务
  4. 悲观锁定:对热点数据采用悲观锁减少冲突

这些机制共同构成了TiKV事务系统的"安全网",确保在各种异常情况下仍能保持数据一致性。

事务恢复:从故障中自动恢复

当集群出现节点宕机、网络分区等故障时,TiKV的事务恢复机制会自动介入,确保所有事务最终达到一致状态。核心恢复逻辑在src/storage/txn/commands/resolve_lock.rs中实现。

事务恢复的两种场景

  1. 未完成事务清理:识别并回滚长时间未提交的事务
  2. 部分提交修复:确保所有次级键要么都提交,要么都回滚

TiKV使用异步解析锁(Async Resolve Lock)机制处理这些场景:后台线程定期扫描存储的锁记录,对超过TTL的锁进行处理——如果主键已提交,则提交次级键;如果主键未提交,则回滚整个事务。

TiKV监控架构

实践案例:电商订单系统中的事务保障

为了更好地理解TiKV事务原子性保障的实际应用,我们以电商订单系统为例:

  1. 创建订单:一个典型订单事务包含扣减库存、创建订单记录、增加用户积分等操作
  2. 预写阶段:同时锁定库存记录、用户账户和订单表
  3. 提交阶段:确认所有锁定成功后,永久化所有修改
  4. 异常处理:如果任何步骤失败,所有修改自动回滚

在高并发场景下,TiKV通过乐观锁+悲观锁混合策略优化性能:对库存等热点资源使用悲观锁,对订单等冷数据使用乐观锁,既保证了数据一致性,又最大化了系统吞吐量。

总结与最佳实践

TiKV通过两阶段提交和MVCC的结合,在分布式环境下实现了高效的事务原子性保障。核心要点包括:

  1. 预写阶段:锁定资源并检测冲突,对应代码实现src/storage/txn/actions/prewrite.rs
  2. 提交阶段:原子化确认所有修改,核心逻辑在src/storage/txn/actions/commit.rs
  3. 恢复机制:后台线程自动处理异常事务,确保最终一致性

事务使用最佳实践

  1. 控制事务大小:大事务会增加冲突概率和恢复成本
  2. 合理设置隔离级别:读已提交(RC)适合报表查询,可串行化(Serializable)适合财务操作
  3. 避免长事务:长时间运行的事务容易导致锁争用和冲突
  4. 热点数据处理:对热点资源使用悲观锁或拆分热点

TiKV的事务系统是一个复杂而精妙的工程实现,它在性能和一致性之间取得了很好的平衡。通过深入理解这些内部机制,开发者可以更好地利用TiKV构建可靠的分布式应用。

如果你想进一步探索TiKV事务实现细节,可以从以下资源入手:

关注TiKV GitHub仓库,获取最新的功能更新和技术演进:https://gitcode.com/GitHub_Trending/ti/tikv

点赞+收藏+关注,不错过分布式存储技术深度解析!下期预告:TiKV事务隔离级别实现原理

【免费下载链接】tikv TiKV 是一个分布式键值存储系统,用于存储大规模数据。 * 提供高性能、可扩展的分布式存储功能,支持事务和分布式锁,适用于大数据存储和分布式系统场景。 * 有什么特点:高性能、可扩展、支持事务和分布式锁、易于集成。 【免费下载链接】tikv 项目地址: https://gitcode.com/GitHub_Trending/ti/tikv

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值