mini-lsm通关笔记Week3Day6

项目地址:https://github.com/skyzh/mini-lsm

个人实现地址:https://gitee.com/cnyuyang/mini-lsm

现在,我们将在事务提交时添加一个冲突检测算法,以便使引擎具有一定程度的可序列化性。

要运行测试用例,请执行以下操作:

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) {

putdelete操作函数改造,在修改完后进行写集合(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=3a=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中的putdeletewrite_batch接口。我们建议你定义一个帮助函数write_batch_internal来处理写批处理。如果options.serializable = true,则putdelete和面向用户的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;
    }
}
posted @   余为民同志  阅读(1)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战
点击右上角即可分享
微信分享提示