Mysql存储引擎Innodb在处理并发读或者写的时候,通过两种类型的锁来解决并发问题,这两种锁通常称为共享锁和排他锁,也叫读锁和写锁。
读锁是共享的,即多个客户端可以同时读取同一资源。
写锁是排他的,也就是说写锁会阻塞其他的写锁和读锁。
举个例子:
客户端A读取操作不需要等待客户端B读取完成并释放锁。但客户端A进行写操作的时候,会阻塞其他客户端的读和写操作,直到客户端A写操作完成并释放锁,其他客户端才可以进行读写。
上面说了这么多,但实际上当客户端A更新某行记录的同时,客户端B任然可读取到数据,不会被阻塞,这是为什么呢?
先来了解两个东西,Redo Log(重做日志)和Undo Log(回滚日志)
redo log通常是物理日志,记录的是数据页的物理修改,它用来恢复提交后的物理数据页(恢复数据页,且只能恢复到最后一次提交的位置)。
undo用来回滚行记录到某个版本。undo log一般是逻辑日志,根据每行记录进行记录。
如果看不懂,就这样理解:当发生写操作时,Innodb会把旧数据存储到Undo Log(回滚日志)中,新数据写到Redo Log(重做日志)中。
当客户端A更新某行记录的同时,客户端B会被写锁阻塞,这时,客户端B会去Undo Log中读取旧数据。
下面来实践下。
CREATE TABLE `test001` (
`id` bigint(11) unsigned NOT NULL AUTO_INCREMENT,
`first_name` char(50) DEFAULT NULL,
`last_name` char(50) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;
INSERT INTO `test001`(`id`, `first_name`, `last_name`) VALUES (1, 'fn001', 'ln001');
INSERT INTO `test001`(`id`, `first_name`, `last_name`) VALUES (2, 'fn002', 'ln002');
INSERT INTO `test001`(`id`, `first_name`, `last_name`) VALUES (3, 'fn003', 'ln003');
在一个终端执行
BEGIN;
UPDATE test001 SET first_name='update_name' WHERE last_name='ln001';
另一个终端执行
SELECT * FROM `test001` WHERE last_name='ln001';
查询依旧可以获取数据
ps:试验完成后,记得commit或者rollback结束事物。
Innodb使用行级锁,然而这并不意味着Innodb只会锁住被修改的行。请看下面的操作。
在一个终端连接中执行
BEGIN;
UPDATE test001 SET first_name='update_name' WHERE last_name='ln001';
在另一个终端连接中执行
BEGIN;
UPDATE test001 SET first_name='update_name2222' WHERE last_name='ln002';
第二条SQL被阻塞超时,即更新操作不仅仅锁住了 last_name='ln001'的行,还锁住了其他行。在这个例子中,实际上锁住了整张表。
回滚上面的事物再进行下面的操作。运行 rollback
那为什么说Innodb实现了行级锁呢?我们来给last_name行加一个索引。
再次执行刚才的语句
可以看到两条更新语句互不干扰,由此可知Innodb通过索引,筛选出了last_name='ln001'的数据行,然后仅对last_name='ln001'的数据行加了锁,其他数据行不会被锁住,所以此时可以更新last_name='ln002'的数据行。
没加索引前,Innodb就没法通过索引筛选出指定的数据行,只能锁住整张表了。