mini-lsm通关笔记Week3Day6
现在,我们将在事务提交时添加一个冲突检测算法,以便使引擎具有一定程度的可序列化性。
要运行测试用例,请执行以下操作:
cargo x copy-test --week 3 --day 6
cargo x scheck
让我们来看一个可序列化的例子。假设我们在引擎中有两个事务:
txn1: put("key1", get("key2"))
txn2: put("key2", get("key1"))
数据库的初始状态是key1=1, key2=2
。Serializable是指执行的结果与按照某种顺序一个一个地串行执行事务的结果是相同的。如果我们执行txn1
,然后执行txn2
,我们将得到key1=2, key2=2
。如果我们执行txn2
,然后执行txn1
,我们将得到key1=1, key2=1
。
但是,在我们当前的实现中,如果这两个事务的执行有重叠:
txn1: get key2 <- 2
txn2: get key1 <- 1
txn1: put key1=2, commit
txn2: put key2=1, commit
我们将得到key1=2, key2=1
。这不能通过这两个事务的串行执行而产生。这种现象称为写倾斜。
使用可序列化验证,我们可以确保对数据库的修改对应于串行执行顺序,因此,用户可能会在需要可序列化执行的系统上运行一些关键工作负载。例如,如果用户在Mini-LSM上运行银行转账工作负载,他们将期望在任何时间点的金额都是相同的。没有可序列化检查,我们无法保证这个不变性。
可序列化验证的一种技术是记录系统中每个事务的读集合和写集合。我们在提交事务之前进行验证(乐观并发控制)。如果事务的读集合(read set
)与在其读取时间戳之后提交的任何事务重叠,则验证失败,并中止事务。
回到上面的例子,如果我们的txn1和txn2都是在timestamp = 1开始的。
txn1: get key2 <- 2
txn2: get key1 <- 1
txn1: put key1=2, commit ts = 2
txn2: put key2=1, start serializable verification
当我们验证txn2时,我们将遍历在其本身的预期提交时间戳之前和其读取时间戳之后(在本例中,1 < ts < 3)启动的所有事务。唯一满足条件的事务是txn1。txn1的写集合为key1
,txn2的读集合为key1
。由于它们重叠,我们应该中止txn2。
Task 1-Track Read Set in Get and Write Set
在此任务中,您需要修改:
src/mvcc/txn.rs src/mvcc.rs
当调用
get
时,应该将key添加到事务的读集合(read set
)中。在我们的实现中,我们存储键的哈希值,以便减少内存使用并使读取读集合(read set
)更快,尽管当两个键具有相同的哈希值时,这可能会导致误报。您可以使用farmhash::hash32
来生成密钥的哈希。需要注意的是,即使get
返回一个键未找到,这个键仍然应该在读集合中被跟踪。在
LsmMvccInner::new_txn
中,如果serializable=true
,则应该为事务创建一个空的读写集。
LsmMvccInner::new_txn
新增初始化,根据serializable
变量是否为true
决定key_hashes
的赋值:
pub fn new_txn(&self, inner: Arc<LsmStorageInner>, serializable: bool) -> Arc<Transaction> {
...
Arc::new(Transaction {
...
key_hashes: if serializable {
Some(Mutex::new((HashSet::new(), HashSet::new())))
} else {
None
},
})
}
get
函数改造,在判断committed
后,local_storage
查找前进行读集合(read set
)的记录:
if self.committed.load(Ordering::SeqCst) {
panic!("cannot operate on committed txn!");
}
// 【新增】get操作,记录读集
if let Some(guard) = &self.key_hashes {
let guard = guard.lock();
let (_, read_set) = &mut *guard;
read_set.insert(farmhash::hash32(key));
}
if let Some(entry) = self.local_storage.get(key) {
put
、delete
操作函数改造,在修改完后进行写集合(write set
)的记录:
self.local_storage
.insert(Bytes::copy_from_slice(key), ....);
// 【新增】put、delete操作,记录写集
if let Some(key_hashes) = &self.key_hashes {
let mut key_hashes = key_hashes.lock();
let (write_hashes, _) = &mut *key_hashes;
write_hashes.insert(farmhash::hash32(key));
}
Task 2-Track Read Set in Scan
在此任务中,您需要修改:
src/mvcc/txn.rs
在本章中,我们只保证
get
请求的完全可序列化。您仍然需要在scan
操作中跟踪读集合(read set
),但在某些特定情况下,您仍然可能获得不可序列化的结果。为了理解为什么这很难,让我们通过下面的示例。
txn1: put("key1", len(scan(..))) txn2: put("key2", len(scan(..)))
如果数据库以初始状态
a=1,b=2
开始,我们应该得到a=1,b=2,key1=2,key2=3
或a=1,b=2,key1=3,key2=2
。但是,如果事务执行如下:
txn1: len(scan(..)) = 2 txn2: len(scan(..)) = 2 txn1: put key1 = 2, commit, read set = {a, b}, write set = {key1} txn2: put key2 = 2, commit, read set = {a, b}, write set = {key2}
这通过了我们的可序列化验证,并不对应任何串行执行顺序!因此,一个完全工作的可序列化验证将需要跟踪键范围,如果只调用
get
,使用键哈希可以加快可序列化检查。关于如何正确实现可序列化检查,请参考附加任务。
本任务就是scan
操作中进行读取时记录读集合(read set
)。
TxnIterator
中新增一个辅助函数add_to_read_set
,内容和get
中记录:
fn add_to_read_set(&self, key: &[u8]) {
if let Some(guard) = &self._txn.key_hashes {
let mut guard = guard.lock();
let (_, read_set) = &mut *guard;
read_set.insert(farmhash::hash32(key));
}
}
TxnIterator
构造函数:
iter.skip_deletes()?;
// 【新增】记录读集
if iter.is_valid() {
iter.add_to_read_set(iter.key());
}
Ok(iter)
TxnIterator::next
函数:
fn next(&mut self) -> Result<()> {
self.iter.next()?;
self.skip_deletes()?;
// 【新增】记录读集
if self.is_valid() {
self.add_to_read_set(self.key());
}
Ok(())
}
Task 3-Engine Interface and Serializable Validation
在此任务中,您需要修改:
src/mvcc/txn.rs src/lsm_storage.rs
现在,我们可以继续在
commit
阶段实现验证。每次我们处理事务提交时,都应该使用commit_lock
。这样可以确保只有一个事务进入事务验证和提交阶段。您需要遍历所有提交时间戳在范围
(read_ts,expected_commit_ts)
内的事务(都是排除边界),并查看当前事务的读集是否与任何满足条件的事务的写集重叠。如果我们可以提交事务,提交一个写批处理,并将这个事务的写集合插入到self.inner.mvcc().committed_txns
中,其中的key
是提交时间戳。如果
write_set
为空,则可以跳过检查。只读事务始终可以提交。您还应该修改
LsmStorageInner
中的put
、delete
和write_batch
接口。我们建议你定义一个帮助函数write_batch_internal
来处理写批处理。如果options.serializable = true
,则put
、delete
和面向用户的write_batch
应该创建一个事务,而不是直接创建一个写批处理。你的write batch helper
函数还应该返回一个u64
提交时间戳,以便Transaction::Commit
可以正确地将提交的事务数据存储到MVCC结构中。
write_batch改造
首先将原来的write_batch
函数名修改为write_batch_inner
,同时修改返回值为Result<u64>
,再将提交时的ts
返回
pub fn write_batch_inner<T: AsRef<[u8]>>(&self, _batch: &[WriteBatchRecord<T>]) -> Result<u64> {
...
self.mvcc().update_commit_ts(ts);
Ok(ts) // 【修改】返回提交的时间戳
}
新write_batch
函数:
pub fn write_batch<T: AsRef<[u8]>>(
self: &Arc<Self>,
batch: &[WriteBatchRecord<T>],
) -> Result<()> {
if !self.options.serializable {
// 原逻辑
self.write_batch_inner(batch)?;
} else {
// 暴露给外部,需要为其创建一个事务
let txn = self.mvcc().new_txn(self.clone(), self.options.serializable);
for record in batch {
match record {
WriteBatchRecord::Del(key) => {
txn.delete(key.as_ref()); // 走事务处理
}
WriteBatchRecord::Put(key, value) => {
txn.put(key.as_ref(), value.as_ref()); // 走事务处理
}
}
}
txn.commit()?; // 批量提交
}
Ok(())
}
同时仿照上面的修改,对put
函数进行修改:
pub fn put(self: &Arc<Self>, key: &[u8], value: &[u8]) -> Result<()> {
if !self.options.serializable {
self.write_batch_inner(&[WriteBatchRecord::Put(key, value)])?;
} else {
let txn = self.mvcc().new_txn(self.clone(), self.options.serializable);
txn.put(key, value);
txn.commit()?;
}
Ok(())
}
修改delete
函数:
pub fn delete(self: &Arc<Self>, key: &[u8]) -> Result<()> {
if !self.options.serializable {
self.write_batch_inner(&[WriteBatchRecord::Del(key)])?;
} else {
let txn = self.mvcc().new_txn(self.clone(), self.options.serializable);
txn.delete(key);
txn.commit()?;
}
Ok(())
}
OCC校验
在提交批量修改任务前进行独写集的验证,就是校验当前事务的读记录,有没有在开始后被提交的事务修改过:
let _commit_lock = self.inner.mvcc().commit_lock.lock();
let serializability_check;
if let Some(guard) = &self.key_hashes {
let guard = guard.lock();
let (write_set, read_set) = &*guard;
println!(
"commit txn: write_set: {:?}, read_set: {:?}",
write_set, read_set
);
if !write_set.is_empty() { // 题目要求,自读事务总是可以提交成功
let committed_txns = self.inner.mvcc().committed_txns.lock();
for (_, txn_data) in committed_txns.range((self.read_ts + 1)..) {
for key_hash in read_set {
if txn_data.key_hashes.contains(key_hash) {
bail!("serializable check failed"); // 当前事务读取数据,已被其他事务修改,报错
}
}
}
}
serializability_check = true;
} else {
serializability_check = false;
}
执行完任务后,将本次的写集记录下来:
if serializability_check {
let mut committed_txns = self.inner.mvcc().committed_txns.lock();
let mut key_hashes = self.key_hashes.as_ref().unwrap().lock();
let (write_set, _) = &mut *key_hashes;
let old_data = committed_txns.insert(
ts,
CommittedTxnData {
key_hashes: std::mem::take(write_set),
read_ts: self.read_ts,
commit_ts: ts,
},
);
assert!(old_data.is_none());
}
Task 4-Garbage Collection
在此任务中,您需要修改:
src/mvcc/txn.rs
当您提交事务时,您还可以清理已提交的
txn
哈希表(committed_txns
)以移除watermark
以下的所有事务,因为它们将不会参与任何未来可序列化的验证。
添加到committed_txns.insert
后:
assert!(old_data.is_none());
// 移除无用记录
let watermark = self.inner.mvcc().watermark();
while let Some(entry) = committed_txns.first_entry() {
if *entry.key() < watermark {
entry.remove();
} else {
break;
}
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战