cmu15445-project4 事务隔离与死锁检测 实验总结

一、实验内容

  P4要求完成事务间的并发控制,包括支持三种隔离事务的加解锁,以及一个死锁检测线程;适配之前实现的执行器,并且修改之前实现的算子实现对并发的支持。

Task #1 - Lock Manager

Task #2 - Deadlock Detection

Task #3 - Concurrent Query Execution

 

二、实验FAQ

1.加锁主流程

对于LockTable,按照以下伪代码执行逻辑:

复制代码
table_lock_map.lock()
check_compatibility()
req_queue = get_lock_queue_for_table(table_id)
table_lock_map.unlock()

req_queue.lock()
success = grant_lock_for_table(table_id)
queue_lock.lock()
while not success:
    success = grant_lock_for_table(table_id)
    if success:
        req_queue.nofity_all()
    else:
        wait(queue_lock)
复制代码

加锁的图如下所示,每个表关联一个请求队列,LM要检查兼容性,当请求与已有的请求兼容的时候,就同意该请求,但此时也表示加锁成功;而wait操作会等待在锁上,直到另外的线程释放锁,才会被唤醒,并尝试重新判定锁的兼容性;释放锁的时候,请求队列里的请求也要被删除。

(图源1)

 

    关于加锁的兼容性,依据是兼容性矩阵,如果队列里有与自己不兼容且已经granted的锁,则加锁失败;接着判断本事务是否已经加过锁,锁是否要升级,并保证请求队列里的一个事务只有一个请求。而即使是在请求队列里,由于唤醒的随机性,事务获得锁的顺序也不一定跟入队的顺序一样;

   多个事务访问加锁的相容性矩阵如下,矩阵中元素(i,j)表示的是表已经加了锁i的情况下,是否能再加上锁j。

 

为什么只考虑与granted的锁的兼容性?

    因为未granted的锁此时也在等待,不能继续执行,所以并不会对数据安全造成影响;这时就相当于多个事务在竞争锁,一旦造成阻塞的事务释放了锁,那么竞争成功者便可以继续执行了;

 

2PL两段锁协议与串行化

    两段锁协议通过限制加锁解锁的顺序,达到实现冲突序列化(CSR)的目的,因为它生成的调度的前序图是无环的。有人说这是为了实现隔离性,这是不准确的。关于序列化、串行化的具体概念最好参考Database System Concepts 并发控制章节,尽量只看必要的,因为概念和扩展实在太多了。在P4实现中我们只需要遵循加锁解锁的阶段限制就可以了。

 

隔离级别跟加解锁的关系

    隔离级别规定了加锁的条件和加锁的类型,以及能够实现的数据库一致性

 

阶段条件

REPEATABLE_READ

GROWING阶段加锁;

SHRINKING阶段不能加锁

READ_COMMITTED

GROWING阶段加锁;

SHRINKING阶段只能加IS,S锁

READ_UNCOMMITTED 

X,IX锁只能在GROWING阶段加

S,IS,SIX锁不允许加

SERIALIZABLE

 

2.Lock要点

Table、Row区别

LockTable可以加任意类型锁

LockRow只能加非意向锁

       Lock的时候要更新对应的Transaction里的锁记录集合

       对异常情况,要设置状态ABORT, 并抛异常。

 

锁的升级只允许以下操作:

IS -> [S, X, IX, SIX]
S -> [X, SIX]
IX -> [X, SIX]
SIX -> [X]

 

为什么RU级别只能加IX, X 锁

首先,RU级别也必须保证写安全,不能同时有两个写,但是RU级别不保证读的一致性,所以不能加包含读的所有锁。

依此类推,S锁都是为了保证读的一致性,而X都是为了保证写的一致性;所以在RR里,由于所有S锁直到事务完成才释放,因此不会有记录在事务期间被修改或删除;而RC级别可以在第二阶段重复加S锁,因此会产生读取到不同的数据的现象。

 

这里需要注意的是,不同的数据库的执行算子加锁实现方式不一定完全相同。例如先读一些记录后更新一些记录的操作,可以是表加IS锁,升级表IX锁,再依次对行加X锁;但也可以直接对表加SIX锁。

加SIX锁对表加IS锁,显式地给所有行加了锁,能让其他事务加读锁,但不再允许其他事务加IX锁;同时,SIX锁表示可能对行加X锁,因此其他事务只能给未加X锁的行加S锁。

3.死锁检测

要点:

    每隔一段时间进行检测,从头构建一个事务的依赖图,并检测有无环;

定义DFS辅助函数如下,通过DFS方式找到环,当然这个方法不适用于同时大量事务的场景(不清楚主流数据库是否需要死锁检测,就算有也不是默认配置的?);

auto LockManager::DFS(txn_id_t cur, txn_id_t *out_id, std::unordered_map<txn_id_t, bool> &vis, std::unordered_map<txn_id_t, txn_id_t> &prev) -> bool;

从最小标号的节点开始寻找环,如果找到环,将该环的标号最大的节点Abort。(标号越大,说明越新创建)。这样能保证找到的环是固定的。

 

4.并发查询执行

为了执行并发,修改算子,算子Init的时候要根据算子类型加IS,IX锁,若Next要加行S锁,必须先加IS锁;若要加行X锁,则必须先加IX锁。

事务Abort,Commit都会执行Unlock操作,而不同的隔离级别对锁的要求也不一样。READ_UNCOMMITTED级别不需要加IS, S锁。

 

5.测试样例、时序图

    根据时序图设计并发测试样例

复制代码
 std::thread t1([&]() {
    std::stringstream ss;
    auto noop_writer = NoopWriter();
    bustub_->ExecuteSqlTxn("INSERT INTO empty_table2 VALUES(200, 20), (201, 21), (202, 22)", noop_writer, txn1);
    bustub_->txn_manager_->Commit(txn1);
  });
 
  std::thread t2([&]() {
    std::stringstream ss;
    auto writer2 = SimpleStreamWriter(ss, true);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    bustub_->ExecuteSqlTxn("SELECT * FROM empty_table2", writer2, txn2);
    EXPECT_EQ(ss.str(), "200\t20\t\n201\t21\t\n202\t22\t\n");
    EXPECT_EQ(0, txn2->GetSharedTableLockSet()->size());
    EXPECT_EQ(0, txn2->GetExclusiveTableLockSet()->size());
    EXPECT_EQ(1, txn2->GetIntentionSharedTableLockSet()->size());
    EXPECT_EQ(0, txn2->GetIntentionExclusiveTableLockSet()->size());
    EXPECT_EQ(0, txn2->GetSharedIntentionExclusiveTableLockSet()->size());
    // read again
    ss.str("");
    std::this_thread::sleep_for(std::chrono::milliseconds(40));
    bustub_->ExecuteSqlTxn("SELECT * FROM empty_table2", writer2, txn2);
    EXPECT_EQ(ss.str(), "200\t20\t\n201\t21\t\n202\t22\t\n");
    bustub_->txn_manager_->Commit(txn2);
  });
 
  std::thread t3([&]() {
    std::stringstream ss;
    auto writer2 = SimpleStreamWriter(ss, true);
    auto noop_writer = NoopWriter();
    std::this_thread::sleep_for(std::chrono::milliseconds(30));
    bustub_->ExecuteSqlTxn("SELECT * FROM empty_table2 LIMIT 1", writer2, txn3);
    bustub_->ExecuteSqlTxn("DELETE FROM empty_table2 WHERE x = 200", noop_writer, txn3);
    std::this_thread::sleep_for(std::chrono::milliseconds(50));
    bustub_->txn_manager_->Commit(txn3);
  });
复制代码

 

6. LeaderBoard Task

只实现了UpdateExecutor,其余暂时不想完成了。贴一下提交的结果。

 

7.其他

实验中MVCC主题未涉及。一旦有了MVCC,则上面实验里关于读阻塞写,写阻塞读的问题就要重新考虑了。

视图序列性、回滚、级联回滚的关系和概念需要参考更多的资料(如:数据库系统概念),实验中不会接触到

如图,两段锁协议产生的调度是所有调度的子集。严格的2PL可以避免级联回滚。

 

References

[1] CMU15445-2022 P4 Concurrency Control https://zhuanlan.zhihu.com/p/600001968

[2] https://15445.courses.cs.cmu.edu/fall2022 课程官网

[3] https://github.com/cmu-db/bustub Bustub Github Repo

[4] Database System Concepts 6th version, Abraham.Silberschatz.

[5]自动测评网站 GradeScope,course entry code: PXWVR5 https://www.gradescope.com/

posted @ 2023-08-21 23:05  stackupdown  阅读(118)  评论(0编辑  收藏  举报