MIT6.830-Lab4

simpleDB项目地址

基础知识

two-phase locking 两阶段锁:事务在操作一个对象时,必须先获得该对象上对应类型的锁,并且只有在事务提交时,才能释放锁。(Lab4要求实现页级别的锁,在之前的Lab中,每当操作表记录时,都会通过BufferPool.getPage()方法获取该记录所在的页对象,所以只需要在该方法中添加上锁的逻辑),两阶段锁保证一个事务要么在需要的时候获得所需的锁,要么等待直到它可以获得所有需要的锁为止。这确保了在事务访问数据时,其他事务无法同时修改这些数据,从而维护了事务的隔离性和原子性。

No-Steal/Force 策略No-Steal代表不允许未提交的事务将脏页刷新到磁盘上(这样就不需要undo日志,想要回滚时直接从磁盘上重新读取),Force代表每次提交事务时,都将与该事务相关的所有脏页刷新到磁盘上(这样就不需要redo日志,文档中说不需要考虑提交时崩溃的场景,只要事务提交成功,磁盘上一定存有该数据),Force操作保证了事务的一致性。

实验部分

实验1

实验1是整个Lab中最重要的部分,要求我们实现一个基于事务的锁管理器(此块参考了之前大佬的实现思路:Lab4),使用ConcurrentHashMap<PageId,ConcurrentHashMap<TransactionId,PageLock>> lockMap记录数据页上存在哪些事务的何种类型的锁,结构图如下:
image
获取锁的思路如下:
image
实现代码如下,使用synchronized关键字保障并发操作,使用DependencyGraph进行死锁检测(之后会详细说明):

public synchronized boolean acquireLock(TransactionId tid,PageId pageId,int lockType) throws TransactionAbortedException {
        ConcurrentHashMap<TransactionId, PageLock> tidLockMap = lockMap.get(pageId);
        // 页面上没锁
        if(tidLockMap == null){
            PageLock pageLock = new PageLock(tid, pageId, lockType);
            tidLockMap = new ConcurrentHashMap<>();
            tidLockMap.put(tid,pageLock);
            lockMap.put(pageId,tidLockMap);
            return true;
        } else if(tidLockMap.containsKey(tid)){
            // 页面上有该事务的锁
            // 请求读锁,因为页面上该事务的锁要么是写锁要么是读锁,所以直接返回true
            if(lockType == PageLock.SHARE)
                return true;
            // 请求写锁
            else if(lockType == PageLock.EXCLUSIVE) {
                PageLock pageLock = tidLockMap.get(tid);
                // 页面上该事务本身就获取了写锁,直接返回true
                if (pageLock.getType() == PageLock.EXCLUSIVE)
                    return true;
                else {
                    // 此时页面上该事务只有读锁且只有该事务持有该页面上的锁,进行锁升级
                    if (tidLockMap.size() == 1) {
                        pageLock.setType(PageLock.EXCLUSIVE);
                        return true;
                    } else {
                        dependencyGraph.addDependencies(tid,pageId,lockType,lockMap.get(pageId));
                        // 此时有其它事务持有页面上的读锁,和写锁互斥
                        return false;
                    }
                }
            } else {
                throw new RuntimeException("the lockType is illegal");
            }
        } else {
            //页面上没有该事务的锁,所以在size>1时,说明该页面上全是读锁
            if(tidLockMap.size() > 1){
                if(lockType == PageLock.SHARE){
                    PageLock pageLock = new PageLock(tid, pageId, PageLock.SHARE);
                    tidLockMap.put(tid,pageLock);
                    return true;
                }else{
                    dependencyGraph.addDependencies(tid,pageId,lockType,lockMap.get(pageId));
                    return false;
                }

            }else {
                // 页面上的锁只有一个,根据情况返回
                PageLock pageLock = null;
                for(TransactionId transactionId:tidLockMap.keySet())
                    pageLock = tidLockMap.get(transactionId);
                if(pageLock.getType() == PageLock.EXCLUSIVE){
                    dependencyGraph.addDependencies(tid,pageId,lockType,lockMap.get(pageId));
                    return false;
                }
                else {
                    if(lockType == PageLock.EXCLUSIVE){
                        dependencyGraph.addDependencies(tid,pageId,lockType,lockMap.get(pageId));
                        return false;
                    }
                    else {
                        PageLock newPageLock = new PageLock(tid, pageId, PageLock.SHARE);
                        tidLockMap.put(tid,newPageLock);
                        return true;
                    }
                }
            }
        }
    }

此外,还实现了isHoldLock()releaseLock()方法,前者来判断某页上是否含有某事物的锁,后者来释放某页上的含有的某个事务的锁:

 public synchronized boolean isHoldLock(TransactionId tid,PageId pageId){
        ConcurrentHashMap<TransactionId, PageLock> tidLockMap = lockMap.get(pageId);
        if(tidLockMap==null || !tidLockMap.containsKey(tid))
            return false;
        else
            return true;
    }
public synchronized void releaseLock(TransactionId tid,PageId pageId){
        if(isHoldLock(tid,pageId)){
            ConcurrentHashMap<TransactionId, PageLock> tidLockMap = lockMap.get(pageId);
            tidLockMap.remove(tid);
            if(tidLockMap.size() == 0)
                lockMap.remove(pageId);
	//在释放锁时,也要将依赖图中的相应的边取消
            dependencyGraph.removeDependencies(tid,pageId);
        }
    }

BufferPool中的getPage()如下,首先通过lockManager尝试获取数据页的锁权限,如果失败就一直自旋阻塞(死锁问题由DependencyGraph进行检测,如果出现死锁则将该事务终止并释放其持有的所有锁,打破死锁的必要条件不剥夺),值得注意的是,如果缓存中缓存中全部是脏页并且没有请求的数据页,会释放该事务对该请求数据页已获得的锁,并抛出异常。

public  Page getPage(TransactionId tid, PageId pid, Permissions perm)
        throws TransactionAbortedException, DbException {
        int lockType = 0;
        if(perm == Permissions.READ_WRITE)
            lockType = 1;
        while(true){
            if(lockManager.acquireLock(tid,pid,lockType))
                break;
        }

        if(map.size() >= pageNum){
            if(map.containsKey(pid)){
                return map.get(pid);
            }else {
                try {
                    evictPage();
                } catch (DbException e){
                    lockManager.releaseLock(tid,pid);
                    throw e;
                }

                Page page = Database.getCatalog().getDatabaseFile(pid.getTableId()).readPage(pid);
                map.put(pid,page);
                return page;
            }
        }else {
            if(map.containsKey(pid)){
                return map.get(pid);
            }else {
                Page page = Database.getCatalog().getDatabaseFile(pid.getTableId()).readPage(pid);
                map.put(pid,page);
                return page;
            }
        }
    }

实验2

修改HeapFile中的insertTuple()方法,注意在查找能插入记录的数据页时,会对遍历过的数据页上读锁,这会导致其它事务无法修改这些数据页,所以要在遍历后及时释放数据页上的读锁,虽然违背了两阶段提交原则,但由于未使用数据页中的数据,这也是可以接受的。此外,在新建数据页时要对整个表上锁,以防多个线程建立多个新数据页。

public List<Page> insertTuple(TransactionId tid, Tuple t)
            throws DbException, IOException, TransactionAbortedException {
        // 返回的pages会被标记成脏页,留待BufferPool统一刷盘
        ArrayList<Page> pages = new ArrayList<>(1);
        int pageNo = 0;
        for(;pageNo<this.numPages();pageNo++){
            HeapPageId pageId = new HeapPageId(getId(), pageNo);
            HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, pageId, Permissions.READ_WRITE);
            if(page.getNumEmptySlots() > 0){
                page.insertTuple(t);
                pages.add(page);
                return pages;
            }else{
                //因为未修改页的内容,所以虽然违背了2pl,也可以释放锁
                Database.getBufferPool().unsafeReleasePage(tid,pageId);
            }
        }
        // 走到这步说明已有的page不满足要求,需要创建新页,该新创建的页要立刻写入磁盘(单元测试的意思是这样)
        // 使用空的字节数组创建HeapPage,会划分好header和tuple部分,并初始化为未使用
        synchronized (file){
            // 为了避免多个线程同时创建新数据页,使用synchronized对表文件上锁
            if(pageNo == this.numPages()){
                // 没有其它线程新建数据页,由本线程新建
                HeapPage newPage = new HeapPage(new HeapPageId(getId(), this.numPages()), HeapPage.createEmptyPageData());
                newPage.insertTuple(t);
                this.writePage(newPage);
                // 新建的页加入缓存
                Database.getBufferPool().getPage(tid, newPage.getId(), Permissions.READ_WRITE);
                pages.add(newPage);
                return pages;
            }else {
                // 已有其它线程新建数据页,直接利用
                HeapPageId pageId = new HeapPageId(getId(), this.numPages());
                HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, pageId, Permissions.READ_WRITE);
                page.insertTuple(t);
                pages.add(page);
                return pages;
            }

        }

    }

实验3

eivctPage()方法只驱逐clean页,并会将lockManager中该页上所有事务的锁信息删除。

实验4

transactionComplete()方法先获取事务所有关联的页,如果参数commitTrue,代表提交该事务,则会将脏页刷新到磁盘上,并释放该事务在对应页上的锁。如果参数commitfalse,代表终止该事务,此时从磁盘读取每页的原始值,重新放入缓存中,再释放锁。

public void transactionComplete(TransactionId tid, boolean commit) {
        List<PageId> list = lockManager.getPageIdWithTID(tid);
        if(commit == true){
            for(PageId pageId:list){
                try {
                    flushPage(pageId);
                    lockManager.releaseLock(tid,pageId);
                    // lab6中要求对每个提交后的页都要重新设置beforeImage
                    map.get(pageId).setBeforeImage();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }else {
            for(PageId pageId:list){
                Page page = Database.getCatalog().getDatabaseFile(pageId.getTableId()).readPage(pageId);
                map.put(pageId,page);
                lockManager.releaseLock(tid,pageId);
            }
        }

    }

实验5

要求实现死锁检测,根据文档描述,检测事务对页面上锁的请求图中是否存在环路,如果有,则说明存在死锁,此时终止导致死锁产生的事务t
新建DependencyGraph类,使用HashMap<TransactionId, HashSet<Pair<PageId, TransactionId>>> requestEdge存放事务对数据页上锁的请求和分配情况,数据结构示意图如下:
image
addDependency()方法

private void addDependency(TransactionId requester,TransactionId owner,PageId pid) throws TransactionAbortedException {
        if(requester.equals(owner))
            return;

        //为了死锁检测时,requestEdge能包含所有的Transaction节点,要加入owner,
        if(!requestEdge.containsKey(owner))
            requestEdge.put(owner,new HashSet<>());

        if(!requestEdge.containsKey(requester))
            requestEdge.put(requester,new HashSet<>());
        HashSet<Pair<PageId, TransactionId>> pairs = requestEdge.get(requester);
        pairs.add(new Pair<>(pid,owner));
        if(detectDeadLock()){
            // 检测到死锁,那么要将该事务终止,删除所有对该事务的请求边
            Database.getBufferPool().transactionComplete(requester,false);
            requestEdge.remove(requester);// 删除该事务所有的请求
            throw new TransactionAbortedException();
        }
    }

detectDeadLock()方法:使用拓扑排序检测环

 public synchronized boolean detectDeadLock(){
        // 通过拓扑排序的算法检测环
        HashMap<TransactionId, Integer> indegrees = new HashMap<>();
        for(Map.Entry<TransactionId, HashSet<Pair<PageId, TransactionId>>> entry : requestEdge.entrySet()) {
            TransactionId source = entry.getKey();
            if(!indegrees.containsKey(source))
                indegrees.put(source, 0);

            HashSet<Pair<PageId, TransactionId>> destinations = entry.getValue();
            for(Pair<PageId, TransactionId> p : destinations) {
                TransactionId dest = p.getValue();
                if(!indegrees.containsKey(dest))
                    indegrees.put(dest, 0);
                indegrees.put(dest, indegrees.get(dest)+1);
            }
        }

        Queue<TransactionId> sources = new LinkedList<>();
        for(Map.Entry<TransactionId, Integer> entry : indegrees.entrySet()) {
            int indegree = entry.getValue();
            if(indegree == 0) {
                sources.add(entry.getKey());
            }
        }
        int visitedCount = 0;
        while(sources.size() > 0) {
            TransactionId current = sources.poll();
            visitedCount++;
            HashSet<Pair<PageId, TransactionId>> children = requestEdge.get(current);
            for(Pair<PageId, TransactionId> p : children) {
                TransactionId childId = p.getValue();
                indegrees.put(childId, indegrees.get(childId)-1);
                if(indegrees.get(childId) == 0)
                    sources.add(childId);
            }
        }
        return visitedCount!=requestEdge.size();
    }
posted @   rockdow  阅读(18)  评论(0编辑  收藏  举报
努力加载评论中...
点击右上角即可分享
微信分享提示