搞懂MySQL InnoDB事务ACID实现原理

前言说到数据库事务,想到的就是要么都做修改,要么都不做。或者是ACID的概念。其实事务的本质就是锁和并发和重做日志的结合体。那么,这一篇主要讲一下InnoDB中的事务到底是如何实现ACID的。原子性(atomicity)一致性(consistency)隔离性(isolation)持久性(durability)一.隔离性其实隔离性的实现原理就是锁,因而隔离性也可以称为并发控制、锁等。事务的隔离性要求...

搞懂MySQL InnoDB事务ACID实现原理

前言

  说到数据库事务,想到的就是要么都做修改,要么都不做。或者是ACID的概念。其实事务的本质就是锁和并发和重做日志的结合体。那么,这一篇主要讲一下InnoDB中的事务到底是如何实现ACID的。

  • 原子性(atomicity)
  • 一致性(consistency)
  • 隔离性(isolation)
  • 持久性(durability)

一.隔离性

  其实隔离性的实现原理就是,因而隔离性也可以称为并发控制、锁等。事务的隔离性要求每个读写事务的对象对其他事务的操作对象能互相分离。再者,比如操作缓冲池中的LRU列表,删除,添加、移动LRU列表中的元素,为了保证一致性那么就要锁的介入。InnoDB使用锁为了支持对共享资源进行并发访问,提供数据的完整性和一致性。那么到底InnoDB支持什么样的锁呢?我们先来看下InnoDB的锁的介绍:

InnoDB中的锁

  你可能听过各种各样的InnoDB的数据库锁,gap锁啊,共享锁,排它锁,读锁,写锁等等。但是InnoDB的标准实现的锁只有2类,一种是行级锁,一种是意向锁。

  InnoDB实现了如下两种标准的行级锁

  • 共享锁(读锁 S Lock),允许事务读一行数据
  • 排它锁(写锁 X Lock),允许事务删除一行数据或者更新一行数据

  行级锁中,除了S和S兼容,其他都不兼容。

  InnoDB支持两种意向锁(即为表级别的锁):

  • 意向共享锁(读锁 IS Lock),事务想要获取一张表的几行数据的共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
  • 意向排他锁(写锁 IX Lock),事务想要获取一张表中几行数据的排它锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁。

  首先解释一下意向锁,以下为意向锁的意图解释:

The main purpose of IX and IS locks is to show that someone is locking a row, or going to lock a row in the table.

  大致意思是加意向锁为了表明某个事务正在锁定一行或者将要锁定一行数据。

  首先申请意向锁的动作是InnoDB完成的,怎么理解意向锁呢?例如:事务A要对一行记录r进行上X锁,那么InnoDB会先申请表的IX锁,再锁定记录r的X锁。在事务A完成之前,事务B想要来个全表操作,此时直接在表级别的IX就告诉事务B需要等待而不需要在表上判断每一行是否有锁。意向排它锁存在的价值在于节约InnoDB对于锁的定位和处理性能。另外注意了,除了全表扫描以外意向锁都不会阻塞。

锁的算法

  InnoDB有3种行锁的算法:

  • Record Lock:单个行记录上的锁
  • Gap Lock:间隙锁,锁定一个范围,而非记录本身
  • Next-Key Lock:结合Gap Lock和Record Lock,锁定一个范围,并且锁定记录本身。主要解决的问题是RR隔离级别下的幻读

  这里主要讲一下Next-Key Lock。mysql默认隔离级别RR下,这时默认采用Next-Key locks。这种间隙锁的目的就是为了阻止多个事务将记录插入到同一范围内从而导致幻读。注意了,如果走唯一索引,那么Next-Key Lock会降级为Record Lock。前置条件为事务隔离级别为RR且sql走的非唯一索引、主键索引。前置条件为事务隔离级别为RR且sql走的非唯一索引、主键索引。前置条件为事务隔离级别为RR且sql走的非唯一索引、主键索引。重要的事情说三遍。如果不是则根本不会有gap锁!先举个例子来讲一下Next-Key Lock。

  首先建立一张表:

mysql> show create table m_test_db.M; ------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | Table | Create Table | ------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | M  | CREATE TABLE `M` (  `id` int(11) NOT NULL AUTO_INCREMENT,  `user_id` varchar(45) DEFAULT NULL,  `name` varchar(45) DEFAULT NULL,  PRIMARY KEY (`id`),  KEY `IDX_USER_ID` (`user_id`)) ENGINE=InnoDB AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 | ------- -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- 1 row in set (0.00 sec)

  首先session A去拿到user_id为26的X锁,用force index,强制走这个非唯一辅助索引,因为这张表里的数据很少。

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> select * from m_test_db.M force index(IDX_USER_ID) where user_id = '26' for update; ---- --------- ------- | id | user_id | name  | ---- --------- ------- |  5 | 26| jerry ||  6 | 26| ketty | ---- --------- ------- 2 rows in set (0.00 sec)

  然后session B插入数据

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into m_test_db.M values (8,25,'GrimMjx');ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

  明明插入的数据和锁住的数据没有毛线关系,为什么还会阻塞等锁最后超时呢?这就是Next-Key Lock实现的。画张图你就明白了。

  gap锁锁住的位置,不是记录本身,而是两条记录之间的间隔gap,其实就是防止幻读(同一事务下,连续执行两句同样的sql得到不同的结果),为了保证图上3个小箭头中间不会插入满足条件的新记录,所以用到了gap锁防止幻读。

  简单的insert会在insert的行对应的索引记录上加一个Record Lock锁,并没有gap锁,所以并不会阻塞其他session在gap间隙里插入记录。不过在insert操作之前,还会加一种锁,官方文档称它为insertion intention gap lock,也就是意向的gap锁。这个意向gap锁的作用就是预示着当多事务并发插入相同的gap空隙时,只要插入的记录不是gap间隙中的相同位置,则无需等待其他session就可完成,这样就使得insert操作无须加真正的gap lock。

  Session A插入数据

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into m_test_db.M values (10,25,'GrimMjx');Query OK, 1 row affected (0.00 sec)

  Session B插入数据,完全没有问题,没有阻塞。

mysql> begin;Query OK, 0 rows affected (0.00 sec)mysql> insert into m_test_db.M values (11,27,'Mjx');Query OK, 1 row affected (0.00 sec)

死锁

  了解了InnoDB是如何加锁的,现在可以去尝试分析死锁。死锁的本质就是两个事务相互等待对方释放持有的锁导致的,关键在于不同Session加锁的顺序不一致。不懂死锁概念模型的可以先看一幅图:

  左鸟线程获取了左肉的锁,想要获取右肉的锁,右鸟的线程获取了右肉的锁。右鸟想要获取左肉的锁。左鸟没有释放左肉的锁,右鸟也没有释放右肉的锁,那么这就是死锁。

  接下来还用刚才的那张M表来分析一下数据库死锁,比较好理解:

http://www.guoxiongfei.cn/cntech/13065.html