死磕数据库:锁的实现

上一篇关于事务与锁的文章最后其实留下了一个悬而未决的问题。像SELECT ... LOCK IN SHARE MODE;这种可能会扫表的操作,明确会添加的是IS锁,剩下会加哪些锁其实和具体的WHERE子句有关。那么到底有几个锁实例的存在怎么确定呢?

mysql> SELECT * FROM account;
+----+------------+---------+
| id | account_id | balance |
+----+------------+---------+
|  1 |         10 |    3000 |
|  2 |         20 |    3000 |
|  3 |         30 |    3000 |
+----+------------+---------+
3 rows in set (0.01 sec)

mysql> SELECT * FROM account WHERE account_id < 10 LOCK IN SHARE MODE;
Empty set (0.04 sec)

mysql> SHOW ENGINE INNODB STATUS \G;
---TRANSACTION 476C0DC, ACTIVE 29 sec
3 lock struct(s), heap size 376, 2 row lock(s)
MySQL thread id 43477964, OS thread handle 0x7f162ccf6700, query id 485892583 127.0.0.1 root

这篇博文WHERE子句限定的是PRIMARYUNIQUE和普通字段的情况进行了分析。可以看出我们的例子中的3种锁结构:其中一个是IS锁对应的结构,另外两个是记录锁和区间锁吗?而记录锁有两个(实例),这是为什么呢?按照前文的分析,Next-Key锁和插入意向锁可以实现为同一个结构、X锁和S锁可以实现为同一个结构。那么实际究竟是不是这样的,本文就来一探究竟。

锁的互斥关系

其实,这一节的标题,我思索良久。因为通常意义下,我们讲互斥锁,会自然联系到传统操作系统下的mutex,也就是独占锁。但我想表达的其实是另一层关系,介于读锁和写锁之间的那层屏障。

很明显,如果所有锁都是独占式的实现方式,问题讨论会变得简单。从引入了读锁(共享锁)、以及读锁和写锁(独占锁)的互斥关系,把锁的讨论变复杂了。试想,如果记录只支持独占锁,不管你要读还是要写,都需要获取一个独占锁,此时整个系统退化为顺序一致性模型,在SQL里也被称作可串行化(Serializable),这也SQL标准定义的最严格的隔离级别。可串行化的原则就一个,每份数据同一时刻只能有一个事务可以访问。这显然是十分安全的,这个安全是以牺牲性能为代价的;同时如果我们要分析互斥关系也变的简单,甚至说没有锁的互斥关系,原因很简单,只有一把锁

  • 每行数据只有一把记录锁,它是独占锁,所有读、写该记录的操作需要先获取这把锁。
  • 每个区间只有一把区间锁,它是独占锁,所有读、写该区间的操作需要先获取这把锁。

那么在我们最常用的隔离级别可重复读(Repeatable Read) 下,情况是怎么变复杂的?因为每个共享单元(区间、行)有了多把锁

  • 每个区间上,我们需要插入意向锁和Next-Key锁的联动,它们之间是互斥的。这是为了提高性能,同时防止幻读(phantom read)
  • 每个记录上,我们需要X锁和S锁的联动,它们之间也是互斥的。这还是为了提高性能,同时防止脏读(dirty read)

既然同一个记录上有多把锁,就很自然地涉及到锁之间的互斥关系:两把锁是否可以同时被征用,即是否兼容。比如同一个记录上的读锁和写锁就不兼容(Conflict),而不同记录上的读锁和写锁就相互兼容(Compitable)。这么讲不是很直观,我们看一下死锁的例子:
互斥与死锁
可串行化(Serializable) 下,因为所有的锁都是独占锁,所以两个事务只要存在ABBA的锁关系,就构成潜在的死锁;而 可重复读(Repeatable Read) 下,相比而言则更加泛化,在ABCD中,如果明确A和D互斥、B和C互斥,就构成了死锁的潜在条件——我们可以另,A为记录1的写锁,B为记录2的读锁,C为记录2的写锁,D为记录2的读锁,死锁条件构成了;但这个假设下,BDDB这种翻转结构却不构成死锁了,因为都是共享锁,B和D并不互斥。可以看到,锁之间的互斥关系引入,为性能带来提升的同时,也把模型变得更复杂,“死锁”一不小心就会发生。如果明确到锁的互斥关系上,可重复读(Repeatable Read) 下有以下互斥关系:

Lock v.s. Lock插入意向锁Next-Key锁行X锁行S锁
插入意向锁兼容互斥--
Next-Key锁互斥兼容--
行X锁-- 互 斥 \color{red}{互斥} 互斥
行S锁--互斥兼容

其中,“-”表示没有必然联系,因为锁定的目标根本不是一种东西。其中,标 红 \color{red}{红} 的互斥是可串行化(Serializable) 下也要面对的。

锁的实现

之所以要探讨锁的互斥关系,其实是为了探讨锁实现的一种可能性。我们知道,同一条记录的读锁和写锁是互斥的,所以如果用一个实例来实现,是最直观的。我们参考Java里的ReadWriteLock给出一种实现:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
* Console output:
* read get lock time 1567930998.354000.
* write get lock time 1567931008.391000.
*/
public class LockTest {
    public static void main(String[] args) throws Exception {
        final ReadWriteLock lock = new ReentrantReadWriteLock();
        Thread readThd = new Thread(new LockTask(lock.writeLock()), "read");
        Thread writeThd = new Thread(new LockTask(lock.readLock()), "write");
		// 由于没有做同步,每次执行的结果可能不同
		// 但readThd和writeThd需要获取的是一对互斥锁,所以只能有一个可以获取到临界区的进入权限
        readThd.start();
        writeThd.start();
    }

    public static class LockTask implements Runnable {
        private Lock lock;

        public LockTask(Lock lock) {
            this.lock = lock;
        }

        public void run() {
            lock.lock();
            // 临界区开始
            System.out.println(String.format("%s get lock time %f.", Thread.currentThread().getName(), System.currentTimeMillis() / 1000.0));
            try {
                Thread.currentThread().sleep(10000);
            } catch (Exception e) {

            }
            // 临界区结束
            lock.unlock();
        }
    }
}

如果把线程看作事务、把ReentrantReadWriteLock看成某条记录上的X锁和S锁的实现,那上面这个过程就是在读已提交(Read Committed) 及以上隔离级别下,两个事务对同一行进行并发读写产生互斥等待的典型过程。

对于数据库索引,我知之甚少。有一个普遍的共识是,索引是通过减少磁盘I/O来达到提升存取速度的。

锁与隔离级别

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值