原理
DBMS包含一个锁管理器,用于决定事务是否可以锁定。 它了解系统内部的最新情况。
- 共享锁(S-LOCK):允许多个事务同时读取同一对象的锁。 如果一个事务持有共享锁,则另一个事务可以获取该共享锁。
- 独占锁定(X-LOCK):允许事务修改对象。 此锁与任何其他锁不兼容。 一次只能有一个事务持有独占锁。
使用锁执行:
1.事务从锁管理器请求锁(或升级)。
2.锁管理器根据其他事务当前持有的锁来授予或阻止请求。
3.当不再需要时,事务释放锁。
4.锁管理器更新其内部锁表,然后把锁给其他等待的事务。
二阶段锁定
两阶段锁定(2PL)是一种悲观的并发控制协议,用于确定是否允许事务访问数据库中的对象。协议不需要知道事务将提前执行的所有查询。
阶段#1:膨胀
•每个事务都从DBMS的锁管理器请求它所需的锁。
•锁管理器授予/拒绝锁定请求。
阶段#2:收缩
•事务在释放第一个锁后立即进入此阶段。
•允许事务仅释放先前获取的锁。它无法在此阶段获得新锁。
就其本身而言,2PL足以保证conflict serializability。它生成precedence graph是无环的。
2个缺点:
但它很容易出现级联中止,即当事务中止并且现在必须回滚另一个事务时,这会导致浪费很多资源。
还有一些可序列化的潜在计划,但2PL不允许这种计划(锁会限制并发)。
严格的2pl(不会级联回滚,不存在shrinking的情况)
在执行事务的过程中,所有的数据库操作有可能会要求加锁,但是不能立刻释放锁。必须要等到整个事务提交或回滚后,才能释放锁。
S2PL确实没有像普通2PL那样shrinking的阶段,如果事务写入的值在该事务完成之前未被其他事务读取或覆盖,则调度是严格的。
这种方法的优点是DBMS不会导致级联中止。
同时只要把原来的值赋值回去就可以实现abort了。
为什么呢?我们看一下S2pl的时序图
死锁问题
下面要解决的就是2pl 的死锁问题
死锁问题的解决思路分为2种,一种是死锁预防,一种是死锁检测。
死锁检测
DBMS 创建 wait-for图:如果事务Ti等待事务Tj释放锁,从Ti到Tj有一条边。系统将定期检查等待图中的环,然后决定如何打破它。
•当DBMS检测到死锁时,它将选择“受害者”事务进行回滚以中断循环。
•受害者事务将重新启动或中止,具体取决于应用程序如何调用它
•选择受害者时需要考虑多个事务属性。没有一个选择比其他选择更好。 2PL DBMS都做不同的事情:
1.按年龄(最新或最旧的时间戳)。
2.按进度(执行的最少/大多数查询)。
3.已锁定的项目数量。
4.通过我们必须用它回滚的#个事务。
5.过去重启事务的次数
•回滚长度:选择要中止的受害者事务后,DBMS还可以决定回滚事务的更改的距离。可以是整个事务,也可以是足够的操作(部分事务)足以来打破僵局
死锁预防(本实现采用,condition variable!!!)
当txn尝试获取另一个txn持有的锁时,DBMS会杀死其中一个以防止死锁。
该方法不需要wait-for图或检测算法。
根据时间戳分配优先级(例如,旧的意味着更高的优先级)。
这些方案保证没有死锁,因为在等待锁时只允许一个方向。 当事务重新启动时,其(新)优先级是其旧时间戳。
•Wait-Die(“Old等待Young”):如果T1具有更高的优先级,则T1等待T2。 否则T1中止
•wound-wait(“Young等待old”):如果T1具有更高的优先级,则T2中止。 否则T1会等待。
代码
事务
/**
* Transaction states:
*
* _________________________
* | v
* GROWING -> SHRINKING -> COMMITTED ABORTED
* |__________|________________________^
*
**/
enum class TransactionState { GROWING, SHRINKING, COMMITTED, ABORTED };
enum class WType { INSERT = 0, DELETE, UPDATE };
class TableHeap;
// write set record
class WriteRecord {
public:
WriteRecord(RID rid, WType wtype, const Tuple &tuple, TableHeap *table)
: rid_(rid), wtype_(wtype), tuple_(tuple), table_(table) {}
RID rid_;
WType wtype_;
// tuple is only for update operation
Tuple tuple_;
// which table
TableHeap *table_;
};
class Transaction {
public:
Transaction(Transaction const &) = delete;
Transaction(txn_id_t txn_id)
: state_(TransactionState::GROWING),
thread_id_(std::this_thread::get_id()),
txn_id_(txn_id), prev_lsn_(INVALID_LSN), shared_lock_set_{new std::unordered_set<RID>},
exclusive_lock_set_{new std::unordered_set<RID>} {
// initialize sets
write_set_.reset(new std::deque<WriteRecord>);
page_set_.reset(new std::deque<Page *>);
deleted_page_set_.reset(new std::unordered_set<page_id_t>);
}
~Transaction() {}
//===--------------------------------------------------------------------===//
// Mutators and Accessors
//===--------------------------------------------------------------------===//
inline std::thread::id GetThreadId() const { return thread_id_; }
inline txn_id_t GetTransactionId() const { return txn_id_; }
inline std::shared_ptr<std::deque<WriteRecord>> GetWriteSet() {
return write_set_;
}
inline std::shared_ptr<std::deque<Page *>> GetPageSet() { return page_set_; }
inline void AddIntoPageSet(Page *page) { page_set_->push_back(page); }
inline std::shared_ptr<std::unordered_set<page_id_t>> GetDeletedPageSet() {
return deleted_page_set_;
}
inline void AddIntoDeletedPageSet(page_id_t page_id) {
bool exists = false;
for (Page *i : *GetPageSet()) {
exists |= (i->GetPageId() == page_id);
}
if (!exists)
std::bad_alloc();
deleted_page_set_->insert(page_id);
}
inline std::shared_ptr<std::unordered_set<RID>> GetSharedLockSet() {
return shared_lock_set_;
}
inline std::shared_ptr<std::unordered_set<RID>> GetExclusiveLockSet() {
return exclusive_lock_set_;
}
inline TransactionState GetState() { return state_; }
inline void SetState(TransactionState state) { state_ = state; }
inline lsn_t GetPrevLSN() { return prev_lsn_; }
inline void SetPrevLSN(lsn_t prev_lsn) { prev_lsn_ = prev_lsn; }
private:
TransactionState state_;
// thread id, single-threaded transactions
std::thread::id thread_id_;
// transaction id
txn_id_t txn_id_;
// Below are used by transaction, undo set
std::shared_ptr<std::deque<WriteRecord>> write_set_;
// prev lsn
lsn_t prev_lsn_;
// Below are used by concurrent index
// this deque contains page pointer that was latche during index operation
std::shared_ptr<std::deque<Page *>> page_set_;
// this set contains page_id that was deleted during index operation
std::shared_ptr<std::unordered_set<page_id_t>> deleted_page_set_;
// Below are used by lock manager
// this set contains rid of shared-locked tuples by this transaction
std::shared_ptr<std::unordered_set<RID>> shared_lock_set_;
// this set contains rid of exclusive-locked tuples by this transaction
std::shared_ptr<std::unordered_set<RID>> exclusive_lock_set_;
};
控制器图例
数据结构设计
这幅图的就是一个HASH TABLE的链表实现法。每一个VALUE里还要存所有的在访问这个KEY的TRANSACTION。
针对每个TRANSACTION 我们需要记录是否是GRANT的 , TX ID , 还有上锁的模式。
所以上述的图是3层结构。一个HASH表KEY是 RID, VALUE是LIST
第二层结构里是个TX LIST 存了 属于这个RID的每一个TX ITEM
第三层 则是TX ITEM。
随后我们按照需求去构造那个LIST,最开始的设计MAP的VALUE 就是一个LIST
但是再做UPGRADING的时候,发现要判断之前有没有正在等待锁升级的TRANSACTION,如果有,需要ABORT。所以加了一个变量来观察。就做了一层封装,同时为了增加并发度,做了一个针对TX LIST的粒度锁。这样可以避免锁整个LOCK TABLE。
最后是最外层结构的定义
算法的核心 就是实现LOCK TEMPLATE 和 UNLOCK。
在LOCK TEMPLATE 中,大致分为4个模块
第一个模块是找到对应的TX LIST并且获得锁
第二个模块是针对LOCK UPGRADING,因为需要抹掉原来的读锁,才能升级为写锁。
第三个模块是判断是否可以GRANT。
第四个模块就是往TX LIST里插入,同时阻塞或者拿锁成功就往TXN 里面放入对应的RID记录。
在UNLOCK 里,首先要区分是否是S 2PL,是的话就要求只能在COMMIT 和ABORT的时候才可以释放锁。
随后定位到要删除的元素的TXLIST,从里面抹除,从TRANSACTIONS 的LOCK集合里抹除对应的RID。
然后判断是否TXLIST EMPTY,抹除对应的KEY。
最后判断是否可以GRANT 锁给其他的TX。
二段锁控制器
class LockManager {
struct TxItem {
TxItem(txn_id_t tid, LockMode mode, bool granted) :
tid_(tid), mode_(mode), granted_(granted) {}
void Wait() {
unique_lock<mutex> ul(mutex_);
cv_.wait(ul, [this] { return this->granted_; });
}
void Grant() {
lock_guard<mutex> lg(mutex_);
granted_ = true;
cv_.notify_one();
}
mutex mutex_;
condition_variable cv_;
txn_id_t tid_;
LockMode mode_;
bool granted_;
};
struct TxList {
mutex mutex_;
list<TxItem> locks_;
bool hasUpgrading_;
bool checkCanGrant(LockMode mode) { //protect by mutex outside
if (locks_.empty()) return true;
const auto last = &locks_.back();
if (mode == LockMode::SHARED) {
return last->granted_ && last->mode_ == LockMode::SHARED;
}
return false;
}
void insert(Transaction* txn, const RID &rid, LockMode mode, bool granted, unique_lock<mutex> *lock) {
bool upgradingMode = (mode == LockMode::UPGRADING);
if (upgradingMode && granted) mode = LockMode::EXCLUSIVE;
locks_.emplace_back(txn->GetTransactionId(),mode,granted);
auto &last = locks_.back();
if (!granted) {
hasUpgrading_ |= upgradingMode;
lock->unlock();
last.Wait();
}
if (mode == LockMode::SHARED) {
txn->GetSharedLockSet()->insert(rid);
} else {
txn->GetExclusiveLockSet()->insert(rid);
}
}
};
public:
LockManager(bool strict_2PL) : strict_2PL_(strict_2PL){};
/*** below are APIs need to implement ***/
// lock:
// return false if transaction is aborted
// it should be blocked on waiting and should return true when granted
// note the behavior of trying to lock locked rids by same txn is undefined
// it is transaction's job to keep track of its current locks
bool LockShared(Transaction *txn, const RID &rid);
bool LockExclusive(Transaction *txn, const RID &rid);
bool LockUpgrade(Transaction *txn, const RID &rid);
// unlock:
// release the lock hold by the txn
bool Unlock(Transaction *txn, const RID &rid);
/*** END OF APIs ***/
private:
bool lockTemplate(Transaction *txn, const RID &rid, LockMode mode);
bool strict_2PL_;
mutex mutex_;
unordered_map<RID,TxList> lockTable_;
};
bool LockManager::LockShared(Transaction *txn, const RID &rid) {
return lockTemplate(txn,rid,LockMode::SHARED);
}
bool LockManager::LockExclusive(Transaction *txn, const RID &rid) {
return lockTemplate(txn,rid,LockMode::EXCLUSIVE);
}
bool LockManager::LockUpgrade(Transaction *txn, const RID &rid) {
return lockTemplate(txn,rid,LockMode::UPGRADING);
}
bool LockManager::lockTemplate(Transaction *txn, const RID &rid, LockMode mode) {
// step 1
if (txn->GetState() != TransactionState::GROWING) {
txn->SetState(TransactionState::ABORTED);
return false;
}
unique_lock<mutex> tableLatch(mutex_);
TxList &txList = lockTable_[rid];
unique_lock<mutex> txListLatch(txList.mutex_);
tableLatch.unlock();
if (mode == LockMode::UPGRADING) {//step 2
if (txList.hasUpgrading_) {
txn->SetState(TransactionState::ABORTED);
return false;
}
auto it = find_if(txList.locks_.begin(), txList.locks_.end(),
[txn](const TxItem &item) {return item.tid_ == txn->GetTransactionId();});
if (it == txList.locks_.end() || it->mode_ != LockMode::SHARED || !it->granted_) {
txn->SetState(TransactionState::ABORTED);
return false;
}
txList.locks_.erase(it);
assert(txn->GetSharedLockSet()->erase(rid) == 1);
}
//step 3
bool canGrant = txList.checkCanGrant(mode);
if (!canGrant && txList.locks_.back().tid_ < txn->GetTransactionId()) {
txn->SetState(TransactionState::ABORTED);
return false;
}
txList.insert(txn,rid,mode,canGrant,&txListLatch);
return true;
}
bool LockManager::Unlock(Transaction *txn, const RID &rid) {
if (strict_2PL_) {//step1
if (txn->GetState() != TransactionState::COMMITTED && txn->GetState() != TransactionState::ABORTED) {
txn->SetState(TransactionState::ABORTED);
return false;
}
} else if (txn->GetState() == TransactionState::GROWING) {
txn->SetState(TransactionState::SHRINKING);
}
unique_lock<mutex> tableLatch(mutex_);
TxList &txList = lockTable_[rid];
unique_lock<mutex> txListLatch(txList.mutex_);
//step 2 remove txList and txn->lockset
auto it = find_if(txList.locks_.begin(), txList.locks_.end(),
[txn](const TxItem &item) {return item.tid_ == txn->GetTransactionId();});
assert(it != txList.locks_.end());
auto lockSet = it->mode_ == LockMode::SHARED ? txn->GetSharedLockSet() : txn->GetExclusiveLockSet();
assert(lockSet->erase(rid) == 1);
txList.locks_.erase(it);
if (txList.locks_.empty()) {
lockTable_.erase(rid);
return true;
}
tableLatch.unlock();
//step 3 check can grant other
for (auto &tx : txList.locks_) {
if (tx.granted_)
break;
tx.Grant(); //grant blocking one
if (tx.mode_ == LockMode::SHARED) {continue;}
if (tx.mode_ == LockMode::UPGRADING) {
txList.hasUpgrading_ = false;
tx.mode_ = LockMode::EXCLUSIVE;
}
break;
}
return true;
}
事务控制器
class TransactionManager {
public:
TransactionManager(LockManager *lock_manager,
LogManager *log_manager = nullptr)
: next_txn_id_(0), lock_manager_(lock_manager),
log_manager_(log_manager) {}
Transaction *Begin();
void Commit(Transaction *txn);
void Abort(Transaction *txn);
private:
std::atomic<txn_id_t> next_txn_id_;
LockManager *lock_manager_;
LogManager *log_manager_;
};
Transaction *TransactionManager::Begin() {
Transaction *txn = new Transaction(next_txn_id_++);
if (ENABLE_LOGGING) {
assert(txn->GetPrevLSN() == INVALID_LSN);
LogRecord log{txn->GetTransactionId(), txn->GetPrevLSN(), LogRecordType::BEGIN};
txn->SetPrevLSN(log_manager_->AppendLogRecord(log));
}
return txn;
}
void TransactionManager::Commit(Transaction *txn) {
txn->SetState(TransactionState::COMMITTED);
// truly delete before commit
auto write_set = txn->GetWriteSet();
while (!write_set->empty()) {
auto &item = write_set->back();
auto table = item.table_;
if (item.wtype_ == WType::DELETE) {
// this also release the lock when holding the page latch
table->ApplyDelete(item.rid_, txn);
}
write_set->pop_back();
}
write_set->clear();
if (ENABLE_LOGGING) {//, you need to make sure your log records are permanently stored on disk file before release the
// locks. But instead of forcing flush, you need to wait for LOG_TIMEOUT or other operations to implicitly trigger
// the flush operations. write log and update transaction's prev_lsn here
LogRecord log{txn->GetTransactionId(), txn->GetPrevLSN(), LogRecordType::COMMIT};
txn->SetPrevLSN(log_manager_->AppendLogRecord(log));
log_manager_->Flush(false);
}
// release all the lock
std::unordered_set<RID> lock_set;
for (auto item : *txn->GetSharedLockSet())
lock_set.emplace(item);
for (auto item : *txn->GetExclusiveLockSet())
lock_set.emplace(item);
// release all the lock
for (auto locked_rid : lock_set) {
lock_manager_->Unlock(txn, locked_rid);
}
}
void TransactionManager::Abort(Transaction *txn) {
txn->SetState(TransactionState::ABORTED);
// rollback before releasing lock
auto write_set = txn->GetWriteSet();
while (!write_set->empty()) {
auto &item = write_set->back();
auto table = item.table_;
if (item.wtype_ == WType::DELETE) {
LOG_DEBUG("rollback delete");
table->RollbackDelete(item.rid_, txn);
} else if (item.wtype_ == WType::INSERT) {
LOG_DEBUG("rollback insert");
table->ApplyDelete(item.rid_, txn);
} else if (item.wtype_ == WType::UPDATE) {
LOG_DEBUG("rollback update");
table->UpdateTuple(item.tuple_, item.rid_, txn);
}
write_set->pop_back();
}
write_set->clear();
if (ENABLE_LOGGING) {
// write log and update transaction's prev_lsn here
LogRecord log{txn->GetTransactionId(), txn->GetPrevLSN(), LogRecordType::ABORT};
txn->SetPrevLSN(log_manager_->AppendLogRecord(log));
log_manager_->Flush(false);
}
// release all the lock
std::unordered_set<RID> lock_set;
for (auto item : *txn->GetSharedLockSet())
lock_set.emplace(item);
for (auto item : *txn->GetExclusiveLockSet())
lock_set.emplace(item);
// release all the lock
for (auto locked_rid : lock_set) {
lock_manager_->Unlock(txn, locked_rid);
}
}