MIT 6.830 Lab 4 实验笔记 两段锁以及事物

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函数,获取锁的逻辑:

      1. 首先,我们获取锁时会获得当前事务的ID,要上锁的页面ID,锁的类型。

      2. 我们需要在我们的锁表(lockMap)中查找看看是否,该页面已经上锁,如果没有上锁,根据事务ID创建传进来的锁,并存入锁表中,并返回True。如果该页面已经有锁则进入第三步。

      3. 获取该页面的所有锁,找到等于当前事务的锁,看他是否满足如下情况:

        • 是否跟当前锁的类型一致,如果一致就可以获取锁。
        • 如果不一致,如果存在的锁是排他锁,那说明请求的锁是共享锁,也是可以直接获取的。这里涉及到一个知识点,**同一事务可以不断对某个数据对象加锁,不需要等锁的释放。**因为同一事务中所有操作都是串行化的,所以不会产生影响。
        • 如果不一致,如果存在的锁是共享锁,那说明请求的锁是排他锁,这里就涉及到一个锁升级的概念,如果当同一事务中只有共享锁,但是即将需要上排他锁时,此时可直接将共享锁升级为排他锁,这里的思想跟意向锁很想,同时官方文档中也提到了。
      4. 如果当前事物在当前页面没有上过锁,那就看看该页面第一个锁是不是排他锁,如果是,按照封锁协议,其他事物都是不能加锁的,所以return False。

      5. 同时如果当前页面上的锁都是共享锁,如果需要加的也是共享锁也就可以直接加上 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

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值