手写数据库轮子项目 MYDB 之七 | VersionManager (VM) 版本之死锁检测与 VM 的实现

一、死锁检测

两阶段锁协议(2PL)通常包括扩张和收缩两个阶段。在扩张阶段,事务将获取锁,但不能释放任何锁。在收缩阶段,可以释放现有的锁,但不能获取新的锁,这种规定存在着死锁的风险。

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 {
            if(isInList(x2u, xid, uid)) { /// 如果 xid 已经获取了资源 uid
                return null;
            }
            if(!u2x.containsKey(uid)) { /// 如果 uid 没有被任何一个 xid 持有
                u2x.put(uid, xid);  /// 更新:uid 正被 xid 持有
                putIntoList(x2u, xid, uid); /// 更新:xid 正持有 uid
                return null;
            }

            /// 如果上面两个判断都为假,则说明是 xid 需要 uid 的释放
            waitU.put(xid, uid); /// 更新: xid 正在等待获取 uid
            putIntoList(wait, uid, xid);  /// 更新: uid 正在被 xid 等待获取
            if(hasDeadLock()) { /// 如果发生死锁
                waitU.remove(xid); ///从等待列表中删除
                removeFromList(wait, uid, xid);
                throw Error.DeadlockException;
            }
            /// 如果没有造成死锁
            Lock l = new ReentrantLock();
            l.lock();
            waitLock.put(xid, l); /// 正在等待资源的 xid 的锁
            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;
        for(long xid : x2u.keySet()) { /// 遍历所有的xid
            Integer s = xidStamp.get(xid);
            if(s != null && s > 0) {
                continue;
            }
            /// 如果一个 xid 尚未被访问,则从这个 xid 开始进行深度优先搜索
            stamp ++;
            if(dfs(xid)) {
                return true;
            }
            /// 如果从这个 xid 开始进行深度优先搜索,没有出现环,则从下一个未被访问到的 xid 继续搜索
        }
        return false;
    }

    private boolean dfs(long xid) {
        Integer stp = xidStamp.get(xid);
        /// 遇到了之前遍历过的节点,出现了环
        if(stp != null && stp == stamp) {
            return true;
        }
        /// 遇到了之前遍历过的节点,但这个节点在别的图中,未成环
        if(stp != null && stp < stamp) {
            return false;
        }
        /// 如果 xid 尚未被遍历过,则用 stamp 标记
        xidStamp.put(xid, stamp);
        /// *** xid 正在等待的 uid ***
        Long uid = waitU.get(xid);
        /// xid 没有正在等待的 uid,未成环,退出深搜
        if(uid == null) return false;
        /// *** 持有 uid 的 xid ***
        Long x = u2x.get(uid);
        /// 如果 uid 没有被任何 xid 持有,即如果发现 x 为空,则引发异常,终止程序
        assert x != null;
        return dfs(x);
    }

二、VM 的实现

1. 获取缓存

VM 提供了 Entry 的缓存,继承了 AbstractCache<Entry>。

@Override
protected Entry getForCache(long uid) throws Exception {
    Entry entry = Entry.loadEntry(this, uid);
    if(entry == null) {
        throw Error.NullEntryException;
    }
    return entry;
}

@Override
protected void releaseForCache(Entry entry) {
    entry.remove();
}

2. 事务

begin() 开启一个事务,并初始化事务的结构,将其存放在 activeTransaction 中,用于检查和快照使用: 

@Override
public long begin(int level) {
    lock.lock();
    try {
        long xid = tm.begin();
        Transaction t = Transaction.newTransaction(xid, level, activeTransaction);
        activeTransaction.put(xid, t);
        return xid;
    } finally {
        lock.unlock();
    }
}

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) {
            System.out.println(xid);
            System.out.println(activeTransaction.keySet());
            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);
    }

3. 数据

read() 方法读取一个 entry,注意判断下可见性即可:

    @Override
    public byte[] read(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 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();
    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() 方法看起来略为复杂,实际上主要是前置的三件事:一是可见性判断,二是获取资源的锁,三是版本跳跃判断。删除的操作只有一个设置 XMAX。

@Override
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 = super.get(uid);
    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;
        }
        if(Visibility.isVersionSkip(tm, t, entry)) {
            t.err = Error.ConcurrentUpdateException;
            internAbort(xid, true);
            t.autoAborted = true;
            throw t.err;
        }
        entry.setXmax(xid);
        return true;
    } finally {
        entry.release();
    }
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值