代码地址:GitHub - zhangqingqing24630/MySQL-
此项目来源于何人听我楚狂声
目录
本节将收尾 VM 层,以及mysql 如何检测 2PL 导致的死锁,还介绍了一些上一章节没有介绍到的方法。
死锁
死锁的造成
在mysql中,两阶段锁协议(2PL)通常包括扩张和收缩两个阶段。在扩张阶段,事务将获取锁,但不能释放任何锁。在收缩阶段,可以释放现有的锁,但不能获取新的锁,这种规定存在着死锁的风险。
例如:当T1’在扩张阶段,获取了Y的读锁,并读取了Y,此时想要去获取X的写锁,却发现T2’的读锁锁定了X,而T2’也想要获取Y的写锁。简而言之,T1’不得到X是不会释放Y的,T2’不得到Y也是不会释放X的,这便陷入了循环,便形成了死锁。
死锁检测
前面提到了 2PL 会阻塞事务,直至持有锁的线程释放锁。可以将这种等待关系抽象成有向边,例如 Tj 在等待 Ti,就可以表示为 Tj --> Ti。这样,无数有向边就可以形成一个图(不一定是连通图)。检测死锁也就简单了,只需要查看这个图中是否有环即可。
创建一个 LockTable 对象,在内存中维护这张图。维护结构如下:
public class LockTable {
private Map<Long, List<Long>> x2u; // 某个XID已经获得的资源的UID列表
private Map<Long, Long> u2x; // UID被某个XID持有
private Map<Long, List<Long>> wait; // 正在等待UID的XID列表
private Map<Long, Lock> waitLock; // 正在等待资源的XID的锁
private Map<Long, Long> waitU; // XID正在等待的UID
private Lock lock;
...
}
在每次出现等待的情况时,就尝试向图中增加一条边,并进行死锁检测。如果检测到死锁,就撤销这条边,不允许添加,并撤销该事务。
// 不需要等待则返回null,否则返回锁对象
// 会造成死锁则抛出异常
public Lock add(long xid, long uid) throws Exception {
lock.lock();
try {
//某个xid已经获得的资源的UID列表,如果在这个列表里面,则不造成死锁,也不需要等待
if(isInList(x2u, xid, uid)) {//x2u xid list<uid>
return null;
}
//这里表示有了一个新的uid,则把uid加入到u2x和x2u里面,不死锁,不等待
//u2x uid被某个xid占有
if(!u2x.containsKey(uid)) {//uid xid
u2x.put(uid, xid);
putIntoList(x2u, xid, uid);
return null;
}
//以下就是需要等待的情况
//多个事务等待一个uid的释放
waitU.put(xid, uid);//把需要等待uid的xid添加到等待列表里面
//System.out.println("waitU"+waitU);
putIntoList(wait, uid, xid);//uid list<xid> 正在等待uid的xid列表
//造成死锁
if(hasDeadLock()) {
//从等待列表里面删除
waitU.remove(xid);
removeFromList(wait, uid, xid);
throw Error.DeadlockException;
}
//没有造成死锁,但是需要等待新的xid获得锁
Lock l = new ReentrantLock();
l.lock();
waitLock.put(xid, l);
return l;
} finally {
lock.unlock();
}
}
调用 add,如果不需要等待则进行下一步,如果需要等待的话,会返回一个上了锁的 Lock 对象。调用方在获取到该对象时,需要尝试获取该对象的锁,由此实现阻塞线程的目的,如下。
Lock l = lt.add(xid, uid);
if(l != null) {
l.lock(); // 其他阻塞在这一步
l.unlock();
}
如何判断死锁
查找图中是否有环的算法实际上就是一个深搜,只是需要注意这个图不一定是连通图。思路就是为每个节点设置一个访问戳,都初始化为 1,随后遍历所有节点,以每个非 1 的节点作为根进行深搜,并将深搜该连通图中遇到的所有节点都设置为同一个数字,不同的连通图数字不同。这样,如果在遍历某个图时,遇到了之前遍历过的节点,说明出现了环。
实现如下:
private boolean hasDeadLock() {
xidStamp = new HashMap<>();
stamp = 1;
System.out.println("xid已经持有哪些uid x2u="+x2u);//xid已经持有哪些uid
System.out.println("uid正在被哪个xid占用 u2x="+u2x);//uid正在被哪个xid占用
for(long xid : x2u.keySet()) {//已经拿到锁的xid
Integer s = xidStamp.get(xid);
if(s != null && s > 0) {
continue;
}
stamp ++;
System.out.println("xid"+xid+"的stamp是"+s);
System.out.println("进入深搜");
if(dfs(xid)) {
return true;
}
}
return false;
}
private boolean dfs(long xid) {
Integer stp = xidStamp.get(xid);
System.out.println("xid"+xid+"的stamp是"+stp);
//遍历某个图时,遇到了之前遍历过的节点,说明出现了环。
if(stp != null && stp == stamp) {
return true;
}
if(stp != null && stp < stamp) {
System.out.println("遇到了前一个图,未成环");
return false;
}
xidStamp.put(xid, stamp);//每个已获得资源的事务一个独特的stamp
System.out.println("xidStamp找不到该xid,加入后xidStamp变为"+xidStamp);
Long uid = waitU.get(xid);//已获得资源的事务xid正在等待的uid
System.out.println("xid"+xid+"正在等待的uid是"+uid);
if(uid == null){
System.out.println("未成环,退出深搜");
return false;//xid没有需要等待的uid,无死锁
}
Long x = u2x.get(uid);//xid需要等待的uid被哪个xid占用了
System.out.println("xid"+xid+"需要的uid被"+"xid"+x+"占用了");
System.out.println("=====再次进入深搜"+"xid"+x+"====");
assert x != null;
return dfs(x);
}
测试代码
public static void main(String[] args) throws Exception {
LockTable lock=new LockTable();
lock.add(1L,3L);
lock.add(2L,4L);
lock.add(3L,5L);
lock.add(1L,4L);
// lock.add(1L,4L);
System.out.println("++++++++++++++++");
// lock.add(3L,4L);
lock.add(2L,5L);
System.out.println("++++++++++++++++");
lock.add(3L,3L);
System.out.println(hasDeadLock());
}
分析流程
为了更好的分析检测死锁的过程。
xid已经持有哪些uid x2u={1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xid1的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xid2的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2}
xid2正在等待的uid是null
未成环,退出深搜
xid3的stamp是null
进入深搜
xid3的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2, 3=3}
xid3正在等待的uid是null
未成环,退出深搜
++++++++++++++++
xid已经持有哪些uid x2u={1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xid1的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xid2的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xid3的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2, 3=2}
xid3正在等待的uid是null
未成环,退出深搜
++++++++++++++++
xid已经持有哪些uid x2u={1=[3], 2=[4], 3=[5]}
uid正在被哪个xid占用 u2x={3=1, 4=2, 5=3}
xid1的stamp是null
进入深搜
xid1的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2}
xid1正在等待的uid是4
xid1需要的uid被xid2占用了
=====再次进入深搜xid2====
xid2的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2}
xid2正在等待的uid是5
xid2需要的uid被xid3占用了
=====再次进入深搜xid3====
xid3的stamp是null
xidStamp找不到该xid,加入后xidStamp变为{1=2, 2=2, 3=2}
xid3正在等待的uid是3
xid3需要的uid被xid1占用了
=====再次进入深搜xid1====
xid1的stamp是2
Exception in thread "main" java.lang.RuntimeException: Deadlock!
at utils.Error.<clinit>(Error.java:20)
at backend.vm.LockTable.add(LockTable.java:61)
at backend.vm.test2.main(test2.java:37)
VM其他实现方法
获取缓存
VM 的实现类还被设计为 Entry 的缓存,需要继承 前面讲的通用缓存框架
。实际上,就是在获取到dataItem的基础上再获取Entry。
protected Entry getForCache(long uid) throws Exception {
Entry entry = Entry.loadEntry(this, uid);
if(entry == null) {
throw Error.NullEntryException;
}
return entry;
}
public static Entry loadEntry(VersionManager vm, long uid) throws Exception {
DataItem di = ((VersionManagerImpl)vm).dm.read(uid);
return newEntry(vm, di, uid);
}
释放缓存也是如此,不再赘诉
从图中删除事务
在一个事务 commit 或者 abort 时,就可以释放所有它持有的锁,并将自身从等待图中删除。并从等待队列中选择一个xid来占用uid
解锁时,将该 Lock 对象 unlock 即可,这样其他业务线程就获取到了锁,就可以继续执行了。
public void remove(long xid) {
lock.lock();
try {
List<Long> l = x2u.get(xid);
if(l != null) {
while(l.size() > 0) {
Long uid = l.remove(0);
selectNewXID(uid);
}
}
waitU.remove(xid);
x2u.remove(xid);
waitLock.remove(xid);
} finally {
lock.unlock();
}
}
private void selectNewXID(long uid) {
u2x.remove(uid);//uid被某个xid持有
List<Long> l = wait.get(uid);//正在等待uid的xid的列表
if(l == null) return;
assert l.size() > 0;
while(l.size() > 0) {
long xid = l.remove(0);//从等待uid的xid的列表中选一个xid出来
if(!waitLock.containsKey(xid)) {//
continue;
} else {//若选出的xid获得了锁
u2x.put(uid, xid);
Lock lo = waitLock.remove(xid);
waitU.remove(xid);
lo.unlock();
break;
}
}
if(l.size() == 0) wait.remove(uid);
}
开启事务
begin()
开启一个事务,并初始化事务的结构,将其存放在 activeTransaction 中,用于检查和快照使用:
//begin() 每开启一个事务,并计算当前活跃的事务的结构,将其存放在 activeTransaction 中,
// 用于检查和快照使用:
@Override
public long begin(int level) {
lock.lock();
try {
long xid = tm.begin();
//activeTransaction 当前事务创建时活跃的事务,,如果level!=0,放入t的快照中
Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
activeTransaction.put(xid, t);
return xid;
} finally {
lock.unlock();
}
}
提交事务
commit()
方法提交一个事务,主要就是 free 掉相关的结构,并且释放持有的锁,并修改 TM 状态:
//commit() 方法提交一个事务,主要就是 free 掉相关的结构,并且释放持有的锁,并修改 TM 状态:
@Override
public void commit(long xid) throws Exception {
lock.lock();
Transaction t = activeTransaction.get(xid);
lock.unlock();
try {
if(t.err != null) {
throw t.err;
}
} catch(NullPointerException n) {
Panic.panic(n);
}
lock.lock();
activeTransaction.remove(xid);
lock.unlock();
lt.remove(xid);
tm.commit(xid);
}
回滚事务
abort 事务的方法则有两种,手动和自动。手动指的是调用 abort() 方法,而自动,则是在事务被检测出出现死锁时,会自动撤销回滚事务;或者出现版本跳跃时,也会自动回滚:
private void internAbort(long xid, boolean autoAborted) {
lock.lock();
Transaction t = activeTransaction.get(xid);
//手动回滚
if(!autoAborted) {
activeTransaction.remove(xid);
}
lock.unlock();
//自动回滚
if(t.autoAborted) return;
lt.remove(xid);
tm.abort(xid);
}
读取数据
read()
方法读取一个 entry,注意判断下可见性即可:
//read() 方法读取一个 entry,注意判断下可见性即可
//xid当前事务 uid要读取的事务
@Override
public byte[] read(long xid, long uid) throws Exception {
lock.lock();
//当前事务xid读取时的快照数据
Transaction t = activeTransaction.get(xid);
lock.unlock();
if(t.err != null) {
throw t.err;
}
Entry entry = null;
try {
//通过uid找要读取的事务dataItem
entry = super.get(uid);
} catch(Exception e) {
if(e == Error.NullEntryException) {
return null;
} else {
throw e;
}
}
try {
if(Visibility.isVisible(tm, t, entry)) {
return entry.data();
} else {
return null;
}
} finally {
entry.release();
}
}
插入数据
//insert() 则是将数据包裹成 Entry,无脑交给 DM 插入即可:
@Override
public long insert(long xid, byte[] data) throws Exception {
lock.lock();
//xid插入时还活跃的快照
Transaction t = activeTransaction.get(xid);
lock.unlock();
if(t.err != null) {
throw t.err;
}
byte[] raw = Entry.wrapEntryRaw(xid, data);
return dm.insert(xid, raw);
}
删除版本
delete()
方法看起来略为复杂:
public boolean delete(long xid, long uid) throws Exception {
lock.lock();
Transaction t = activeTransaction.get(xid);
lock.unlock();
if(t.err != null) {
throw t.err;
}
Entry entry = null;
try {
entry = super.get(uid);
} catch(Exception e) {
if(e == Error.NullEntryException) {
return false;
} else {
throw e;
}
}
try {
if(!Visibility.isVisible(tm, t, entry)) {
return false;
}
Lock l = null;
try {
l = lt.add(xid, uid);
} catch(Exception e) {
//检测到死锁
t.err = Error.ConcurrentUpdateException;
internAbort(xid, true);
t.autoAborted = true;
throw t.err;
}
//没有死锁,但是需要等待当前事务拿到锁
if(l != null) {
l.lock();
l.unlock();
}
if(entry.getXmax() == xid) {
return false;
}
//System.out.println(entry.getXmax());
//检查到版本跳跃,回滚事务
if(Visibility.isVersionSkip(tm, t, entry)) {
System.out.println("检查到版本跳跃,自动回滚");
t.err = Error.ConcurrentUpdateException;
internAbort(xid, true);
t.autoAborted = true;
throw t.err;
}
//删除事务,把当前xid设置为Xmax
entry.setXmax(xid);
return true;
} finally {
entry.release();
}
}
实际上主要是前置的三件事:一是可见性判断,二是获取资源的锁,三是版本跳跃判断。删除的操作只有一个设置 XMAX。