MIT6.830 lab4 SimpleDB Transactions

MIT6.830 lab4 SimpleDB Transactions

lab4要做的是让SimpleDB支持事务,基于严格两阶段封锁协议去实现原子性和隔离性的,所以开始前也需要了解两阶段封锁协议是如何实现事务的。

两阶段封锁协议
首先是封锁协议:我们将要求在系统中的每一个事务遵从封锁协议,封锁协议的一组规则规定事务何时可以对数据项们进行加锁、解锁。

对于两阶段封锁协议:两阶段封锁协议要求每个事务分两个节点提出加锁和解锁申请:

增长阶段:事务可以获得锁,但不能释放锁;
缩减阶段:事务可以释放锁,但不能获得新锁。
最初,事务处于增长阶段,事务根据需要获得锁。一旦该事务释放了锁,它就进入了缩减阶段,并且不能再发出加锁请求。

严格两阶段封锁协议不仅要求封锁是两阶段,还要求事务持有的所有排他锁必须在事务提交后方可释放。这个要求保证未提交事务所写的任何数据在该事务提交之前均已排他方式加锁,防止了其他事务读这些数据。

强两阶段封锁协议。它要求事务提交之前不释放任何锁。在该条件下,事务可以按其提交的顺序串行化。

锁转换:在两阶段封锁协议中,我们允许进行锁转换。我们用升级表示从共享到排他的转换,用降级表示从排他到共享的转换。锁升级只能发送在增长阶段,锁降级只能发生在缩减阶段。

Exercise1 Granting Locks

exercise1需要做的是在getPage获取数据页前进行加锁,这里我们使用一个LockManager来实现对锁的管理,LockManager中主要有申请锁、释放锁、查看指定数据页的指定事务是否有锁这三个功能,其中加锁的逻辑比较麻烦,需要基于严格两阶段封锁协议去实现。事务t对指定的页面加锁时,思路如下:

锁管理器中没有任何锁或者该页面没有被任何事务加锁,可以直接加读/写锁;

如果t在页面有锁,分以下情况讨论:

2.1 加的是读锁:直接加锁;

2.2 加的是写锁:如果锁数量为1,进行锁升级;如果锁数量大于1,会死锁,抛异常中断事务;

如果t在页面无锁,分以下情况讨论:

3.1 加的是读锁:如果锁数量为1,这个锁是读锁则可以加,是写锁就wait;如果锁数量大于1,说明有很多读锁,直接加;

3.2 加的是写锁:不管是多个读锁还是一个写锁,都不能加,wait

private class PageLockManager{
        ConcurrentHashMap<PageId,Vector<Lock>> lockMap;

        public PageLockManager(){
            lockMap = new ConcurrentHashMap<PageId,Vector<Lock>>();
        }

        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;
        }


        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;
        }


        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;
        }
    }

Exercise2 Lock Lifetime

Ensure that you acquire and release locks throughout SimpleDB. Some (but not necessarily all) actions that you should verify work properly:

  • Reading tuples off of pages during a SeqScan (if you implemented locking in BufferPool.getPage(), this should work correctly as long as your HeapFile.iterator() uses BufferPool.getPage().)
  • Inserting and deleting tuples through BufferPool and HeapFile methods (if you implemented locking in BufferPool.getPage(), this should work correctly as long as HeapFile.insertTuple() and HeapFile.deleteTuple() use BufferPool.getPage().)

You will also want to think especially hard about acquiring and releasing locks in the following situations:

  • Adding a new page to a HeapFile. When do you physically write the page to disk? Are there race conditions with other transactions (on other threads) that might need special attention at the HeapFile level, regardless of page-level locking?
  • Looking for an empty slot into which you can insert tuples. Most implementations scan pages looking for an empty slot, and will need a READ_ONLY lock to do this. Surprisingly, however, if a transaction t finds no free slot on a page p, t may immediately release the lock on p. Although this apparently contradicts the rules of two-phase locking, it is ok because t did not use any data from the page, such that a concurrent transaction t’ which updated p cannot possibly effect the answer or outcome of t.

exercise2主要是要让我们考虑什么时候要加锁,什么时候要解锁

‎您将需要实施严格的两阶段锁定。这意味着事务应在访问任何对象之前获取该对象的适当类型的锁,并且在事务提交之前不应释放任何锁。‎

‎SimpleDB 的设计使得在读取或修改页面之前可以在页面上获取锁。因此,我们建议不要在每个运算符中向锁定例程添加调用,而是在 中获取锁定。根据您的实现,您可能不必在其他任何地方获取锁。由您来验证这一点!‎在读取任何页面(或元组)之前,您需要在任何页面(或元组)上获取‎‎共享‎‎锁,并且在写入之前,您需要在任何页面(或元组)上获取‎‎独占‎‎锁。您会注意到我们已经在缓冲区池中传递对象;这些对象指示调用方希望在被访问对象上具有的锁定类型(我们已经为您提供了该类的代码)。

Exercise3 Implementing NO STEAL‎

Modifications from a transaction are written to disk only after it commits. This means we can abort a transaction by discarding the dirty pages and rereading them from disk. Thus, we must not evict dirty pages. This policy is called NO STEAL.

You will need to modify the evictPage method in BufferPool. In particular, it must never evict a dirty page. If your eviction policy prefers a dirty page for eviction, you will have to find a way to evict an alternative page. In the case where all pages in the buffer pool are dirty, you should throw a DbException. If your eviction policy evicts a clean page, be mindful of any locks transactions may already hold to the evicted page and handle them appropriately in your implementation.

‎事务中的修改仅在提交后写入磁盘。这意味着我们可以通过丢弃脏页并从磁盘重新读取它们来中止事务。因此,我们绝不能驱逐肮脏的页面。此策略称为"无窃取"。‎

‎您需要在 ‎‎BufferPool‎‎ 中修改 ‎‎extictPage‎‎ 方法。特别是,它绝不能驱逐脏页面。如果您的逐出政策更喜欢使用脏页面进行逐出,则必须找到一种方法来逐出替代页面。如果缓冲池中的所有页都是脏的,则应引发 ‎‎DbException‎‎。如果您的逐出策略逐出干净的页面,请注意任何锁定事务可能已经保留到已逐出的页面,并在您的实现中适当地处理它们。‎

为了支持原子性,我们对脏页的处理是在事务提交时才写入磁盘,或者事务中断时将脏页恢复成磁盘文件原来的样子。在之前我们实现的LRU缓存淘汰策略中,我们并没有对脏页加以区分。exercise4要我们在淘汰数据页时不能淘汰脏页,如果bufferpool全部是脏页则抛出异常,我们只需要修改淘汰页面时的代码

Exercise4 Transactions

In SimpleDB, a TransactionId object is created at the beginning of each query. This object is passed to each of the operators involved in the query. When the query is complete, the BufferPool method transactionComplete is called.

Calling this method either commits or aborts the transaction, specified by the parameter flag commit. At any point during its execution, an operator may throw a TransactionAbortedException exception, which indicates an internal error or deadlock has occurred. The test cases we have provided you with create the appropriate TransactionId objects, pass them to your operators in the appropriate way, and invoke transactionComplete when a query is finished. We have also implemented TransactionI

SimpleDB是如何实现事务的?

在SimpleDB中,每个事务都会有一个Transaction对象,我们用TransactionId来唯一标识一个事务,TransactionId在Transaction对象创建时自动获取。事务开始前,会创建一个Transaction对象,后续的操作会通过传递TransactionId对象去进行,加锁时根据加锁页面、锁的类型、加锁的事务id去进行加锁。当事务完成时,调用transactionComplete去完成最后的处理。transactionComplete会根据成功还是失败去分别处理,如果成功,会将事务id对应的脏页写到磁盘中,如果失败,会将事务id对应的脏页淘汰出bufferpool或者从磁盘中获取原来的数据页。脏页处理完成后,会释放事务id在所有数据页中加的锁。

Exercise5 Deadlocks and Aborts

什么时候会发生死锁?

1.如果两个事务t0,t1,两个数据页p0,p1,t0有了p1的写锁然后申请p0的写锁,t1有了p0的写锁然后申请p1的写锁,这个时候会发生死锁;

2.如果多个事务t0,t1,t2,t3同时对数据页p0都加了读锁,然后每个事务都要申请写锁,这种情况下只能每一个事务都不可能进行锁升级,所以需要有其中三个事务进行中断或者提前释放读锁,由于我们实现的是严格两阶段封锁协议,这里只能中断事务让其中一个事务先执行完。

死锁的解决方案?一般有两种解决方案:

1.超时。对每个事务设置一个获取锁的超时时间,如果在超时时间内获取不到锁,我们就认为可能发生了死锁,将该事务进行中断。

2.循环等待图检测。我们可以建立事务等待关系的等待图,当等待图出现了环时,说明有死锁发生,在加锁前就进行死锁检测,如果本次加锁请求会导致死锁,就终止该事务。

本文通过对每个事务设置一个获取锁的超时时间,如果在超时时间内获取不到锁,我们就认为可能发生了死锁,将该事务进行中断。这以简单的方法实现思索和终止,

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

实验总结

这个lab的实验应该是最难的一节,特别是事务和锁这种并发的玩意,幸亏提前学了并发的知识。不过还是有些不足重写了好几遍代码,也遇到了很多困难,这地方还带加强。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值