MySQL数据库InnoDB引擎的读写锁、事务隔离级别实例分析

数据库锁分类

按性能分

乐观锁(比如使用version字段比对,无需等待)、悲观锁(需要等待其他事务)

乐观锁,如它的名字那样,总是认为别人不会去修改,只有在提交更新的时候去检查数据的状态。通常是给数据增加一个字段来标识数据的版本。

悲观锁,正如它的名字那样,数据库总是认为别人会去修改它所要操作的数据,因此在数据库处理过程中将数据加锁。其实现依靠数据库底层。

按访问模式分

读锁(共享锁)写锁(排他锁) 均属于悲观锁

按粒度分

行级锁表级锁

案例演示数据库锁机制

对表加读锁:lock table tableName read

所有session可以读;但是当前session 更新插入报错,其他session 更新插入等待。
比如: 在数据迁移时,加读锁,防止任何session的更新操作。

对表加写锁:lock table tableName write

当前session可以查询、更新;其他session阻塞读、写。

读锁示范

创建user表
CREATE TABLE `user` (
  `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键id',
  `name` varchar(20) DEFAULT '' COMMENT '姓名',
  `age` int(11) DEFAULT '0' COMMENT '年龄',
  `salary` decimal(15,2) NOT NULL DEFAULT '0.00' COMMENT '薪水',
  `version` int(11) NOT NULL DEFAULT '0' COMMENT '版本号',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
插入数据
insert into `user` (`id`, `name`, `age`, `salary`, `version`) values('1','风清扬','60','2000.00','0');
insert into `user` (`id`, `name`, `age`, `salary`, `version`) values('2','朱元璋','80','3000.00','0');
insert into `user` (`id`, `name`, `age`, `salary`, `version`) values('3','孙猴子','50','4000.00','0');
打开数据库session1、session2

在2个会话中查询到刚才插入的数据。

mysql> select * from user;
+----+--------+------+---------+---------+
| id | name   | age  | salary  | version |
+----+--------+------+---------+---------+
|  1 | 风清扬 |   60 | 2000.00 |       0 |
|  2 | 朱元璋 |   80 | 3000.00 |       0 |
|  3 | 孙猴子 |  500 | 4000.00 |       0 |
+----+--------+------+---------+---------+
3 rows in set (0.00 sec)
第一步:在session1中对表user进行加读锁:

session1:

mysql> lock table user read;
Query OK, 0 rows affected (0.00 sec)
第二步:session2进行读、写操作:
mysql> select * from user;
+----+--------+------+---------+---------+
| id | name   | age  | salary  | version |
+----+--------+------+---------+---------+
|  1 | 风清扬 |   60 | 2000.00 |       0 |
|  2 | 朱元璋 |   80 | 3000.00 |       0 |
|  3 | 孙猴子 |  500 | 4000.00 |       0 |
+----+--------+------+---------+---------+
3 rows in set (0.00 sec)

mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('唐玄奘','40','8000.00','0');


发现session2的插入操作会一直等待,等待session1释放锁。

第三步:session中进行读没问题,在session1中进行更新操作会直接报错,提示表存在读锁不能被更新:
mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('唐玄奘','40','8000.00','0');
ERROR 1099 (HY000): Table 'user' was locked with a READ lock and can't be updated
第四步: 释放session1中的读锁
mysql> unlock  tables;
Query OK, 0 rows affected (0.00 sec)

此时会发现session2中等待的插入操作立马执行完毕:

mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('唐玄奘','40','8000.00','0');
Query OK, 1 row affected (6 min 38.15 sec)

mysql>

第五步:session1给表1加读锁,查询表2时报错
## 对表user加读锁
mysql> lock table user read;
Query OK, 0 rows affected (0.00 sec)

## 可以对表user进行查询
mysql> select * from user limit 1;
+----+--------+------+---------+---------+
| id | name   | age  | salary  | version |
+----+--------+------+---------+---------+
|  1 | 风清扬 |   60 | 2000.00 |       0 |
+----+--------+------+---------+---------+
1 row in set (0.00 sec)


## 查询表employee时报错
mysql> select * from employee limit 1;
ERROR 1100 (HY000): Table 'employee' was not locked with LOCK TABLES

读锁总结: 对表加读锁,不影响任何session的读操作,都是共享的。 但是当前session执行更新操作会失败报错,其他session更新则会阻塞等待。因为会阻塞等待,所以读锁是悲观锁。对表加读锁后,session仅仅能对当前表进行操作,不能对其表操作

读锁的特点

  • 持有表A读锁的session可以读表A,但是不能更新表A,也不能对其他表进行任何查询、更新操作
  • 其他session可以读取表A,但是不能更新表A

写锁示范

第一步:session1 对表user加写锁
mysql> lock table user write;
Query OK, 0 rows affected (0.00 sec)
第二部:session1 查询、更新操作不影响
mysql> select * from user;
+----+--------+------+---------+---------+
| id | name   | age  | salary  | version |
+----+--------+------+---------+---------+
|  1 | 风清扬 |   60 | 2000.00 |       0 |
|  2 | 朱元璋 |   80 | 3000.00 |       0 |
|  3 | 孙猴子 |  500 | 4000.00 |       0 |
|  4 | 唐玄奘 |   40 | 8000.00 |       0 |
+----+--------+------+---------+---------+
4 rows in set (0.00 sec)

mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('赵孟頫','20','1000.00','0');
Query OK, 1 row affected (0.04 sec)
第三步: session2 查询、更新操作都会阻塞等待

sesion1查询:

mysql> select * from user;

session2更新:

mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('王羲之','40','3000.00','0');


第四步: 释放session1的写锁,session2的更新操作立马完成

session1的写锁释放:

mysql> unlock tables;
Query OK, 0 rows affected (0.00 sec)

session2更新完成:

mysql> insert into `user` ( `name`, `age`, `salary`, `version`) values('王羲之','40','3000.00','0');
Query OK, 1 row affected (3 min 3.12 sec)

第五步:session1给表1加写锁,查询表2时报错
## 对表user加写锁
mysql> lock table user read;
Query OK, 0 rows affected (0.00 sec)

## 可以对表user进行查询
mysql> select * from user limit 1;
+----+--------+------+---------+---------+
| id | name   | age  | salary  | version |
+----+--------+------+---------+---------+
|  1 | 风清扬 |   60 | 2000.00 |       0 |
+----+--------+------+---------+---------+
1 row in set (0.00 sec)


## 查询表employee时报错
mysql> select * from employee limit 1;
ERROR 1100 (HY000): Table 'employee' was not locked with LOCK TABLES

写锁总结: 对表加写锁,不影响当前session的操作,但是会影响其他session的读写操作,其他session任何操作都会阻塞等待。因为会阻塞等待,所以写锁是悲观锁。对表加写锁后,session仅仅能对当前表进行操作,不能对其表操作

写锁的特点

  • session持有表A的写锁,则session可以对表A进行查询、更新操作
  • 排他性,其他session不能对表A进行查询、更新操作

读写锁注意事项

对表A加锁后,当前session仅仅能操作表A
mysql> LOCK TABLES A READ;
mysql> SELECT COUNT(*) FROM A;
+----------+
| COUNT(*) |
+----------+
|        3 |
+----------+
mysql> SELECT COUNT(*) FROM B;
ERROR 1100 (HY000): Table 'B' was not locked with LOCK TABLES
如果加锁时取了别名,则操作时只能使用别名
mysql> LOCK TABLE t AS myalias READ;
mysql> SELECT * FROM t;
ERROR 1100: Table 't' was not locked with LOCK TABLES
mysql> SELECT * FROM t AS myalias;
不能在一次查询中多次引用加锁的表
mysql> LOCK TABLE t WRITE, t AS t1 READ;
mysql> INSERT INTO t SELECT * FROM t;
ERROR 1100: Table 't' was not locked with LOCK TABLES
mysql> INSERT INTO t SELECT * FROM t AS t1;

并发带来的问题

更新丢失

脏读

事务A读取到了事务B修改的数据,但是事务B还未提交,事务B失败回滚,则事务A读取的无效,不符合一致性

幻读

事务A多次读取,后面读取读到了其他事务的提交新增的数据,导致查询结果前后不一,比如开始查询结果是1条,后面查询是2条。不符合隔离性

不可重复读

事务A多次读取,后面读取读到了其他事务的提交,导致查询的数据已经变化。
比如开始读取id为1的姓名是xiaoming,后面再次读取发现姓名是lilei。不符合隔离性。

事务

A:原子性,全部执行全部不执行
C:一致性,开始和完成时数据状态一致
I:隔离性,事务之间隔离
D:持久性,事务完成是永久的

指定事务特征的语法

## 官方语法, 可以设置事务隔离级别与访问模式; 用于InnoDB表。
SET [GLOBAL | SESSION] TRANSACTION
    transaction_characteristic [, transaction_characteristic] ...

transaction_characteristic: {
    ISOLATION LEVEL level
  | READ WRITE   # 读写模式
  | READ ONLY    # 只读模式
}

level: {
     REPEATABLE READ
   | READ COMMITTED
   | READ UNCOMMITTED
   | SERIALIZABLE
}

## 示例--将当前session的事务隔离级别设置为可重复读
 SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ ;

等效于:

mysql> set tx_isolation = 'repeatable-read';
Query OK, 0 rows affected (0.00 sec)

## 设置访问模式
SET TRANSACTION READ WRITE;

可选项 GLOBAL or SESSION 是用来指定执行的语句的范围为全局或者当前session。

MySQL 设置事务隔离级别从低到高分别为:

  • 读未提交 : READ UNCOMMITTED
  • 读已提交 : READ COMMITTED
  • 可重复读 : REPEATABLE READ 默认级别
  • 串行化 : SERIALIZABLE

注意事项:不允许在事务中指定 没有标明SESSION 或者 GLOBAL 的事务设置:

# 开启一个事务
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.02 sec)

# 在开启的事务中设置事务特征是不允许的
mysql> SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
ERROR 1568 (25001): Transaction characteristics can't be changed
while a transaction is in progress

命令行设置隔离级别,可以参考:https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_transaction_isolation

## 命令行设置事务隔离级别
- set tx_isolation = 'REPEATABLE-READ' | 'READ-UNCOMMITTED' | 'READ-COMMITTED' |'SERIALIZABLE'

命令行演示,使用begin、commit演示多个session之间的事务

事务隔离级别示例

Mysql默认是可重复读。

  • Mysql查看隔离级别命令
## 查看隔离级别
show variables like 'tx_isolation'

mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set, 1 warning (0.00 sec)

读未提交 READ UNCOMMITTED

SET TRANSACTION ISOLATION LEVEL READ UMCOMMITED;

事务A可以读取事务B未提交的事务,会出现脏读。

读已提交 READ COMMITTED

读提交都是读取自身session的最新快照。当通过锁读时(SELECT with FOR UPDATE or FOR SHARE),InnoDB 只会锁索引行记录,并不是他们之间的范围记录,如此便会允许其他事务插入新的记录。 间隙锁gap lock只会用在外键约束去检查重复键。

因为间隙锁不可用,所以可能出现幻读,其他session可以在间隙中插入数据。

只有基于行级的二进制日志支持 READ COMMITED 的隔离级别。如果使用 binlog_format = MIXED, 服务器自动使用基于行级的日志。

使用读提交的隔离级别,会有额外影响:

  • 对于UPDATE、DELETE 语句,InnoDB 仅仅持有更新或者删除的行的锁。 MySQL 在评估完 WHERE 条件之后,会释放不匹配的行的锁,大大减少了思索的可能性,但是还是有可能会发生死锁的。
  • 对于 UPDATE 语句,如果一条记录已经被锁了,InnoDB会采取“半一致性” 的读方式,会返回最新的已经提交的版本记录给MySQL,来匹配WHERE条件是否匹配。 如果存在记录匹配到了(必须会被UPDATE的),MySQL会再次读取这些记录并且这次 InnoDB 要么锁定它 要么 等待它的锁。

这里看一个官方示例。

CREATE TABLE t (a INT NOT NULL, b INT) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2),(2,3),(3,2),(4,3),(5,2);
COMMIT;

在这个示例中, 表时没有索引的,所以查询或者索引扫描时,会采用隐藏的集群索引来锁住记录行。

Session A 开启事务,并执行 UPDATE 操作:

# Session A
START TRANSACTION;
UPDATE t SET b = 5 WHERE b = 3;  # 没有用到索引会全表扫描,所有记录都会锁住

此时,session A 事务并未结束, 在另一个 Session B 中执行 UPDATE操作:

# Session B
UPDATE t SET b = 4 WHERE b = 2;

因为 InnoDB 执行每一个 UPDATE 操作,首先会为每一行记录获取可执行锁,然后才决定是否去更新。 如果 InnoDB 没有更新记录行,就会释放锁。 否则, InnoDB 会持有锁直到事务结束。这个示例影响事务如下:

当使用 REPEATABLE READ 级别时, 第一个 UPDATE 获取到了每一行的 x-lock, 事务A 读取并且没有释放它们:

x-lock(1,2); retain x-lock
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); retain x-lock
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); retain x-lock

此时事务B进行UPDATE,尝试进行锁,等待...

x-lock(1,2); block and wait for first UPDATE to commit or roll back

而如果是 READ COMMITED 级别,事务A获得了锁并且会释放那些不匹配的行记录:

x-lock(1,2); unlock(1,2)
x-lock(2,3); update(2,3) to (2,5); retain x-lock
x-lock(3,2); unlock(3,2)
x-lock(4,3); update(4,3) to (4,5); retain x-lock
x-lock(5,2); unlock(5,2)

事务B的UPDATE操作时,事务B会采取“半一致性”读方式,会读取最近已经提交的版本记录,决定哪些是匹配的:

x-lock(1,2); update(1,2) to (1,4); retain x-lock
x-lock(2,3); unlock(2,3)
x-lock(3,2); update(3,2) to (3,4); retain x-lock
x-lock(4,3); unlock(4,3)
x-lock(5,2); update(5,2) to (5,4); retain x-lock

不管怎样,如果where条件包含一个索引列,InnoDB 就会使用索引,只有索引列会考虑持有锁。 在下面的例子中,事务A的 UPDATE 操作持有每一个b=2的记录的 x-lock 锁。 事务B会阻塞因为它也试图获得相同记录的锁。

CREATE TABLE t (a INT NOT NULL, b INT, c INT, INDEX (b)) ENGINE = InnoDB;
INSERT INTO t VALUES (1,2,3),(2,2,4);
COMMIT;

# Session A
START TRANSACTION;
UPDATE t SET b = 3 WHERE b = 2 AND c = 3;

# Session B  会阻塞
UPDATE t SET b = 4 WHERE b = 2 AND c = 4;

设置事务隔离级别为读提交:

SET TRANSACTION ISOLATION LEVEL READ COMMITED;

事务B提交后,事务A才可能读取到事务B的数据
灭有脏读,但是可能幻读、不可重复读
同一个事务中多次查询结果可能是不一致的。
示例:
事务A先读取id为1的姓名为xiaoming;
事务B提交改为lilei;
事务A再次读取时读到lilei。
这样事务A两次读取的数据不一致,出现了幻读,不可重复读。按业务来讲,事务A应该第二次读取到的是xiaoming,因为事务A可能在第一次读到xiaoming时已经利用它实现了一些业务操作了。所以出现了不可重复读。

可重复读 REPEATABLE READ

set tx_isolation = 'repeatable-read';
或者
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

Mysql InnoDB默认隔离级别。
可以解决上述的案例问题。同一个事务中多次查询结果是一致的。
没法解决幻读。

示例:
事务A读取 薪水是2000,事务B改为3000并提交事务,
事务A再次读取还是2000;
事务A将薪水翻倍改为salary*2以为变成了4000,结果事务A查询发现是6000了。如果使用的是set salary=4000,那就是4000(所以这是一种错误的方式)。 事务A拿到了事务B的数据,产生了幻读。

演示幻读: 事务A作更新后,再次查询就出现了幻读。

【select读取的是快照版本,更新读取的是真实数据。】

MVVC机制: 多版本并发控制机制,使用快照版本数据达到可重复读,但是快照版本是不可信的,无法解决幻读问题。

可串行化 SERIALIZABLE

set tx_isolation = 'SERIALIZABLE';

效率低,可以解决脏读、不可重复读、幻读;
因为它将事务串行化了,没有了并发事务。

Mysql间隙锁

也叫Gap lock

事务A使用范围update时,假如表中只有id1-11的数据,mysl会对这些记录比如update table where id > 10 and id <20;会对id为11--19的数据都加锁,即便此时数据库并没有这些id为12--19的记录;
此时其他事务B来插入这样的记录insert id 为12 时,则事务B会等待,因为id12被事务A锁了。

死锁

事务A:select * from table where id = 1 for update;

事务B:select * from table where id = 2 for update;

事务A:select * from table where id = 2 for update;

事务B:select * from table where id = 1 for update;

查看死锁日志:

show engine innodb status\G 

延伸阅读:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容