innodb锁机制探究(一)
//
关于innodb里面的锁
在之前的文章中,我们说过三次关于锁的文章,分别是去年的12月25号、12月26号以及今年的1月14号的文章,这3篇文章里面简单介绍过一些锁的知识点,后续我们将详细介绍MySQL里面的锁,但是不能保证每天更新,所以把题目做成一个系列标签,如果有时间我做下归类整理。
innodb中的锁当中有两个概念需要搞清楚,一个是latch,一个是lock,我们一般说的锁就是lock。这两个概念的区别是什么呢?
latch一般是轻量级的锁,它的锁定时间特别短,否则应用的性能特别差,在innodb存储引擎中,latch可以分为mutex互斥量和rwlock读写锁,它的目的是保证并发线程操作临界资源的正确性。并且通常没有死锁检测的机制。关于它的状态,可以使用show engine innodb mutex来查看。
lock的对象是事务,它通常用来锁定页、行、表等等,并且lock住的对象一般在commit事务或者rollback事务之后提交。lock是有死锁机制的。
关于latch,我们这里不再多说,后面有时间单独拿出一篇文章来讲述。
锁问题实战
我们简单演示一下锁的生成吧,如下代码所示,我们首先创建一个表,然后插入数据,接着使用select * for update给这个表加上排他锁,也就是X锁,如下:
SESSION 1
mysql:yeyztest 12:16:19>>create table test (id int primary key auto_increment,age int);
Query OK, 0 rows affected (0.02 sec)
mysql:yeyztest 12:20:49>>select * from test;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
| 2 | 2 |
| 5 | 5 |
| 8 | 8 |
+----+------+
4 rows in set (0.00 sec)
mysql:yeyztest 13:19:55>>begin;
Query OK, 0 rows affected (0.00 sec)
mysql:yeyztest 13:21:37>>select * from test where id=1 for update;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
+----+------+
1 row in set (0.00 sec)
然后我们在另外一个回话中使用下面的命令再次请求这行记录的锁:
SESSION 2:
mysql:yeyztest 13:22:48>>select * from test where id =1 lock in share mode;
可以看到,返回的结果迟迟没有出现,没有得到对应的记录,此时我们查看数据字典information_schema中关于锁和事务的表,结果如下:
mysql:yeyztest 13:23:47>>select * from information_schema.innodb_locks\G
*************************** 1. row ***************************
lock_id: 422062388676960:164:3:2
lock_trx_id: 422062388676960
lock_mode: S
lock_type: RECORD
lock_table: `yeyztest`.`test`
lock_index: PRIMARY
lock_space: 164
lock_page: 3
lock_rec: 2
lock_data: 1
*************************** 2. row ***************************
lock_id: 1288486:164:3:2
lock_trx_id: 1288486
lock_mode: X
lock_type: RECORD
lock_table: `yeyztest`.`test`
lock_index: PRIMARY
lock_space: 164
lock_page: 3
lock_rec: 2
lock_data: 1
2 rows in set, 1 warning (0.00 sec)
可以看到,有两个锁的信息,我们解释下各个字段的意思:
lock_id:锁的id
lock_trx_id:事务id
lock_mode:锁的模式,S还是X
lock_type:锁的类型,表锁还是行锁
lock_table:要加锁的表
lock_index:锁住的索引
lock_space:锁对象的space id
lock_page:锁住的页数量,若是表锁,这个值是null
lock_rec:锁住的行的数量,若是表锁,这个值是null
lock_data:事务锁定记录的主键值,若是表锁,这个值是null
需要注意的是,这个lock_data的值不是可信的,如果我们锁定了一个范围内的记录,这个lock_data的值会变成这个范围内的第一条记录的id值。
再来看下lock_wait表里面的内容:
mysql:yeyztest 13:24:56>>select * from information_schema.innodb_lock_waits\G
*************************** 1. row ***************************
requesting_trx_id: 422062388676960
requested_lock_id: 422062388676960:164:3:2
blocking_trx_id: 1288486
blocking_lock_id: 1288486:164:3:2
1 row in set, 1 warning (0.00 sec)
可以看到,这里写的是当前请求锁的事务id和锁id,以及阻塞的事务id和锁id。
一段时间过后,session 2上面的锁没有拿到,发生了锁超时的错误,如下:
SESSION 2
mysql:yeyztest 13:24:39>>select * from test where id =1 lock in share mode;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
再回过头来看session 1中的所等待信息,可以看到,所等待的信息已经不存在了:
mysql--dba_admin@127.0.0.:yeyztest 13:24:56>>select * from information_schema.innodb_lock_waits\G
*************************** 1. row ***************************
requesting_trx_id: 422062388676960
requested_lock_id: 422062388676960:164:3:2
blocking_trx_id: 1288486
blocking_lock_id: 1288486:164:3:2
1 row in set, 1 warning (0.00 sec)
mysql:yeyztest 13:25:13>>select * from information_schema.innodb_lock_waits\G
Empty set, 1 warning (0.00 sec)
一致性非锁定读
一致性非锁定读是指innodb通过行记录的多版本控制来读取当前执行时间数据库中行的数据,如果读取的行正在执行delete或者update操作,则会读取一个该行记录的一个快照数据。
它的实现实际上是通过undo日志来实现的,undo日志使用在事务回滚时候的一些日志,关于啥是个undo日志的具体细节,如果大家不了解,后续会单独拿一篇文章去写,现在只需要知道有这么个东西即可。
通过上面的描述,我们可以发现,一致性非锁定读提高了数据库的并发性,因为读操作不用等待锁的释放。然而这并不能解决所有的问题,因为读取到的快照数据是当前行记录之前的历史版本,每行记录可能有多个历史版本,也就是说你现在看到这个id是4,可能之前是3,再往前是2,我们到底使用哪个版本的数据给客户看,这就牵扯到多版本控制方法,也就是常说的MVCC。
一致性锁定读
上面说到了 一致性非锁定,下面我们看看一致性锁定读,非锁定读我们知道读取的是数据的快照,可能不是数据当前的真是内容,而锁定读读取的就是当前数据的真是内容。它需要在读取的时候给记录进行上锁。看到这里,可能你已经熟悉了,这不就是我们使用的:
select * for update(X锁定)
select * lock in share mode(S锁定)
这两种方法终于有名字了,就叫做一致性锁定读。这里需要注意的是,这两种方法必须存在于一个事务当中,才能获得锁,否则是不行的,我专门做了测试:
SESSION 1
mysql:yeyztest 13:19:29>>select * from test where id =1 for update;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
+----+------+
1 row in set (20.11 sec)
SESSION 2
mysql:yeyztest 13:21:37>>select * from test where id=1 for update;
+----+------+
| id | age |
+----+------+
| 1 | 1 |
+----+------+
1 row in set (0.00 sec)
可以看到,第一个会话不加begin等关键字使用上述语句的时候,第二个会话中还是可以读到这行记录的值,而不收到阻塞。
所以,一致性锁定读,必须写在事务里面才管用,否则这样的语句不会获得这一行记录的锁。