MySQL学习笔记-锁相关话题

时间:2024-01-14 19:01:08

在事务相关话题中,已经提到事务隔离性依靠锁机制实现的。在本篇中围绕着InnoDB与MyISAM锁机制的不同展开,进而描述锁的实现方式,多种锁的概念,以及死锁产生的原因。

Mysql常用存储引擎的锁机制

MyISAM和MEMORY采用表级锁(table-level locking);

BDB采用页面锁(page-leve locking)或表级锁,默认为页面锁;

InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁;

各种锁特点


表级锁(table-level locking):开销小,加锁快;不会出现死锁;锁定粒度大,发生冲突的概率最高,并发度最低

行级锁(row-level locking):开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高

页面锁(page-level locking):开销和加锁时间介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般

 

锁的算法


InnoDB存储引擎有3种行所算法设计,分别是:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,但不包含记录本身
  • Next-key Lock:Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

Record Lock总是会去锁住索引记录,如果InnoDB存储引擎表建立的时候没有设置任何一个索引,这时InnodB存储引擎会使用隐式的主键来进行锁定,在Repeatable Read隔离级别下,Next-key Lock 算法是默认的行记录锁定算法。

阻塞、系统互斥量(mutex)和信号量(event)


因为不同锁之间兼容性关系,一个事务中的锁需要等待另一个事务中的锁释放它所占用的资源。在InnoDB存储引擎的源代码中,用Mutex数据结构来实现锁。在访问资源前需要用mutex_enter函数进行申请,在资源访问或修改完毕后立即执行mutex_exit函数。当一个资源已被一个事务占有时,另一个事务执行mutex_enter函数会发生等待,这就是阻塞。阻塞并不是一件坏事,阻塞是为了保证事务可以并发并且正常运行。

在InnoDB引擎当中,封装了操作系统提供的基本mutex(互斥量)和event(信号量),在WINDOWS下的实现暂时不做记录,主要还是对支持POSIX系统来做介绍。在POSIX系统的实现是os_fast_mutex_t和os_event_t。os_fast_mutex_t相对简单,其实就是pthread_mutex。定义如下:

  1. typedef pthread_mutex os_fast_mutex_t;
而os_event_t相对复杂,它是通过os_fast_mutex_t和一个pthread_cond_t来实现的,定义如下:
  1. typedef struct os_event_struct
  2. {
  3. os_fast_mutex_t os_mutex;
  4. ibool is_set;
  5. pthread_cond_t cond_var;
  6. }os_event_t;

以下是os_event_t的两线程信号控制的例子流程:

对于系统的封装,最主要的就是os_event_t接口的封装,而在os_event_t的封装中,os_event_set、os_event_reset、os_event_wait这三 个方法是最关键的。

CAS原子操作


在InnoDB的mutex(互斥量)的实现中,除了引用系统的os_mutex_t以外,还使用了原子操作来进行封装一个高效的mutex实现。在系统支持原子操作的情况下,会采用自己封装的mutex来做互斥,如果不支持,就使用os_mutex_t。
因为我对于操作系统编程不熟,在此用Java的util.concurrent.atomic包举例,其封装了一些具有原子性操作的类,大量的利用了类似于mutex(互斥量)的操作。
CAS原子操作——Compare & Set,自旋锁是采用让当前线程不停地的在循环体内执行实现的,当循环的条件被其他线程改变时才能进入临界区。
  1. /**
  2. * Atomically updates the current value with the results of
  3. * applying the given function, returning the previous value. The
  4. * function should be side-effect-free, since it may be re-applied
  5. * when attempted updates fail due to contention among threads.
  6. *
  7. * @param updateFunction a side-effect-free function
  8. * @return the previous value
  9. * @since 1.8
  10. */
  11. public final int getAndUpdate(IntUnaryOperator updateFunction) {
  12. int prev, next;
  13. do {
  14. prev = get();
  15. next = updateFunction.applyAsInt(prev);
  16. } while (!compareAndSet(prev, next));
  17. return prev;
  18. }
  19. /**
  20. * Atomically sets the value to the given updated value
  21. * if the current value {@code ==} the expected value.
  22. *
  23. * @param expect the expected value
  24. * @param update the new value
  25. * @return {@code true} if successful. False return indicates that
  26. * the actual value was not equal to the expected value.
  27. */
  28. public final boolean compareAndSet(int expect, int update) {
  29. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  30. }

InnoDB存储引擎中的锁


InnoDB是一个多线程并发的引擎,内部的读写都是用多线程来实现的,所以InnoDB内部实现了一个比较高级的并发同步机制。InnoDB并没有直接使用系统提供的锁(latch)同步结构,而是对其进行自己的封装和实现优化,但是也兼容系统的锁。我们先看一段InnoDB内部的注释(MySQL-3.23):

Semaphore operations in operating systems are slow: Solaris on a 1993 Sparc takes 3 microseconds (us) for a lock-unlock pair and Windows NT on a 1995 Pentium takes 20 microseconds for a lock-unlock pair. Therefore, we have toimplement our own efficient spin lock mutex. Future operating systems mayprovide efficient spin locks, but we cannot count on that.

大概意思是说1995年的时候,一个Windows NT的 lock-unlock所需要耗费20us,即使是在Solaris 下也需要3us,这也就是他为什么要实现自定义latch的目的,在innodb中作者实现了系统latch的封装、自定义mutex和自定义 rw_lock。
InnoDB存储引擎实现了以下两种标准的行机锁:
共享锁(S Lock):允许事务读一行数据。
排他锁(X Lock):允许事务删除或者更新一行数据。
未完待续...
参考文献:

《MySQL技术内幕InnoDB存储引擎》