- 锁
- 共享读锁(S锁)和 排他写锁(X锁)
- 行锁与表锁
innodb用的是行级锁,相对于表锁来说性能开销会更大。虽然叫做行级锁,但不表示他只锁住修改的行记录,即使找不到行记录,他也会产生锁。innodb 是根据扫描范围来锁定行记录,如果有索引,那么只会锁定索引的覆盖范围,如果找不到索引,就会扫描全表,那么行级锁就会升级为表级锁。
做个测试:
CREATE TABLE `user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(10) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `age` (`age`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'Jammes', 23);
INSERT INTO `user` VALUES (2, 'Kobe', 34);
INSERT INTO `user` VALUES (3, 'Smith', 45);
INSERT INTO `user` VALUES (4, 'JR', 34);
INSERT INTO `user` VALUES (5, 'Yi', 45);
Session 1 | Session 2 | |
T1 | start transaction; | start transaction; |
T2 |
select * from user;
+----+--------+------+
| id | name | age |
+----+--------+------+
| 1 | Jammes | 23 |
| 2 | Kobe | 34 |
| 3 | Smith | 45 |
| 4 | JR | 34 |
| 5 | Yi | 45 |
+----+--------+------+
| |
T3 |
update user set name="Polo" where name="Kobe" and age=45;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0 Changed: 0 Warnings: 0
| |
T4 |
update user set name = "Smith" where id=5;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
|
在T3,Session1 虽然查找不到任何符合条件的行记录,
但innodb会根据 ‘age’ 索引给 age = 45 的行上加上写锁, 而其他行不会加锁。
如果把 ‘age’ 索引去掉, 那么innodb 就会锁全表。
所以建立合适的索引不仅对查询性能有很大的提升, 对于并行写入也能有很大的帮助。
- 死锁
死锁是指两个事务互相等待对方锁的释放,恶性循环下去就会产生死锁。但是mysql默认会开启死锁检查,发现事务会产生死锁,就会主动结束掉事务。
如何检查运行服务器的死锁状态:
Show engine innodb status;
- 事务的特性ACID
- 原子性(atomicity)
- 一致性(consistency)
- 隔离型(isolation)
- 持久性(durability)
- MVCC, 多版本并发控制, 是指为了提高mysql的并发读写能力,事务查询时只是读取行记录某个时间点的快照,所以并不会去对数据行加读锁,也不用等待数据行锁的释放,自然对并发读写性能会有很大提升。
- 隔离级别
- 读未提交(Read Uncommitted)
这是隔离性最低的级别, 当前事务可以读取到其他事务未提交的更改。在此隔离级别下事务读取时也不会对记录行加读锁。
开启两个会话,隔离级别都设置为读未提交(
set session transaction isolation level read uncommitted;)
Session 1 | Session 2 | |
T1 | start transaction; | start transaction; |
T2 |
select * from user where id = 4;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 4 | Smith | 34 |
+----+-------+------+
| |
T3 |
update user set name = 'JR' where id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
| |
T4 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
1 row in set (0.00 sec)
| |
T5 | Rollback; | |
T6 |
select * from user where id = 4;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 4 | Smith | 34 |
+----+-------+------+
|
- 读提交(Read Committed)
读提交顾名思义就是在当前事务中只能读取到其他事务提交后到记录,他其实也是利用多版本并发控制(MVCC)的方式去读取行记录,但他会读取行记录的最新版本,所以在读提交事务隔离级别下, 事务不会对数据行加读锁。
开启两个会话,隔离级别都设置为读提交(
set session transaction isolation level read committed;)
Session 1 | Session 2 | |
T1 | start transaction; | start transaction; |
T2 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
| |
T3 |
update user set name = 'Smith' where id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
| |
T4 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
1 row in set (0.00 sec)
| |
T5 | commit; | |
T6 |
select * from user where id = 4;
+----+-------+------+
| id | name | age |
+----+-------+------+
| 4 | Smith | 34 |
+----+-------+------+
|
- 可重复读(Repeatable Read)
mysql默认的隔离级别就是可重复读,他是利用MCCC的方式去读取记录行, 但他读取的行记录版本永远是事务开启时的那个版本,这跟读提交读取的最新版本是有区别的。
开启两个会话,隔离级别都设置为可重复读(
set session transaction isolation level repeatable read;)
Session 1 | Session 2 | |
T1 | start transaction; | start transaction; |
T2 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
| |
T3 |
update user set name = 'Smith' where id = 4;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
| |
T4 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
1 row in set (0.00 sec)
| |
T5 | commit; | |
T6 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
|
- 可串行化(Serializable)
可串行化是隔离级别最高的隔离级别, 他会锁住数据行。
开启两个会话,隔离级别都设置为可串行化(
set session transaction isolation level serializable;)
Session 1 | Session 2 | |
T1 | start transaction; | start transaction; |
T2 |
select * from user where id = 4;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | JR | 34 |
+----+------+------+
| |
T3 |
update user set name = 'Smith' where id = 4;
等待锁释放……..
|
在测试中遇到一个不能理解的情况:
在autocommit 开启时, Session 1 更新id=4的记录行, 也就对记录行加了X锁,但是session 2在不显示声明start transaction 的情况下,竟然可以读取,理论上应该要等待Session1的锁释放。但是如果显示声明start transaction就正常会等待锁的状态。
Session 1 | Session 2 | |
T1 | start transaction; | |
T2 | update user set name = "Smith" where id=4; Query OK, 1 row affected (0.00 sec) | |
T3 | select * from user; +----+--------+———+ | id | name | age | +----+--------+———+ | 1 | Jammes | 23 | | 2 | Kobe | 34 | | 3 | Wade | 45 | | 4 | JR | 34 | +----+--------+------+ |
- 一致性非锁定读
一致性非锁定读是指在MVCC方式下事务读取的时候并不会锁定数据行或者等待数据行锁的释放,他只会读取数据行某个时间快照版本。 隔离级别读提交和可重复读下的读取方式就是一致性非锁定读,但两者还是又些区别的:
读提交级别下读到的数据行版本是最新的,而可重复读级别下读到的事务开启时的版本。
- 关于脏读和幻读
读未提交隔离级别下可以读取到其他事务未提交的数据行记录,如果其他事务回滚了, 那么该事务读取到的数据行记录就是错误的,这就是脏读;(测试下删除)
读提交隔离级别读取的最新版本的行记录, 所以在一个事务的不同时间点读取到的行记录可能就会不一样,这就是幻读;
可重复读隔离级别读取的是事务开启时的行记录版本,所以无论在事务的任何时间点读取到的结果都是一致的, 这就不会产生脏读和幻读。
- 隔离级别的应用场景
默认情况下mysql的隔离级别是可重复读, 那么是否有必要因为特定的场景去切换隔离级别呢?