Lab 4 两段锁以及事务
本章Lab就开始进入数据库的深水区了,前面的就算是开胃小菜吧,从本章开始我们需要接触到数据库中重要的知识点——事务,不了解事物的同学可以看看这篇文章 事务的理解 。同时,与事务同时出现的带有两段锁协议,以及排他锁、共享锁和意向锁等关于锁的理解可以看这篇文章 锁的理解 。
Exercise 1 Acquire and Release locks in BufferPool
Exercise 1 是让我们在BufferPool.java中实现,加锁和解锁等一系类操作,并且锁的颗粒度是以页为单位。
-
首先我们需要定义锁的数据结构,这里定义的是一个锁的数据结构里有,事务的ID和锁的类型,我们这里用0来代表共享锁,1来代表排他锁。
private class Lock{ TransactionId tid; int lockType; // 0 for shared lock and 1 for exclusive lock public Lock(TransactionId tid,int lockType){ this.tid = tid; this.lockType = lockType; } }
-
然后我们需要定义一个锁管理中心(PageLockManager),用来判断是否能分配和释放锁,我们先来讲讲获取锁的逻辑。
-
实现acquireLock函数,获取锁的逻辑:
-
首先,我们获取锁时会获得当前事务的ID,要上锁的页面ID,锁的类型。
-
我们需要在我们的锁表(lockMap)中查找看看是否,该页面已经上锁,如果没有上锁,根据事务ID创建传进来的锁,并存入锁表中,并返回True。如果该页面已经有锁则进入第三步。
-
获取该页面的所有锁,找到等于当前事务的锁,看他是否满足如下情况:
- 是否跟当前锁的类型一致,如果一致就可以获取锁。
- 如果不一致,如果存在的锁是排他锁,那说明请求的锁是共享锁,也是可以直接获取的。这里涉及到一个知识点,**同一事务可以不断对某个数据对象加锁,不需要等锁的释放。**因为同一事务中所有操作都是串行化的,所以不会产生影响。
- 如果不一致,如果存在的锁是共享锁,那说明请求的锁是排他锁,这里就涉及到一个锁升级的概念,如果当同一事务中只有共享锁,但是即将需要上排他锁时,此时可直接将共享锁升级为排他锁,这里的思想跟意向锁很想,同时官方文档中也提到了。
-
如果当前事物在当前页面没有上过锁,那就看看该页面第一个锁是不是排他锁,如果是,按照封锁协议,其他事物都是不能加锁的,所以return False。
-
同时如果当前页面上的锁都是共享锁,如果需要加的也是共享锁也就可以直接加上 return True,但是如果需要加上排他锁,根据共享锁的性质,排他锁将不被允许加上。
public synchronized boolean acquireLock(PageId pid,TransactionId tid,int lockType){ // if no lock held on pid if(lockMap.get(pid) == null){ Lock lock = new Lock(tid,lockType); Vector<Lock> locks = new Vector<>(); locks.add(lock); lockMap.put(pid,locks); return true; } // if some Tx holds lock on pid // locks.size() won't be 0 because releaseLock will remove 0 size locks from lockMap Vector<Lock> locks = lockMap.get(pid); // if tid already holds lock on pid for(Lock lock:locks){ if(lock.tid == tid){ // already hold that lock if(lock.lockType == lockType) return true; // already hold exclusive lock when acquire shared lock if(lock.lockType == 1) return true; // already hold shared lock,upgrade to exclusive lock if(locks.size()==1){ lock.lockType = 1; return true; }else{ return false; } } } // if the lock is a exclusive lock if (locks.get(0).lockType ==1){ assert locks.size() == 1 : "exclusive lock can't coexist with other locks"; return false; } // if no exclusive lock is held, there could be multiple shared locks if(lockType == 0){ Lock lock = new Lock(tid,0); locks.add(lock); lockMap.put(pid,locks); return true; } // can not acquire a exclusive lock when there are shard locks on pid return false; }
以上就是是否能获取锁的逻辑。下面我们来讲讲如何释放锁
-
-
然后就是实现 releaseLock 函数,释放锁的逻辑就很简单了,如果锁表中没有当前页面没有锁,就直接返回 True。如果该页面有当前事务的锁,那就挨个remove掉,并且最后如果这个页面没锁了就在锁表中也直接remove掉。当前事务不在当前页面有锁则返回False。
public synchronized boolean releaseLock(PageId pid,TransactionId tid){ // if not a single lock is held on pid assert lockMap.get(pid) != null : "page not locked!"; Vector<Lock> locks = lockMap.get(pid); for(int i=0;i<locks.size();++i){ Lock lock = locks.get(i); // release lock if(lock.tid == tid){ locks.remove(lock); // if the last lock is released // remove 0 size locks from lockMap if(locks.size() == 0) lockMap.remove(pid); return true; } } // not found tid in tids which lock on pid return false; }
-
最后就是实现 holdsLock 函数,就是判断当前事务是否在该页面有锁,那就直接遍历就好了,然后注意一下空值。
public synchronized boolean holdsLock(PageId pid,TransactionId tid){ // if not a single lock is held on pid if(lockMap.get(pid) == null) return false; Vector<Lock> locks = lockMap.get(pid); // check if a tid exist in pid's vector of locks for(Lock lock:locks){ if(lock.tid == tid){ return true; } } return false; }
-
-
然后就是需要在BufferPool的构造函数中初始化一下变量。
public BufferPool(int numPages) { // some code goes here this.numPages = numPages; pageStore = new ConcurrentHashMap<PageId,Page>(); lockManager = new PageLockManager(); // 这里的pageQueue是为了后面驱逐策略做准备的 pageQueue = new LinkedList<>(); }
同时在getPage中修改一下。
public Page getPage(TransactionId tid, PageId pid, Permissions perm) throws TransactionAbortedException, DbException { // some code goes here int lockType; if(perm == Permissions.READ_ONLY){ lockType = 0; }else{ lockType = 1; } boolean lockAcquired = false; if(!pageStore.containsKey(pid)){ int tabId = pid.getTableId(); DbFile file = Database.getCatalog().getDatabaseFile(tabId); Page page = file.readPage(pid); if(pageStore.size()==numPages){ evictPage(); } pageStore.put(pid,page); pageAge.put(pid,age++); return page; } return pageStore.get(pid); }
然后再加一个重新读脏页的函数,应用在终止事务也就是回滚的时候(restorePages函数),当page.isDirty()等于当前事务的时候,说明当前事物需要回滚,就从disk里面再读一遍当前页面并覆盖他。
private synchronized void restorePages(TransactionId tid) { for (PageId pid : pageStore.keySet()) { Page page = pageStore.get(pid); if (page.isDirty() == tid) { int tabId = pid.getTableId(); DbFile file = Database.getCatalog().getDatabaseFile(tabId); Page pageFromDisk = file.readPage(pid); pageStore.put(pid, pageFromDisk); } } }
Exercise 2 修改heapFile 和 BufferPool
在Exerise2中,我建议跟Excerise 5一起做了,感觉没差别。首先就是把HeapFile.java 进行修改,逻辑稍微有了一些改变。
-
首先是insertTuple函数,就是先看传进来的事务ID在该页面上是否能上排他锁。如果不能就会超时,然后回溯(回溯是在getPage中进行),如果能上锁就判断page在不在当前缓冲区中,如果不在就从磁盘里读出来(同时如果Buffer pool满了的话,要采用驱逐策略将页面驱逐),将页面放到pageQueue的队尾。如果在就从缓冲区读出来,并把当前页送到pageQueue的队尾。传回来的page在insertTuple函数中判断有没有空值,如果没有进行下一个页面(并且把上了的锁清除),如果有就将tuple插入。如果遍历完都没能插入tuple,则创建新页插入table中。
public Page getPage(TransactionId tid, PageId pid, Permissions perm) throws TransactionAbortedException, DbException { // some code goes here int lockType; if(perm == Permissions.READ_ONLY){ lockType = 0; }else{ lockType = 1; } boolean lockAcquired = false; if(!pageStore.containsKey(pid)){ int tabId = pid.getTableId(); DbFile file = Database.getCatalog().getDatabaseFile(tabId); Page page = file.readPage(pid); if(pageStore.size()==numPages){ evictPage(); } pageStore.put(pid,page); pageAge.put(pid,age++); return page; } return pageStore.get(pid); } public void transactionComplete(TransactionId tid, boolean commit) throws IOException { // some code goes here // not necessary for lab1|lab2 if(commit){ flushPages(tid); }else{ restorePages(tid); } for(PageId pid:pageStore.keySet()){ if(holdsLock(tid,pid)) releasePage(tid,pid); } }
public ArrayList<Page> insertTuple(TransactionId tid, Tuple t) throws DbException, IOException, TransactionAbortedException { // some code goes here HeapPage page = null; // find a non full page for(int i=0;i<numPages();++i){ HeapPageId pid = new HeapPageId(getId(),i); page = (HeapPage)Database.getBufferPool().getPage(tid,pid,Permissions.READ_WRITE); if(page.getNumEmptySlots()!=0){ break; } else{ Database.getBufferPool().releasePage(tid,pid); } } // if not exist an empty slot, create a new page to store if(page == null || page.getNumEmptySlots() == 0){ HeapPageId pid = new HeapPageId(getId(),numPages()); byte[] data = HeapPage.createEmptyPageData(); HeapPage heapPage = new HeapPage(pid,data); writePage(heapPage); page = (HeapPage)Database.getBufferPool().getPage(tid,pid,Permissions.READ_WRITE); } page.insertTuple(t); ArrayList<Page> res = new ArrayList<>(); res.add(page); return res; }
-
然后就是deleteTuple函数,这就是找到页面删除就好,找页面的途中还是要判断一下能不能上锁,不能上锁就回滚。
public ArrayList<Page> deleteTuple(TransactionId tid, Tuple t) throws DbException, TransactionAbortedException { // some code goes here RecordId rid = t.getRecordId(); PageId pid = rid.getPageId(); // delete tuple and mark page as dirty HeapPage page = (HeapPage)Database.getBufferPool().getPage(tid,pid,Permissions.READ_WRITE); page.deleteTuple(t); // return res ArrayList<Page> res = new ArrayList<>(); res.add(page); return res; }
Exercise 3 修改evictPage函数
本节练习比较简单就是完成驱逐函数,驱逐函数就是当BufferPool满的时候,要对脏页进行写回硬盘并从BufferPool进行删除,这里我们只需要删除一页就return,当然也可以采取其他策略。
private synchronized void evictPage() throws DbException {
// some code goes here
// not necessary for lab1
try {
Iterator<PageId> iterator = pageQueue.iterator();
while(iterator.hasNext()) {
PageId pid = iterator.next();
if(pageStore.containsKey(pid)) {
Page page = pageStore.get(pid);
if(page.isDirty() == null) {
flushPage(pid); // 刷到磁盘
discardPage(pid); // 从缓冲区中删除该页
return;
}
}
}
throw new DbException("全部都是脏页,不能被替换!");
} catch (IOException e) {
e.printStackTrace();
}
}
Exercise 4 事务的完整性补全
-
为什么要叫事务的完整性补全呢,因为在之前很多操作都涉及到事务的回滚完成的操作了,其实代码也就是改那几行,比如完成事务时记得写到硬盘中,如果事务没有完成那就回溯,无论是回溯还是写硬盘都要记得把上面相关的锁去掉。
/** * Release all locks associated with a given transaction. * * @param tid the ID of the transaction requesting the unlock */ public void transactionComplete(TransactionId tid) throws IOException { // some code goes here // not necessary for lab1|lab2 transactionComplete(tid,true); } /** * Commit or abort a given transaction; release all locks associated to * the transaction. * * @param tid the ID of the transaction requesting the unlock * @param commit a flag indicating whether we should commit or abort */ public void transactionComplete(TransactionId tid, boolean commit) throws IOException { // some code goes here // not necessary for lab1|lab2 if(commit){ flushPages(tid); }else{ restorePages(tid); } for(PageId pid:pageStore.keySet()){ if(holdsLock(tid,pid)) releasePage(tid,pid); } } private synchronized void restorePages(TransactionId tid) { for (PageId pid : pageStore.keySet()) { Page page = pageStore.get(pid); if (page.isDirty() == tid) { int tabId = pid.getTableId(); DbFile file = Database.getCatalog().getDatabaseFile(tabId); Page pageFromDisk = file.readPage(pid); pageStore.put(pid, pageFromDisk); } } } /** Write all pages of the specified transaction to disk. */ public synchronized void flushPages(TransactionId tid) throws IOException { // some code goes here // not necessary for lab1|lab2 for (PageId pid : pageStore.keySet()) { Page page = pageStore.get(pid); if (page.isDirty() == tid) { flushPage(pid); } } }
Excerise 5 死锁检测
这里的死锁检测很简单就是,判断超时没超时,这在官方文档中写的很清楚,当然你也可以通过广度搜索来遍历环,如果有的话就是死锁。
public Page getPage(TransactionId tid, PageId pid, Permissions perm)
throws TransactionAbortedException, DbException {
// some code goes here
// if(!pageStore.containsKey(pid)){
// if(pageStore.size()>numPages){
// evictPage();
// }
// DbFile dbfile = Database.getCatalog().getDatabaseFile(pid.getTableId());
// Page page = dbfile.readPage(pid);
// pageStore.put(pid,page);
// }
// return pageStore.get(pid);
int lockType;
if(perm == Permissions.READ_ONLY){
lockType = 0;
}else{
lockType = 1;
}
boolean lockAcquired = false;
long start = System.currentTimeMillis();
long timeout = new Random().nextInt(2000) + 1000;
while(!lockAcquired){
long now = System.currentTimeMillis();
if(now-start > timeout){
transactionComplete(tid, false);
// TransactionAbortedException means detect a deadlock
// after upper caller catch TransactionAbortedException
// will call transactionComplete to abort this transition
// give someone else a chance: abort the transaction
throw new TransactionAbortedException();
}
lockAcquired = lockManager.acquireLock(pid,tid,lockType);
}
if(!pageStore.containsKey(pid)){
int tabId = pid.getTableId();
DbFile file = Database.getCatalog().getDatabaseFile(tabId);
Page page = file.readPage(pid);
if(pageStore.size()==numPages){
evictPage();
}
pageStore.put(pid,page);
pageQueue.offer(page.getId());
// pageAge.put(pid,age++);
return page;
}else{
pageQueue.remove(pid);
pageQueue.offer(pid);
return pageStore.get(pid);
}
}
参考文章:
https://blog.csdn.net/hjw199666/category_9588041.html 特别鸣谢hjw199666 在我完成6.830的道路上给了很多代码指导,我的很多代码都是基于他的改的
https://www.zhihu.com/people/zhi-yue-zhang-42/posts