SimpleDB数据库 Lab4实现

lab4实现与事务Transaction有关的方法,并实现页粒度的两段锁,实现SimpleDB多事务处理的功能,至此已经简单实现了一个具备增、删、改、查能力的数据库。

代码链接:SimpleDB Lab4

提交历史

在这里插入图片描述

设计思路

Transaction(事务)是一组以原子方式执行的数据库操作(插入、删除或读取等),要么所有的动作都完成了,要么一个动作都没有完成。

为了防止事务之间相互干涉导致读写异常,Lab4中将为SimpleDB实现两段锁,两段锁的阶段分别为:

  1. 第一阶段:扩展阶段,事务可以申请获得任意数据上的任意锁,但是不能释放任何锁;

  2. 第二阶段:收缩阶段,事务可以释放任何数据上的任何类型的锁,但是不能再次申请任何锁

两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁。于是Lab4中也要实现死锁的检测和解决。

此外,Lab4还要实现NO STEAL策略,即事务对page的修改只有在commit之后才会写入到磁盘(flush),但是在之前的Lab中实现页面置换策略时,当置换掉的页面是dirty
page时,也会将更改写回到磁盘。这是不允许的,所以需要完善evictPage()方法,当需要置换的page是dirty page时,需要跳过此page,去置换下一个非dirty的page。

Exercise 1 实现页粒度的锁并完善相应方法

SimpleDB中的两端锁的要求如下:

  1. 在事务可以读取一个对象之前,它必须拥有一个共享锁(SHARED LOCK);

  2. 在一个事务可以写一个对象之前,它必须有一个互斥锁(EXCLUSIVE LOCK);

  3. 多个事务可以在一个对象上拥有一个共享锁;

  4. 只有一个事务能对一个对象具有互斥锁;

  5. 如果对象o上只有事务t持有共享锁,则t可以将其对o的锁升级为互斥锁;

为此,首先在BufferPool中实现一个Lock类:

private class Lock{
        public static final int SHARE = 0;  //0 stands for shared lock,
        // Multiple transactions can have a shared lock on an object before reading
        public static final int EXCLUSIVE = 1;  //1 stands for exclusive lock,
        // Only one transaction may have an exclusive lock on an object before writing
        private TransactionId tid;
        private int type;

        public Lock(TransactionId tid, int type){
            this.tid =  tid;
            this.type = type;
        }
        public TransactionId getTid() {
            return tid;
        }
        public int getType() {
            return type;
        }

        public void setType(int type) {
            this.type = type;
        }
    }

而后,需要再创建一个LockManager类实现对每个事务在每个页上的锁的管理,包括获取和释放,而获取锁的逻辑如下:

在这里插入图片描述

整个BufferPool中只需要一个LockManager成员变量即可。
在LockManager中,创建了一个ConcurrentHashMap<PageId,ConcurrentHashMap<TransactionId,PageLock>> lockMap
对象来记录每个Page上事务t和其锁的映射,该类型的变量在多线程访问时更加安全。

在这里插入图片描述

具体的代码和注释如下:

        private class LockManager{
            private ConcurrentHashMap<PageId,ConcurrentHashMap<TransactionId,Lock>> pageLocks;
            //use concurrentHashMap instead of HashMap cuz it's safer than the latter
            //when facing multiple threads

            public LockManager(){
                pageLocks = new ConcurrentHashMap<PageId,ConcurrentHashMap<TransactionId,Lock>>();
            }

            public synchronized boolean acquireLock(PageId pid,TransactionId tid,int lockType)
                    throws TransactionAbortedException {
                if(pageLocks.get(pid)==null){  // no exsisting locks, create newPageLock and return ture
                    Lock newLock = new Lock(tid,lockType);
                    ConcurrentHashMap<TransactionId,Lock> newPageLock = new ConcurrentHashMap<>();
                    newPageLock.put(tid,newLock);
                    pageLocks.put(pid,newPageLock);
                    return true;
                }
                //if there's locks already
                ConcurrentHashMap<TransactionId,Lock> nowPageLock = pageLocks.get(pid);

                if(nowPageLock.get(tid)==null){   //no locks from tid
                    if(nowPageLock.size()>1){  //exists locks from other transactions
                        if(lockType == Lock.SHARE){
                            Lock newLock = new Lock(tid,lockType); //if requring a read lock
                            nowPageLock.put(tid,newLock);
                            pageLocks.put(pid,nowPageLock);
                            return true;
                        }
                        else{  //requiring a write lock
                            return false;
                        }
                    }
                    else if(nowPageLock.size()==1){  //only one other lock,could be a read or write lock
                        Lock existLock = null;
                        for(Lock lock:nowPageLock.values())
                            existLock = lock;
                        if(existLock.getType() == Lock.SHARE){  // if it is a read lock
                            if(lockType == Lock.SHARE){// require a read lock too, return true
                                Lock newLock = new Lock(tid,lockType);
                                nowPageLock.put(tid,newLock);
                                pageLocks.put(pid,nowPageLock);
                                return true;
                            }
                            else if(lockType == Lock.EXCLUSIVE){
                                return false;
                            }
                        }
                        else if(existLock.getType() == Lock.EXCLUSIVE){ //some transaction is writing this page
                            return false;
                        }
                    }
                }
                else{
                    Lock preLock = nowPageLock.get(tid);
                    if(preLock.getType() == Lock.SHARE){  //if previous lock is a read lock
                        if(lockType == Lock.SHARE){ //and require a read lock too
                            return true;
                        }
                        else{ // if want a write lock
                            if(nowPageLock.size() == 1){ // if only this tid hold a lock,we could update it
                                preLock.setType(Lock.EXCLUSIVE);
                                nowPageLock.put(tid,preLock);
                                return true;
                            }
                            else{
                                throw new TransactionAbortedException();
                            }
                        }
                    }

                    else if(preLock.getType() == Lock.EXCLUSIVE){ //the previous one is a write lock
                        //means no other locks on this page
                        return true;
                    }
                }
                return false;
            }

            public synchronized boolean holdsLock(TransactionId tid,PageId pid){
                if(pageLocks.get(pid) == null){
                    return false;
                }
                ConcurrentHashMap<TransactionId,Lock> nowPageLock = pageLocks.get(pid);
                if(nowPageLock.get(tid) == null){
                    return false;
                }
                return true;
            }

            public  synchronized boolean releaseLock(TransactionId tid,PageId pid){
                if(holdsLock(tid,pid)){
                    ConcurrentHashMap<TransactionId,Lock> nowPageLock = pageLocks.get(pid);
                    nowPageLock.remove(tid);
                    if(nowPageLock.size() == 0){
                        pageLocks.remove(pid);
                    }
                    this.notifyAll();
                    return true;

                }
                return false;
            }

            public synchronized boolean TransactonCommitted(TransactionId tid){ // when a transaction completes,release all the locks
                for(PageId pid : pageLocks.keySet()){
                    releaseLock(tid,pid);
                }
                return true;
            }

        }

在lockManager里的每一个方法前都加上了synchronized关键字,也是出于对多线程并发控制的考虑,其中acquireLock即是对以上获取锁的逻辑的代码实现,holdsLock查看页面p上是否有事务t的锁;releaseLock则是释放事务t在页面p上的锁;TransactionCommitted则是释放一个事务t获取过的所有锁,表示该事务完成。

Exercise 2+Exercise 5 getPage的完善和死锁解决

在此前实现的文件的读取页面的方法中,都是通过调用BufferPool的getpage()方法来完成的,因此只用在BufferPool中实现锁的获取即可。在此处我直接与Exercise5中的死锁检测和解决结合起来完善了getpage()方法。

在如HeapFile等文件的getPage函数中,调用BufferPoo的getpage方法时会传输一个Permission对象,表示对该页面的操作权限,可根据此来对应判断该获取哪一种锁。除了READ_ONLY是SHARED LOCK外,其余的都是EXCLUSIVE LOCK,因此getpage中需要加入这一段代码:

        int lockType = (perm == Permissions.READ_ONLY)? Lock.SHARE : Lock.EXCLUSIVE;
            //only READ_ONLY stands for a shared lock
            long begin = System.currentTimeMillis();
          Booleanan ifAcquired = false;
            while (!ifAcquired){
                ifAcquired = manager.acqureLock(pid,tid,lockType);
                long rightNow = System.currentTimeMillis();
                if(rightNow - begin > 500) {  //timeout policy
                    //System.out.println("time out"+begin+"  "+rightNow);
                    throw new TransactionAbortedException();
                }
            }

其中lockType会根据Permission的类型判断是哪一种锁,此后在while循环中不停尝试获取锁,如果在500毫秒内无法得到锁,那么说明有死锁存在,这便是Exercise 5中的超时检测死锁(timeouts)的机制。

此外,还可以用依赖图的原理实现死锁检测,由于进行该Lab4时没有学习到该部分内容,故没有实现。该方法的思路为:画出数据库访问的依赖图,然后可以利用深度优先搜索算法进行环的检测,如果出现有环,则说明是死锁,这是一种更加科学的方法,不过在性能方面没有做探究。

Exercise 2中还提到,在事务向page中的空slot插入tuple时,首先需要遍历该page上有无空slot,如果该page上没有空的slot,则需要创建一个新的page将tuple插入其中。此时事务不会对已满的page进行操作,但它依旧持有这些page上的写锁,其它事务也不能访问这些page,所以当判断某一page上的slot已满时,可以释放掉该page上的写锁。尽管不满足两段锁协议,但该事务并没有对page进行任何操作,所以并不会有任何影响,而且也可以让其他事务访问这些page,于是在HeapFile的insertTuple方法中,需要改写这一段:

        for(int i=0;i<numPages();i++){
                HeapPage page = (HeapPage) bp.getPage(tid, new HeapPageId(this.getId(),i),Permissions.READ_WRITE);  //读取对应页
                if(page.getNumEmptySlots()==0) {     //full
                    //added in lab4,when there's no emptslots, wewe could unlock the page
                    bp.releasePage(tid,new HeapPageId(this.getId(),i));
                    //this will do no harm to 2PL lock,cuz we didn't read any data
                    continue;
                }
                page.insertTuple(t);
                PageList.add(page);
                //page.markDirty(true,tid); // added in lab4
                return PageList;
            }

可见,当某page的空slots数为0时,将释放事务t对此页的锁。

Exercise 3 实现NO STEAL策略

在此前实现的汰换策略evictPage()中,会将最近最少使用的页面刷新到磁盘上然后从BufferPool中删除,然而在Lab4中,为了使事务运行正常,被事务修改过的dirtypages只有在commit之后才会写入到磁盘,因此Exercise3中要对evictPage()做一些改动,使得脏页不会被汰换:

        private synchronized  void evictPage() throws DbException {
            int length = recentUsedPages.size()-1;
            while (recentUsedPages.get(length).isDirty()!=null){  //NO STEAL policy
                length--;  //if it is a dirpage, do,do not evict it
                if(length<0)
                    throw new DbException("all pages are dirty");
            }
            Page page = recentUsedPages.remove(length);
            discardPage(page.getId());   //remove it from the BufferPool

        }

Exercise4 实现transactionComplete

BufferPool中transactionComplete()有重载,一个带有布尔参数(为true时,进行提交;false时进行回滚),另一个无参数,总是提交committed。

在提交事务t时,所有被t修改的脏页将全部刷新至磁盘(调用flushpages),然后t拥有的所有锁将全部释放;如果进行回滚,那么将先从内存中删去所有t修改过的脏页,然后从磁盘中重新读取这些内存页,具体代码如下:

        public void transactionComplete(TransactionId tid, boolean commit)
            throws IOException {
            // some code goes here
            // not necessary for lab1|lab2
            if(commit){
                try {
                    flushPages(tid);
                }catch (IOException e){
                    e.printStackTrace();
                }
            }
            else{ //recovery
                for(Page page : idToPage.values()){
                    PageId pid = page.getId();
                    if(tid.equals(page.isDirty())){    //if tid has modified this page,we try to recover it
                        int tableId = pid.getTableId();
                        DbFile file = Database.getCatalog().getDatabaseFile(tableId);
                        Page recoverPage = file.readPage(pid);   //reload the page
                        idToPage.put(pid,recoverPage);
                        recentUsedPages.add(0,recoverPage);  //reput it
                    }
                }
            }
            manager.TransactonCommitted(tid);
        }

其中,在LockManager中实现的TransactionCommitted就起到了作用,用于把事务t的所有锁释放。

重难点

本次实验的重难点主要在:

  1. 获取锁的逻辑:获取锁时不仅要判断页面是否有锁,锁的类型,有时候甚至要通过页面上锁的数量来进行分支操作;

  2. 死锁的判断和解决:对于如何判断事务间发生死锁也是重难点,因为此次实现的两段锁并没有保证死锁不会发生。而Lab4中我采用了超时检测死锁的方法,如果某事务在500ms内没有得到需要的锁,说明可能发生了死锁,则停止该事务并抛出TransactionAbortedException异常;

  3. NO STEAL策略也是重点之一,具体可以参见Exercise 3;

  4. 多线程并发读写时的管理:在此次的test中,包括多线程对于BufferPool的访问,其中LinkedArrayList类型的用以存储最近使用页面的recentUsedPage在最初面临多线程访问和修改时常常报错,因此我对其的add方法和remove方法在外层又进行了包装,用synchronized关键字修饰,使得多线程访问的安全:

                private synchronized void moveToHead(PageId pid){

                String thread = Thread.currentThread().getName();
                //System.out.println("modify from "+thread);
                for(int i = 0;i<recentUsedPages.size();i++){
                    Page page = recentUsedPages.get(i);
                    if(page.getId().equals(pid)) {
                        recentUsedPages.remove(i);
                        recentUsedPages.add(0,page);    //find the required page,and put it at the top as the recent used page
                        break;
                    }
                }
                //System.out.println("done from "+thread);
            }

            private synchronized void addRecentUsed(Page page){
                recentUsedPages.add(0,page);
            }

            private synchronized void delRecentUsed(PageId pid){

                for(int i = 0;i<recentUsedPages.size();i++){
                    Page page = recentUsedPages.get(i);
                    if(pid.equals(page.getId())) {
                        recentUsedPages.remove(i);
                        break;
                    }

注意:如果要通过test中的BTreeTest,则必须实现Tuple粒度的两段锁,此处不做讨论。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值