Lab4主要包括5个exercise
exercise1、2 :实现一个page级别,遵从二段锁协议的锁管理器。即在访问任何page之前,事务应该获取该page上适当类型的锁,并且在事务提交之前不应该释放任何锁。
exercise3 :完善BufferPool中的evictPage()方法,避免数据丢失,当需要置换的页面为脏页时,要跳过脏页,置换掉不是脏页的page
exercise4 :实现事务的功能,当事务提交时,将事务涉及的脏页写回磁盘,然后释放锁。当事务回滚时,清理该事务涉及到的脏页,重新从磁盘中读取清理的page
exercise5:实现死锁防范功能,当发生死锁时,抛出AbortException
两段锁协议
- 第一阶段:扩展阶段,事务可以申请获得任意数据上的任意锁,但是不能释放任何锁。
- 第二阶段:收缩阶段,事务可以释放任何数据上的任何类型的锁,但是不能再次申请任何锁
遵守两端所协议可能会发生死锁:两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁。
simpleDB中的实现过程为,事务提交之前可以获取任何锁,事务提交之后释放该事务所拥有的所有锁。同时在获取锁的过程中进行死锁检测。
实验概述
事务
事务是一组以原子方式执行的数据库操作(例如,插入、删除和读取);也就是说,要么所有的动作都完成了,要么一个动作都没有完成。
- 原子性:通过两段锁协议和BufferPool的管理实现simpleDB的原子性
- 一致性:通过原子性实现事务的一致性,simpleDB中没有解决其他一致性问题(例如,键约束)
- 隔离性:严格的两段锁提供隔离
- 持久性:事务提交时将脏页强制写进磁盘
锁的赋予
- 在事务可以读取一个对象之前,它必须拥有一个共享锁。
- 在一个事务可以写一个对象之前,它必须有一个排他锁。
- 多个事务可以在一个对象上拥有一个共享锁。
- 只有一个事务可能对一个对象具有排他锁。
- 如果对象o上只有事务t持有共享锁,则t可以将 其对o的锁升级为排他锁。
exercise 1
在BufferPool中编写获取和释放锁的方法 ,构造一个page级别的锁管理器
在BufferPool中构造两个类,PageLock
类和LockManager
类。当事务通过BufferPool访问page时,需要通过LockManager申请锁。
PageLock类
存储事务id和该事务获取的锁的类型。
private class PageLock{
public static final int SHARE = 0;
public static final int EXCLUSIVE = 1;
private TransactionId tid;
private int type;
public PageLock(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类
参数:
ConcurrentHashMap<PageId,ConcurrentHashMap<TransactionId,PageLock>> lockMap;
:pageId与锁的映射,记录page上现存的锁。
方法:
public synchronized boolean acquireLock(PageId pageId, TransactionId tid, int requiredType)
:事务向LockManager请求获取page上锁的方法,申请思路如下:
根据以上思路实现acquireLock的主要内容
public synchronized boolean acquireLock(PageId pageId, TransactionId tid, int requiredType) throws TransactionAbortedException, InterruptedException {
final String lockType = requiredType == 0 ? "read lock" : "write lock";
final String thread = Thread.currentThread().getName();
if(lockMap.get(pageId) == null){
PageLock pageLock = new PageLock(tid,requiredType);
ConcurrentHashMap<TransactionId,PageLock> pageLocks = new ConcurrentHashMap<>();
pageLocks.put(tid,pageLock);
lockMap.put(pageId,pageLocks);
//System.out.println(thread + ": the " + pageId + " have no lock, transaction" + tid + " require " + lockType + ", accept");
return true;
}
ConcurrentHashMap<TransactionId,PageLock> pageLocks = lockMap.get(pageId);
if(pageLocks.get(tid) == null){
// tid没有该page上的锁
if(pageLocks.size() > 1){
//page 上有其他事务的读锁
if (requiredType == PageLock.SHARE){
//tid 请求读锁
PageLock pageLock = new PageLock(tid,PageLock.SHARE);
pageLocks.put(tid,pageLock);
lockMap.put(pageId,pageLocks);
//System.out.println(thread + ": the " + pageId + " have many read locks, transaction" + tid + " require " + lockType + ", accept and add a new read lock");
return true;
}
if (requiredType == PageLock.EXCLUSIVE){
// tid 需要获取写锁
wait(20);
System.out.println(thread + ": the " + pageId + " have lock with diff txid, transaction" + tid + " require write lock, await...");
return false;
}
}
if (pageLocks.size() == 1){
//page 上有一个其他事务的锁 可能是读锁,也可能是写锁
PageLock curLock = null;
for (PageLock lock : pageLocks.values()){
curLock = lock;
}
if (curLock.getType() == PageLock.SHARE){
//如果是读锁
if (requiredType == PageLock.SHARE){
// tid 需要获取的是读锁
PageLock pageLock = new PageLock(tid,PageLock.SHARE);
pageLocks.put(tid,pageLock);
lockMap.put(pageId,pageLocks);
//System.out.println(thread + ": the " + pageId + " have one read lock with diff txid, transaction" + tid + " require read lock, accept and add a new read lock");
return true;
}
if (requiredType == PageLock.EXCLUSIVE){
// tid 需要获取写锁
wait(10);
System.out.println(thread + ": the " + pageId + " have lock with diff txid, transaction" + tid + " require write lock, await...");
return false;
}
}
if (curLock.getType() == PageLock.EXCLUSIVE){
// 如果是写锁
wait(10);
System.out.println(thread + ": the " + pageId + " have one write lock with diff txid, transaction" + tid + " require read lock, await...");
return false;
}
}
}
if (pageLocks.get(tid) != null){
// tid有该page上的锁
PageLock pageLock = pageLocks.get(tid);
if (pageLock.getType() == PageLock.SHARE){
// tid 有 page 上的读锁
if (requiredType == PageLock.SHARE){
//tid 需要获取的是读锁
//System.out.println(thread + ": the " + pageId + " have one lock with same txid, transaction" + tid + " require " + lockType + ", accept");
return true;
}
if (requiredType == PageLock.EXCLUSIVE){
//tid 需要获取的是写锁
if(pageLocks.size() == 1){
// 该page上 只有tid的 读锁,则可以将其升级为写锁
pageLock.setType(PageLock.EXCLUSIVE);
pageLocks.put(tid,pageLock);
//System.out.println(thread + ": the " + pageId + " have read lock with same txid, transaction" + tid + " require write lock, accept and upgrade!!!");
return true;
}
if (pageLocks.size() > 1){
// 该page 上还有其他事务的锁,则不能升级
System.out.println(thread + ": the " + pageId + " have many read locks, transaction" + tid + " require write lock, abort!!!");
throw new TransactionAbortedException();
}
}
}
if (pageLock.getType() == PageLock.EXCLUSIVE){
// tid 有 page上的写锁
//System.out.println(thread + ": the " + pageId + " have write lock with same txid, transaction" + tid + " require " + lockType + ", accept");
return true;
}
}
System.out.println("----------------------------------------------------");
return false;
}
public synchronized boolean isholdLock(TransactionId tid, PageId pid)
:判断指定事务是否持有某一page上的锁
public synchronized boolean isholdLock(TransactionId tid, PageId pid){
ConcurrentHashMap<TransactionId,PageLock> pageLocks;
pageLocks = lockMap.get(pid);
if(pageLocks == null){
return false;
}
PageLock pageLock = pageLocks.get(tid);
if(pageLock == null){
return false;
}
return true;
}
public synchronized boolean releaseLock(TransactionId tid, PageId pid)
:释放指定事务在某一page上的所有锁
public synchronized boolean releaseLock(TransactionId tid, PageId pid){
if (isholdLock(tid,pid)){
ConcurrentHashMap<TransactionId,PageLock> pageLocks = lockMap.get(pid);
pageLocks.remove(tid);
if (pageLocks.size() == 0){
lockMap.remove(pid);
}
this.notifyAll();
return true;
}
return false;
}
public synchronized void completeTransaction(TransactionId tid)
:事务执行完成,释放该事务在所有page上的锁
public synchronized void completeTransaction(TransactionId tid){
Set<PageId> pageIds = lockMap.keySet();
for (PageId pageId : pageIds){
releaseLock(tid,pageId);
}
//System.out.println(Thread.currentThread().getName() + " transaction" + tid + " release the lock on the all locks");
}
exercise2、5
完善BufferPool中的getPage()方法、并实现死锁检测
Permissions类:枚举类,有READ_ONLY
、 READ_WRITE
两个属性。当getPage()
方法传入READ_ONLY
时,表明该事务请求的是share_lock。传入READ_WRITE
时,请求的是exclusive_lock。
同时还要检查以往Lab中调用getPage()方法的其他方法(HeapFile.insertTuple()
、 HeapFile.deleteTuple()
)是否传入了相应的Permissions对象。除此之外,要对HeapFile.insertTuple()
进行完善,当事务向page中的空slot插入tuple时,首先需要获取该page上的锁。如果该page上没有空的slot,需要访问下一个page,如果该HeapFile上所有的page都已满,需要创建一个新的page将tuple插入其中。但是,虽然该事务不会对已满的page进行操作,可此时它依旧持有这些page上的锁,其它事务也不能访问这些page,所以当判断某一page上的slot已满时,需要释放掉该page上的锁。虽然这不满足两段锁协议,但该事务并没有使用page中的任何数据,后序也不会用到,所以并不会有任何影响,而且也可以让其他事务访问那些slot已满的page。
解决死锁的方式有:
- 超时等待:对每个事务设置一个获取锁的超时时间,如果在超时时间内获取不到锁,我们就认为可能发生了死锁,将该事务进行中断。
- 循环等待图检测:建立事务等待关系的等待图,当等待图出现了环时,说明有死锁发生,在加锁前就进行死锁检测,如果本次加锁请求会导致死锁,就终止该事务。
本exercise中采取的是超时等待的方式解决死锁。
完善getPage方法
当事务通过BufferPool的getPage()
方法获取page时,先确定该事务需要获取锁的类型。通过LockManager获取响应类型的锁,成功获取则正常读取页面。获取失败时,执行acquireLock()
方法的线程会进行超时等待,等待超时或者被其他线程唤醒后会再次尝试获取锁,等待时间超过500ms时,对该事务进行回滚并抛出异常,避免死锁的发生。
public synchronized Page getPage(TransactionId tid, PageId pid, Permissions perm)
throws TransactionAbortedException, DbException {
// some code goes here
//------------------------lab3添加的内容------------------
int lockType;
if (perm == Permissions.READ_ONLY){
lockType = PageLock.SHARE;
} else {
lockType = PageLock.EXCLUSIVE;
}
long st = System.currentTimeMillis();
boolean isacquired = false;
while(!isacquired){
try {
isacquired = lockManager.acquireLock(pid,tid,lockType);
} catch (InterruptedException e) {
e.printStackTrace();
}
long now = System.currentTimeMillis();
if(now - st > 500){
throw new TransactionAbortedException();
}
}
//------------------------lab3添加的内容------------------
if(!bufferPool.containsKey(pid)){
DbFile dbFile = Database.getCatalog().getDatabaseFile(pid.getTableId());
Page page = dbFile.readPage(pid);
LinkedNode node = new LinkedNode(pid,page);
if(numPages > bufferPool.size()){
addToHead(node);
bufferPool.put(pid,node);
return node.page;
}
else{
//LRU
evictPage();
addToHead(node);
assert bufferPool.size() < numPages;
bufferPool.put(page.getId(), node);
return page;
}
}
else{
LinkedNode node = bufferPool.get(pid);
moveToHead(node);
return node.page;
}
}
public void unsafeReleasePage(TransactionId tid, PageId pid) {
// some code goes here
// not necessary for lab1|lab2
lockManager.releaseLock(tid,pid);
}
完善InsertTuple方法
完善HeapFile中的InsertTuple方法
public List<Page> insertTuple(TransactionId tid, Tuple t)
throws DbException, IOException, TransactionAbortedException {
// some code goes here
// not necessary for lab1
ArrayList<Page> arrayList = new ArrayList<>();
HeapPage heapPage;
for(int pgNo=0; pgNo<numPages(); pgNo++){
HeapPageId pageId = new HeapPageId(getId(),pgNo);
heapPage = (HeapPage) Database.getBufferPool().getPage(tid,pageId,Permissions.READ_WRITE);
if(heapPage.getNumEmptySlots() == 0){
//完善的内容,当该page上没有空slot时,释放该page上的锁,避免影响其他事务的访问
Database.getBufferPool().unsafeReleasePage(tid,pageId);
continue;
}
else {
heapPage.insertTuple(t);
arrayList.add(heapPage);
return arrayList;
}
}
BufferedOutputStream bw = new BufferedOutputStream(new FileOutputStream(file,true));
byte[] emptyPage = HeapPage.createEmptyPageData();
bw.write(emptyPage);
bw.close();
HeapPageId newPageId = new HeapPageId(getId(),numPages()-1);
HeapPage newPage = (HeapPage) Database.getBufferPool().getPage(tid,newPageId,Permissions.READ_WRITE);
newPage.insertTuple(t);
arrayList.add(newPage);
return arrayList;
}
exercise3
实现NO STEAL策略
事务对page的修改只有在commit之后才会写入到磁盘,但是在之前的Lab中实现页面置换策略时,当置换掉的页面是dirty page时,也会将更改写回到磁盘。这是不允许的,所以需要完善evictPage()
方法,当需要置换的page是dirty page时,需要跳过此page,去置换下一个非dirty的page。当BufferPool中缓存的page都是dirty page时,抛出异常。
完善evictPage()方法
private synchronized void evictPage() throws DbException {
// some code goes here
// not necessary for lab1
for (int i=0; i<numPages; i++){
LinkedNode tail = removeTail();
Page evictPage = tail.getPage();
if (evictPage.isDirty() != null){
addToHead(tail);
}
else{
PageId evictPageId = tail.getPageId();
discardPage(evictPageId);
return;
}
}
throw new DbException("all pages are dirty page ");
}
exercise 4
实现BufferPool中的transactionComplete方法
transactionComplete()
有两个版本,一个接受额外的布尔参数(当布尔参数为true时,进行提交。false时进行回滚),另一个不接受。没有附加参数的版本应该总是提交,因此可以简单地通过调用来实现 transactionComplete(tid, true)。当进行回滚时,从BufferPool中清除掉该事务造成的脏页,并将原始版本重新读到BufferPool中
public void transactionComplete(TransactionId tid){
// some code goes here
// not necessary for lab1|lab2
transactionComplete(tid,true);
}
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 {
restorePages(tid);
}
lockManager.completeTransaction(tid);
}
public synchronized void restorePages(TransactionId tid){
for(LinkedNode node : bufferPool.values()){
PageId pageId = node.getPageId();
Page page = node.getPage();
if(tid.equals(page.isDirty())){
int tableId = pageId.getTableId();
DbFile table = Database.getCatalog().getDatabaseFile(tableId);
Page pageFromDisk = table.readPage(pageId);
node.setPage(pageFromDisk);
bufferPool.put(pageId,node);
moveToHead(node);
}
}
}