告别数据不一致: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中经历以下生命周期:
- 开始阶段:客户端获取事务ID(start_ts)
- 预写阶段:锁定所有要修改的数据,并检查冲突
- 提交阶段:确认所有节点准备就绪,永久化修改
- 清理阶段:释放锁资源,删除临时数据
其中预写和提交两个阶段是保障原子性的关键,接下来我们将深入剖析这两个阶段的实现细节。
预写阶段:为事务"占座"
预写(Prewrite)就像你去餐厅吃饭时的"占座"行为——在正式用餐(提交)前,先确认座位(数据资源)是否可用,并暂时锁定它们。这一阶段的核心代码在src/storage/txn/actions/prewrite.rs中实现。
预写阶段的三大任务
- 冲突检测:检查要修改的数据是否已被其他事务锁定
- 锁定资源:为当前事务创建锁记录,防止并发修改
- 准备数据:将修改后的数据写入临时存储
下面是预写阶段的核心函数调用流程:
// 预写单个修改操作
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的两阶段提交包含以下关键步骤:
- 提交主键:首先提交事务的主键(Primary Key)
- 提交次级键:然后异步提交所有次级键(Secondary Keys)
- 写入提交记录:在MVCC版本链中写入提交记录
这种设计的优势在于:只要主键提交成功,即使部分次级键提交失败,系统也能通过重试完成剩余提交,从而保障事务的最终一致性。
提交阶段的核心实现
提交函数的核心逻辑如下:
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通过以下机制确保事务原子性:
- 超时机制:锁记录设置TTL,超时后自动释放
- 后台清理:定期扫描并清理过期锁和未完成事务
- 乐观重试:客户端检测到冲突后自动重试事务
- 悲观锁定:对热点数据采用悲观锁减少冲突
这些机制共同构成了TiKV事务系统的"安全网",确保在各种异常情况下仍能保持数据一致性。
事务恢复:从故障中自动恢复
当集群出现节点宕机、网络分区等故障时,TiKV的事务恢复机制会自动介入,确保所有事务最终达到一致状态。核心恢复逻辑在src/storage/txn/commands/resolve_lock.rs中实现。
事务恢复的两种场景
- 未完成事务清理:识别并回滚长时间未提交的事务
- 部分提交修复:确保所有次级键要么都提交,要么都回滚
TiKV使用异步解析锁(Async Resolve Lock)机制处理这些场景:后台线程定期扫描存储的锁记录,对超过TTL的锁进行处理——如果主键已提交,则提交次级键;如果主键未提交,则回滚整个事务。
实践案例:电商订单系统中的事务保障
为了更好地理解TiKV事务原子性保障的实际应用,我们以电商订单系统为例:
- 创建订单:一个典型订单事务包含扣减库存、创建订单记录、增加用户积分等操作
- 预写阶段:同时锁定库存记录、用户账户和订单表
- 提交阶段:确认所有锁定成功后,永久化所有修改
- 异常处理:如果任何步骤失败,所有修改自动回滚
在高并发场景下,TiKV通过乐观锁+悲观锁混合策略优化性能:对库存等热点资源使用悲观锁,对订单等冷数据使用乐观锁,既保证了数据一致性,又最大化了系统吞吐量。
总结与最佳实践
TiKV通过两阶段提交和MVCC的结合,在分布式环境下实现了高效的事务原子性保障。核心要点包括:
- 预写阶段:锁定资源并检测冲突,对应代码实现src/storage/txn/actions/prewrite.rs
- 提交阶段:原子化确认所有修改,核心逻辑在src/storage/txn/actions/commit.rs
- 恢复机制:后台线程自动处理异常事务,确保最终一致性
事务使用最佳实践
- 控制事务大小:大事务会增加冲突概率和恢复成本
- 合理设置隔离级别:读已提交(RC)适合报表查询,可串行化(Serializable)适合财务操作
- 避免长事务:长时间运行的事务容易导致锁争用和冲突
- 热点数据处理:对热点资源使用悲观锁或拆分热点
TiKV的事务系统是一个复杂而精妙的工程实现,它在性能和一致性之间取得了很好的平衡。通过深入理解这些内部机制,开发者可以更好地利用TiKV构建可靠的分布式应用。
如果你想进一步探索TiKV事务实现细节,可以从以下资源入手:
- 官方文档:doc/deploy.md
- 事务测试用例:tests/integrations/txn/
- 性能调优指南:PERFORMANCE_CRITICAL_PATH.md
关注TiKV GitHub仓库,获取最新的功能更新和技术演进:https://gitcode.com/GitHub_Trending/ti/tikv
点赞+收藏+关注,不错过分布式存储技术深度解析!下期预告:TiKV事务隔离级别实现原理
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考






