MIT6.830-Lab4
基础知识
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
记录数据页上存在哪些事务的何种类型的锁,结构图如下:
获取锁的思路如下:
实现代码如下,使用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()
方法先获取事务所有关联的页,如果参数commit
为True
,代表提交该事务,则会将脏页刷新到磁盘上,并释放该事务在对应页上的锁。如果参数commit
为false
,代表终止该事务,此时从磁盘读取每页的原始值,重新放入缓存中,再释放锁。
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
存放事务对数据页上锁的请求和分配情况,数据结构示意图如下:
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();
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步