MySql锁和事务隔离级别

前言

MySql索引底层数据结构和算法:https://blog.csdn.net/yhl_jxy/article/details/88392411

MySql explan执行计划详解:https://blog.csdn.net/yhl_jxy/article/details/88570154

MySql 索引优化原则:https://blog.csdn.net/yhl_jxy/article/details/88636685

一 锁概述

1、什么是锁?

锁是计算机协调多个进程或线程并发访问某一资源的机制。

在数据库中,锁主要用于解决并发访问时保证数据的一致性和有效性。

2、锁的分类?

1)从性能上分为乐观锁和悲观锁

2)从对数据库操作的类型分为读锁和写锁(都属于悲观锁)。

    读锁(共享锁):多个读操作可以同时对同一份数据进行读而不会互相影响;

    写锁(排它锁):当前写操作没有完成前,它会阻断其他写锁和读锁;

扫描二维码关注公众号,回复: 10599148 查看本文章

3)从对数据操作的粒度分为表锁和行锁

二 表锁

1、表锁

开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

2、手动加锁语法

lock table 表名1 read(write),表名2 read(write);

3、表锁演示

3.1 脚本准备

drop table if exists testlock;

CREATE TABLE testlock (
 id INT (11) NOT NULL AUTO_INCREMENT,
 userName VARCHAR (20) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE = MyISAM DEFAULT CHARSET = utf8;

INSERT INTO testlock (id, userName) VALUES (1, 'a');
INSERT INTO testlock (id, userName) VALUES (2, 'b');
INSERT INTO testlock (id, userName) VALUES (3, 'c');

3.2 表上加读锁演示

开两个MySql终端窗口。

SessionA SessionB 说明
mysql> lock table testlock read;
Query OK, 0 rows affected (0.00 sec)
  SessionA给testlock加读锁。
  mysql> select * from testlock;
+----+----------+
| id | userName |
+----+----------+
|  1 | a        |
|  2 | b        |
|  3 | c        |
+----+----------+
3 rows in set (0.00 sec)
SessionB可以对testlock进行查询,
说明表上加读锁,别的session可以读取
数据,不受表读锁的影响。
  mysql> insert into testlock values(4, 'd'); SessionB做插入操作,插入语句
被阻塞,说明表上的读锁阻塞写操作,即insert,update,delete操作。
mysql> unlock table;
Query OK, 0 rows affected (0.00 sec)
  SessionA释放表上的读锁。
  mysql> insert into testlock values(4, 'd');
Query OK, 1 row affected (532.36 sec)
 
mysql> select * from testlock;
+----+----------+
| id | userName |
+----+----------+
|  1 | a        |
|  2 | b        |
|  3 | c        |
|  4 | d        |
+----+----------+
4 rows in set (0.00 sec)
当SessionA释放表锁后,SessionB
的insert插入成功。如果再次查询,
可以看到成功插入4这条数据。

表上加读锁会阻塞写操作,但是不会阻塞读操作。

3.3 表上加写锁演示

SessionA SessionB 说明
mysql> lock table testlock write;
Query OK, 0 rows affected (0.00 sec)
  SessionA给testlock加写锁。
  mysql> select * from testlock; SessionB对testlock进行查询被阻塞,
说明表上加写锁,别的session不能
进行读操作,当然写操作也是不行的。
mysql> unlock table;
Query OK, 0 rows affected (0.00 sec)
  SessionA释放表上的写锁。
  mysql> select * from testlock;
+----+----------+
| id | userName |
+----+----------+
|  1 | a        |
|  2 | b        |
|  3 | c        |
|  4 | d        |
+----+----------+
4 rows in set (0.00 sec)
当SessionA释放表锁后,SessionB
的select查询成功。

表上加写锁则会把读操作和写操作都阻塞;

4、表锁总结

表上加读锁会阻塞写操作,但是不会阻塞读操作;

而表上加写锁则会把读操作和写操作都阻塞;

三 行锁

行锁偏向InnoDB存储引擎,开销大,加锁慢,会出现死锁,锁定粒度最小,发生锁冲突的概率最低,

并发度也最高。InnoDB与MYISAM有两个最大的不同点:支持事务(TRANSACTION)和采用了行级锁。

1、事务ACID

事务是由一组SQL语句组成的不可分割的最小逻辑单元。事务具有ACID属性。

原子性(Atomicity) 

      事务是一个原子操作单元,整个事务中的所有操作要么全部提交成功,要么全部执行失败,

对于一个事务的操作不可能只执行其中的一部分操作,这就是事务的原子性。

一致性(Consistent) 

      在事务开始和完成时,数据都必须保持一致状态。这说明,事务里面操作的数据要保证数据的完整性和正确性。

隔离性(Isolation) 

      通常来说,一个事务在未提交的时候,对另外一个事务来说数据是不可见的。

持久性(Durable) 

      事务提交之后,修改的数据就保存在数据库中,就算机器崩掉了,数据也还存在。

2、并发事务处理存在的问题

更新丢失(Lost Update)

     当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,

就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新。

脏读(Dirty Reads)

      A事务正在修改数据,事务B读取到了事务A已经修改但尚未提交的数据,B还在这个数据基础上做了操作。

此时,如果A事务回滚,B读取的数据无效,并且拿无效的数据去做了别的操作,数据乱套了,不符合一致性要求。

这个现象就称为脏读。

不可重读(Non-Repeatable Reads) 

      A事务正在修改数据,事务B第一次读取了事务A还没修改的数据,B拿这个数据处理相应业务,过了会B又一次读取

同样一条A修改并提交事务的数据。B事务两次拿到认为是同样的数据,其实不是同样的数据,重复读取数据导致

数据不一致问题,所以这种现象就叫做“不可重复读”。

幻读(Phantom Reads)

      一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,

这种现象就称为“幻读”。

3、事务隔离级别

对“肮读”、“不可重复读”、“幻读”通过不同的事务隔离级别来解决。

Read Uncommitted(未提交读)

       该隔离级别,所有事务都可以看到其他事务未提交的执行结果。该隔离级别很少用于实际应用,

因为它的性能也不比其他级别好多少。同时,会产生脏读(Dirty Read)。

Read Committed(已提交读)

       大多数数据库系统的默认隔离级别(但不是MySQL默认的)。一个事务只能看见已经提交事务所做的改变。

但是该隔离级别处理不了“重复读取”的问题,所以也叫不可重复读。

Repeatable Read(可重复读)

       该隔离级别是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,

会看到同样的数据行。但是会产生幻读 (Phantom Read)问题。

InnoDB和Falcon存储引擎可以通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决该问题。

Serializable(可串行化)

       这是最高的隔离级别,它通过强制事务排序,不会出现并发访问数据,从而解决幻读问题。

它是在每个读的数据行上加上共享锁,可能导致大量的超时现象和锁竞争。

事务隔离级别 肮脏(Dirty Read)可能性 不可重复读(Non-Repeatable Reads)可能性 幻读(Phantom Reads)可能性
未提交读(Read uncommited) Yes Yes Yes
提交读(Read commited) No Yes Yes
可重复读(Repeatable Read) No No Yes
可串行化(Serializable) No No No

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度

上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,

比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力。

4、事务隔离级别演示

脚本准备

drop table if exists account;
create table account (
  id int(11) not null auto_increment,
  user_name varchar(225) default null comment '用户名',
  balance bigint(11) default null comment '账户余额',
  create_time datetime default current_timestamp comment '开户时间',
  primary key (id)
) engine = InnoDB default charset = utf8 comment = '账户表';

insert into account (user_name, balance, create_time)
values ('ZhangSan', 1200, now()), ('LiSi', 3600, now()), ('WangWu', 2500, now());

未提交读(Read uncommited)

在实战中,不要使用该事务隔离级别,下面演示未提交读现象。

SessionA SessionB 说明
mysql> set tx_isolation='read-uncommitted';
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='read-uncommitted';
Query OK, 0 rows affected (0.00 sec)
SessionA和SessionB事务隔离级别
设置为未提交读(Read uncommited)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
SessionA和SessionB开启事务
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1200 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1200 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
SessionA和SessionB看到的数据一样
mysql> update account set balance = balance - 200
where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
  SessionA将id为1的数据金额
从1200修改为1000。
  mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
SessionB能够读到SessionA还没有提交的执行结果,balance=1000。
mysql> rollback;
Query OK, 0 rows affected (0.00 sec)
 
mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1200 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
 

SessionA将事务回滚,id为1的balance恢复为1200。

  mysql> update account set balance = 1000 + 500 where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)

SessionB拿到1000,在账户上加500,然后id为1的balance变为了1500,但是,实际上SessionA已经回滚,数据变为了1200,SessionB加500,实际上应该是1700才对,中间丢了200,因为SessionB读取的1000是脏数据,加完500就变成了1500。这就是未提交读隔离级别带来的问题,脏读。

提交读(Read commited)

为了解决脏读,将事务隔离级别提高到“提交读”,下面演示“提交读”事务隔离级别是怎么回事,以及会带来什么问题。

SessionA SessionB 说明
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='read-committed';
Query OK, 0 rows affected (0.00 sec)
在设置事务级别前,先commit一下,预防出现你在干别的,影响效果。SessionA和SessionB事务隔离级别设置为提交读(Read commited)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
SessionA和SessionB开启事务
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1500 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1500 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
SessionA和SessionB看到的数据一样
mysql> update account set balance = balance - 500
where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
  SessionA将id为1的数据金额
从1500修改为1000。
  mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
SessionB能够读不到SessionA还没有提交的执行结果,balance还是1500。解决了脏读的问题。

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

mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)

  SessionA将事务提交,id为1的balance持久化为1000。
  mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
SessionB再次读取id为1的数据,balance拿到了SessionA修改并提交事务后的1000
    如果程序里面通过balance的值判断程序逻辑,会导致两次走的逻辑不一样,因为两次拿到不同的值。这就是“提交读”带来的问题,
因为重复读取数据,会拿到不同的值,可能会导致出现问题,所以,改级别也称为“不可重复读”。

可重复读(Repeatable Read)

为了解决“不可重复读”,将事务隔离级别提高到“可重复读”,下面演示“可重复读”事务隔离级别是怎么回事,

以及会带来什么问题。

SessionA SessionB 说明
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='repeatable-read';
Query OK, 0 rows affected (0.00 sec)
在设置事务级别前,先commit一下,预防出现你在干别的,影响效果。SessionA和SessionB事务隔离级别设置为可重复读(Repeatable Read)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)
SessionA和SessionB开启事务
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)
SessionA和SessionB看到的数据一样
mysql> update account set balance = balance - 500
where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
 
mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
  SessionA将id为1的数据金额
从1000修改为500。
  mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
SessionB读不到SessionA还没有提交的执行结果,balance还1000。
解决了脏读的问题。

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

mysql> select * from account;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    500 | 2019-03-20 11:21:31 |
|  2 | LiSi      |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu    |    2500 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
3 rows in set (0.00 sec)

  SessionA将事务提交,id为1的balance持久化为500。
  mysql> select * from account where id = 1;
+----+-----------+---------+---------------------+
| id | user_name | balance | create_time         |
+----+-----------+---------+---------------------+
|  1 | ZhangSan  |    1000 | 2019-03-20 11:21:31 |
+----+-----------+---------+---------------------+
1 row in set (0.00 sec)
SessionB再次读取id为1的数据,balance拿到的还是1000,并没有读取SessionA提交的500,因为MySql用MVCC对数据做了版本控制,解决了“不可重复读”问题,是可以重复读的,并且前后读取的数据是一样,所以,这个事务隔离级别称为“可重复读”。
  mysql> update account set user_name = 'ZhangSan01' where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

mysql> select * from account where id = 1;
+----+------------+---------+---------------------+
| id | user_name  | balance | create_time         |
+----+------------+---------+---------------------+
|  1 | ZhangSan01 |     500 | 2019-03-20 11:21:31 |
+----+------------+---------+---------------------+
1 row in set (0.00 sec)
SessionB将id为1的user_name更新为"ZhangSan01",然后再查下id为1的数据,发现balance变为了500,这是为什么,不是说好的MVCC版本控制的吗?
这是因为MySql在发生写操作的时候,会让数据同步为最新的数据,所以SessionB在发生写后读取的就是数据库持久化的最新数据。
SessionB在没有对id为1进行写操作的时候,每次读取1000,但是当发生写操作后,读取数据库持久化的最新值500,这个就是”幻读“问题,仿佛给你变魔术变出来的一样。

可串行化(Serializable)

为了解决“幻读”,将事务隔离级别提高到“可串行化”,下面演示“可串行化”事务隔离级别是怎么回事,

以及会带来什么问题。

SessionA SessionB 说明
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='serializable';
Query OK, 0 rows affected (0.00 sec)
mysql>commit;
Query OK, 0 rows affected (0.00 sec)
mysql> set tx_isolation='serializable';
Query OK, 0 rows affected (0.00 sec)
在设置事务级别前,先commit一下,预防出现你在干别的,影响效果。SessionA和SessionB事务隔离级别设置为可串行化(Serializable)。
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> set Innodb_lock_wait_timeout = 10;
Query OK, 0 rows affected (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> set Innodb_lock_wait_timeout = 10;
Query OK, 0 rows affected (0.00 sec)
SessionA和SessionB开启事务,并且设置锁会话锁超时为10秒。
mysql> select * from account;
+----+------------+---------+---------------------+
| id | user_name  | balance | create_time         |
+----+------------+---------+---------------------+
|  1 | ZhangSan01 |     500 | 2019-03-20 11:21:31 |
|  2 | LiSi       |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu     |    2500 | 2019-03-20 11:21:31 |
+----+------------+---------+---------------------+
3 rows in set (0.00 sec)
mysql> select * from account;
+----+------------+---------+---------------------+
| id | user_name  | balance | create_time         |
+----+------------+---------+---------------------+
|  1 | ZhangSan01 |     500 | 2019-03-20 11:21:31 |
|  2 | LiSi       |    3600 | 2019-03-20 11:21:31 |
|  3 | WangWu     |    2500 | 2019-03-20 11:21:31 |
+----+------------+---------+---------------------+
3 rows in set (0.00 sec)
SessionA和SessionB看到的数据一样
  mysql> insert into account values(4, 'four', 400, now());
1205 - Lock wait timeout exceeded; try restarting transaction
SessionB执行insert阻塞,获取不到锁,被SessionA锁住了。等10秒后,SessionB获取锁超时。所以,可串行化(Serializable)隔离级别排队上锁执行,效率非常低,容易出现锁超时等问题,实际中一般不用。任务排队执行,没有并发访问数据情况,所以不会出现”幻读“问题。

四 总结

InnoDB存储引擎实现了行级锁,在锁定机制方面所带来的性能损耗可能比表级锁定更高一些,

但是在整体并发处理能力要远远优于MYISAM的表级锁定的。当系统并发量高的时候,

InnoDB的整体性能和MYISAM相比就会有比较明显的优势了。但是,InnoDB的行级锁定同样也有其脆弱的一面,

当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差。

但是,一般现实中很多业务都是需要事务的,所以一般都是用InnoDB引擎。

发布了502 篇原创文章 · 获赞 358 · 访问量 118万+

猜你喜欢

转载自blog.csdn.net/yhl_jxy/article/details/88657913