MIT6.830 lab4 一个简单数据库实现


前言

这次的效率还算比较高,主要是在家也没什么事情做,就写写报告好了。

完整代码


一、关于lab4?

In this lab, you will implement a simple locking-based transaction system in SimpleDB. You will need to add lock and unlock calls at the appropriate places in your code, as well as code to track the locks held by each transaction and grant locks to transactions as they are needed.
The remainder of this document describes what is involved in adding transaction support and provides a basic outline of how you might add this support to your database.

lab4让我们实现一个Page粒度的锁,遵从两阶段锁定来确保事务的隔离性和
原子性

两阶段锁协议

两阶段锁协议要求每个事务分两个节点提出加锁和解锁申请,引入两阶段锁协议是为了保证事务的隔离性,即多个事务在并发的情况下等同于串行的执行

  • 第一阶段:扩展阶段,事务可以申请获得任意数据上的任意锁,但是不能释放任何锁。
  • 第二阶段:收缩阶段,事务可以释放任何数据上的任何类型的锁,但是不能再次申请任何锁

在这里插入图片描述

在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段, 其余时间为加锁阶段

Page锁

lab4需要实现Page粒度共享锁和独占锁

在事务可以读取对象之前,它必须具有共享锁。
在事务可以写入对象之前,它必须具有独占锁。
多个事务可以在一个对象上具有共享锁。
只有一个事务可以在一个对象上具有独占锁。 如果事务 t 是唯一在对象 o 上持有共享锁的事务,则 t 可以将其在 o 上的锁升级为独占锁。
如果事务请求无法立即授予的锁,则代码应阻塞,等待该锁可用

课程地址

Lab地址

二、lab4

1.Exercise 1

Exercise 1主要是让我们去编写在缓冲池中获取和释放锁的方法,那么我们首先需要一个锁对象Lock,Lock的构造函数由permission许可权限和transactionId事务Id两组参数构造,其中permission包含READ_ONLY和 READ_WRITE两种权限。

public class Lock {
    
    
    private Permissions permissions;
    private TransactionId transactionId;

    public Lock(Permissions permissions, TransactionId transactionId) {
    
    
        this.permissions = permissions;
        this.transactionId = transactionId;
    }

    public TransactionId getTransactionId() {
    
    
        return transactionId;
    }

    public Permissions getPermissions() {
    
    
        return permissions;
    }

    public void setPermissions(Permissions permissions) {
    
    
        this.permissions = permissions;
    }

    @Override
    public String toString() {
    
    
        return "Lock{" +
                "permissions=" + permissions +
                ", transactionId=" + transactionId +
                '}';
    }
}

当然我们的BufferPool不是直接访问Lock创建锁,而是通过LockManager进行锁的申请。我们的锁由一个map结构保存,最开始的键值对本来是使用<Integer, List>来表示,即第几页:锁,可是在对多个表操作的时候就会出现问题,即注释中写的对两个表进行插入,第一个表的第1页插入后加了锁,如果是integer:list他们都是第0页,第二个表误以为自己加了锁,所以最后选择用PageId作为key。

public class LockManager {
    
    
    // integer:list --> 第几页:锁
//    private Map<Integer, List<Lock>> map; //锁表

    // 这里应该用 PageId:list --> 哪个表的第几页:锁
    //LogTest的测试用例TestAbortCommitInterleaved中,对两个表进行插入,第一个表的第1页插入后加了锁,如果是integer:list他们都是第0页,第二个表误以为自己加了锁
    private Map<PageId, List<Lock>> map; //锁表

    public LockManager() {
    
    
        this.map = new ConcurrentHashMap<>();

    }

    public synchronized Boolean acquireLock(TransactionId tid, PageId pageId, Permissions permissions) {
    
    
//        Integer pid = pageId.getPageNumber();
        Lock lock = new Lock(permissions, tid);
//        List<Lock> locks = map.get(pid);
        List<Lock> locks = map.get(pageId);
        if (locks == null) {
    
    
            locks = new ArrayList<>();
            locks.add(lock);
//            map.put(pid, locks);
            map.put(pageId, locks);
            return true;
        }
        if (locks.size() == 1) {
    
      //只有一个事务占有锁
            Lock firstLock = locks.get(0);
            if (firstLock.getTransactionId().equals(tid)) {
    
    
                if (firstLock.getPermissions().equals(Permissions.READ_ONLY) && lock.getPermissions().equals(Permissions.READ_WRITE)) {
    
    
                    firstLock.setPermissions(Permissions.READ_WRITE); //锁升级
                }
                return true;
            } else {
    
    
                if (firstLock.getPermissions().equals(Permissions.READ_ONLY) && lock.getPermissions().equals(Permissions.READ_ONLY)) {
    
    
                    locks.add(lock);
                    return true;
                }
                return false;
            }
        }
        //list中有多个事务则说明全是共享锁
        if (lock.getPermissions().equals(Permissions.READ_WRITE)) {
    
    
            return false;
        }
        //同一个事务重复获取读锁,不要进入列表!
        for (Lock lock1 : locks) {
    
    
            if (lock1.getTransactionId().equals(tid)) {
    
    
                return true;
            }
        }
        locks.add(lock);
        return true;
    }


    public synchronized void releaseLock(TransactionId transactionId, PageId pageId) {
    
    
//        List<Lock> locks = map.get(pageId.getPageNumber());
        List<Lock> locks = map.get(pageId);
        for (int i = 0; i < locks.size(); i++) {
    
    
            Lock lock = locks.get(i);
            // release lock
            if (lock.getTransactionId().equals(transactionId)) {
    
    
                locks.remove(lock);
                if (locks.size() == 0) {
    
    
//                    map.remove(pageId.getPageNumber());
                    map.remove(pageId);
                }
                return;
            }
        }
    }

    public synchronized void releaseAllLock(TransactionId transactionId) {
    
    
//        for (Integer k : map.keySet()) {
    
    
        for (PageId k : map.keySet()) {
    
    
            List<Lock> locks = map.get(k);
            for (int i = 0; i < locks.size(); i++) {
    
    
                Lock lock = locks.get(i);
                // release lock
                if (lock.getTransactionId().equals(transactionId)) {
    
    
                    locks.remove(lock);
                    if (locks.size() == 0) {
    
    
                        map.remove(k);
                    }
                    break;
                }
            }
        }
    }

    public synchronized Boolean holdsLock(TransactionId tid, PageId p) {
    
    
//        List<Lock> locks = map.get(p.getPageNumber());
        List<Lock> locks = map.get(p);
        for (int i = 0; i < locks.size(); i++) {
    
    
            Lock lock = locks.get(i);
            if (lock.getTransactionId().equals(tid)) {
    
    
                return true;
            }
        }
        return false;
    }
}

重写BufferPool的getPage(),通过一个死循环不断获取锁,如果超过一定时间还没获得锁,抛出TransactionAbortedException。

    public Page getPage(TransactionId tid, PageId pid, Permissions perm)
        throws TransactionAbortedException, DbException {
    
    
        // some code goes here
//        Page page = map.get(pid);
//        if (page == null) {
    
    
//            page = Database.getCatalog().getDatabaseFile(pid.getTableId()).readPage(pid);
//            addToBufferPool(pid, page);
//        }
//
//        return page;

//        boolean lockAcquired = false;
//        long start = System.currentTimeMillis();
//        long timeout = new Random().nextInt(2000);
//        while (!lockAcquired) {
    
    
//            long now = System.currentTimeMillis();
//            if (now - start > timeout) {
    
    
//                throw new TransactionAbortedException();
//            }
//            lockAcquired = lockManager.acquireLock(tid, pid, perm);
//        }

        long st = System.currentTimeMillis();
        while (true) {
    
    
            //获取锁,如果获取不到会阻塞
                if (lockManager.acquireLock(tid, pid, perm)) {
    
    
                    break;
                }
            long now = System.currentTimeMillis();
            if (now - st > 500) throw new TransactionAbortedException();
        }

        if (lruCache.get(pid) == null) {
    
    
            DbFile databaseFile = Database.getCatalog().getDatabaseFile(pid.getTableId());
            Page page = databaseFile.readPage(pid);
            addToBufferPool(pid, page);
            return page;

        } else {
    
    
            return lruCache.get(pid);
        }
    }

2.Exercise 2

Exercise 2主要是让我们对前面的一些调用了getPage()的方法打补丁,保证能够正确的获取和释放锁,比如HeapFile.insertTuple(),HeapFile.deleteTuple()。另外在插入元组时找空slot的时候需要READ_ONLY锁才能执行此操作。如果事务 t 在页面 p 上找不到空闲插槽,t 可能会立即释放 p 上的锁。虽然这显然与两阶段锁定的规则相矛盾,但没关系,因为t 没有使用页面中的任何数据,因此更新p 的并发事务 t’ 不可能影响 t 的答案或结果。说来惭愧,我在写这个报告的时候才发现,这部分提前释放没有实现,好在发现了,在if-else分支补上Database.getBufferPool().unsafeReleasePage(tid, new HeapPageId(getId(), i));即可

    public List<Page> insertTuple(TransactionId tid, Tuple t)
            throws DbException, IOException, TransactionAbortedException {
    
    
        // some code goes here
        //这里没标记脏页
//        ArrayList<Page> list = new ArrayList<>();
//        for (int i = 0; i < numPages(); i++) {
    
    
//            HeapPageId heapPageId = new HeapPageId(this.getId(), i);
//            HeapPage heapPage = (HeapPage) Database.getBufferPool().getPage(tid, heapPageId, Permissions.READ_WRITE);
//            if (heapPage.getNumEmptySlots() == 0) continue;
//
//            heapPage.insertTuple(t);
//            list.add(heapPage);
//            return list;
//        }
//
//        // create a empty page and load with 0
//        BufferedOutputStream bufferOS = new BufferedOutputStream(new FileOutputStream(this.file, true));
//        byte[] emptyData = HeapPage.createEmptyPageData();
//        bufferOS.write(emptyData);
//        bufferOS.close();
//
//        // load into the BufferPool
//        HeapPage page = (HeapPage) Database.getBufferPool().getPage(tid, new HeapPageId(getId(), numPages() - 1), Permissions.READ_WRITE);
//        page.insertTuple(t);
//        list.add(page);
//        return list;
        // not necessary for lab1
        ArrayList<Page> list = new ArrayList<>();
        BufferPool pool = Database.getBufferPool();
        int tableid = getId();
        for (int i = 0; i < numPages(); ++i) {
    
    
            HeapPage page = (HeapPage) pool.getPage(tid, new HeapPageId(tableid, i), Permissions.READ_WRITE);
            if (page.getNumEmptySlots() > 0) {
    
    
                page.insertTuple(t);
                page.markDirty(true, tid);
                list.add(page);
                return list;
            } else {
    
    
                Database.getBufferPool().unsafeReleasePage(tid, new HeapPageId(getId(), i));
            }
        }

        HeapPage page = new HeapPage(new HeapPageId(tableid, numPages()), HeapPage.createEmptyPageData());
        page.insertTuple(t);
        writePage(page);
        list.add(page);
        return list;
    }

3.Exercise 3

为了保证原子性,我们在事务提交时才对脏页进行写盘,那么我们前面进行页面淘汰的时候,如果是脏页的话就要跳过脏页,选择其他页面,如果所有页面都是脏的话就抛出DbException。

    private synchronized  void evictPage() throws DbException {
    
    
        // some code goes here
//        assert map.size() == numPages;
//        for (Page page : map.values()) {
    
    
//            if (page.isDirty() == null) { // no steal page is dirty, load to disk
//                map.remove(page.getId());
//                return;
//            }
//        }
//        throw new DbException("No Clean Page to EVICT");
        // not necessary for lab1
        //如果为脏页则不能替换
        Page value = lruCache.getTail().prev.value;
        //  如果是脏页
        if (value != null && value.isDirty() != null) {
    
    
            findNotDirty();
        } else {
    
    
            //不是脏页没改过,不需要写磁盘
            lruCache.discard();
        }
    }

    private void findNotDirty() throws DbException {
    
    
        LRUCache<PageId, Page>.DLinkedNode head = lruCache.getHead();
        LRUCache<PageId, Page>.DLinkedNode tail = lruCache.getTail();
        tail = tail.prev;
        while (head != tail) {
    
    
            Page value = tail.value;
            if (value != null && value.isDirty() == null) {
    
    
                lruCache.remove(tail);
                return;
            }
            tail = tail.prev;
        }
        throw new DbException("no dirty page");
    }

4.Exercise 4

每个事务都会有一个Transaction对象,TransactionId来唯一标识一个事务,当事务完成时,调用transactionComplete()。如果参数为true,会将TransactionId对应的脏页写到磁盘中,如果参数为false,会将TransactionId对应的脏页删除并从磁盘中获取原来的数据页。脏页处理完成后,会释放TransactionId持有的所有锁。

    public void transactionComplete(TransactionId tid, boolean commit) {
    
    
        // some code goes here
        // not necessary for lab1|lab2
        if (commit) {
    
    
            try {
    
    
                flushPages(tid);
            } catch (IOException e) {
    
    
                e.printStackTrace();
            }
        } else {
    
    
            rollback(tid);
        }
        lockManager.releaseAllLock(tid);

    }

5.Exercise 5

实现死锁检测,解决方案:

  • 超时,对每个事务设置一个获取锁的超时时间,如果在超时时间内获取不到锁,我们就认为可能发生了死锁,将该事务进行中断。
  • 循环等待图检测,我们可以建立事务等待关系的等待图,当等待图出现了环时,说明有死锁发生,在加锁前就进行死锁检测,如果本次加锁请求会导致死锁,就终止该事务

这里用的是超时方案,简单快捷嘛,Exercise 1已给出方法。


总结

整个lab4理解了做起来挺快的,记得最开始做的时候不知道Lock部分怎么实现,参考了网上前辈的方法,今天写报告的时候也发现了之前一些漏洞。还有两个lab报告没写,这些全部写完后如果有时间可能还是会考虑做一下lab3,说是这么说,但感觉应该没有多少时间了,毕竟学校里面的文章也是大头。

猜你喜欢

转载自blog.csdn.net/weixin_44153131/article/details/128848486