栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

手写Mysql(七)VM模块篇-死锁检测与VM的实现

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

手写Mysql(七)VM模块篇-死锁检测与VM的实现

目录

版本跳跃问题

测试代码

死锁

死锁的造成

死锁检测

测试代码

分析流程

VM其他实现方法

获取缓存

从图中删除事务

删除版本


本节将收尾 VM 层,介绍一下 MVCC 可能导致的版本跳跃问题,以及mysql 如何检测 2PL 导致的死锁,还介绍了一些上一章节没有介绍到的方法。

版本跳跃问题

版本跳跃问题,考虑如下的情况,假设 X 最初只有 x0 版本,T1 和 T2 都是可重复读的隔离级别:

T1 begin
T2 begin
R1(X) // T1读取x0
R2(X) // T2读取x0
U1(X) // T1将X更新到x1
T1 commit
U2(X) // T2将X更新到x2
T2 commit

这种情况实际运行起来是没问题的,但是逻辑上不太正确。T1 将 X 从 x0 更新为了 x1,这是没错的。但是 T2 则是将 X 从 x0 更新成了 x2,跳过了 x1 版本。

读提交是允许版本跳跃的,而可重复读则是不允许版本跳跃的。解决版本跳跃的思路也很简单:如果 Ti 需要修改 X,而 X 已经被 Ti 不可见的事务 Tj 修改了,那么要求 Ti 回滚。

if(Visibility.isVersionSkip(tm, t, entry)) {
      System.out.println("检查到版本跳跃,自动回滚");
      t.err = Error.ConcurrentUpdateException;
      internAbort(xid, true);
      t.autoAborted = true;
      throw t.err;
}

上一节中就总结了,Ti 不可见的 Tj,有两种情况:

  1. XID(Tj) > XID(Ti)
  2. Tj in SP(Ti)

于是版本跳跃的检查也就很简单了,取出要修改的数据 X 的最新提交版本,并检查该最新版本的创建者对当前事务是否可见:

public static boolean isVersionSkip(TransactionManager tm, Transaction t, Entry e) {
    long xmax = e.getXmax();
    if(t.level == 0) {
        return false;
    } else {
        return tm.isCommitted(xmax) && (xmax > t.xid || t.isInSnapshot(xmax));
  }
}

测试代码
public class skip {
    public static void main(String[] args) throws Exception {
        TransactionManagerImpl tm= TransactionManager.open("cun/tm");
        DataManager dm=DataManager.open("cun/dm",1 << 20,tm);
        VersionManager vm=VersionManager.newVersionManager(tm,dm);
        byte b[]=new byte[1122];
        byte b_update[]=new byte[1122];

        long uid1=vm.insert(0,b);
        long xid1=vm.begin(1);
        System.out.println("开启事务"+xid1);
        long xid2=vm.begin(1);
        System.out.println("开启事务"+xid2);
        //xid1将x更新到x1
        vm.delete(xid1,uid1);
        long uid2=vm.insert(xid1,b_update);
        vm.commit(xid1);
        System.out.println("======");
        //xid1将x更新到x2
        vm.delete(xid2,uid1);
        long uid3=vm.insert(xid2,b_update);
        vm.commit(xid2);
    }
}

死锁

死锁的造成

在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> x2u;  // 某个XID已经获得的资源的UID列表
    private Map u2x;        // UID被某个XID持有
    private Map> wait; // 正在等待UID的XID列表
    private Map waitLock;   // 正在等待资源的XID的锁
    private Map 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
                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 正在等待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.(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 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 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);
    }

删除版本

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。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/978823.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号