赞
踩
官网文档:MySQL :: MySQL 5.7 Reference Manual
目录
事务定义:事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行;事务是一组不可再分割的操作集合。
ACID:
多个事务并发执行的时候会出现以下问题:
1.脏读:事务1读取到事务2未提交的数据,事务2回滚了数据,则事务1读取到了无效数据。
2.不可重复读: 事务1读取了一条数据,然后事务2修改了这条数据并且提交了,然后事务1再读这条数据,数据前后不一致。跟脏读有点像,但是这里提交了数据。
不可重复读重点在于 update 和 delete。
3.幻读:事务1使用范围查询数据,事务2往表里添加了数据,事务1再次查询,前后查询结果不一致。跟不可重复读有点像,但是这里是插入数据。
幻读重点在于 insert。
“脏读”、“不可重复读”和“幻读”,都是数据库读一致性问题,由数据库提供一定的事务隔离机制来解决。
不同的隔离级别是在数据可靠性和并发性之间的均衡取舍,隔离级别越高,对应的并发性能越差,数据越安全可靠。
隔离级别 | 脏读(Dirty Read) | 不可重复读(NonRepeatable Read) | 幻读(Phantom Read) |
读未提交(Read uncommitted) | 可能 | 可能 | 可能 |
读已提交(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
串行化(Serializable) | 不可能 | 不可能 | 不可能 |
设置系统级隔离级别 | 设置会话级隔离级别 |
set global tx_isolation='read-uncommitted'; | set tx_isolation='read-uncommitted'; |
set global tx_isolation='read-committed'; | set tx_isolation='read-committed'; |
set global tx_isolation='repeatable-read'; | set tx_isolation='repeatable-read'; |
set global tx_isolation='serializable'; | set tx_isolation='serializable'; |
查看系统级隔离级别 | 查看会话级隔离级别 |
select @@global.tx_isolation; | select @@tx_isolation; |
注意:修改隔离级别后需要重新连接客户端才会生效!
Mysql默认的事务隔离级别是可重复读(oracle是读已提交),用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别 。
上面说到数据库使用mvcc和锁实现隔离性,接下来将详细介绍 锁。下篇详细介绍MVCC。
mysql锁官网文档:MySQL :: MySQL 5.7 Reference Manual :: 14.7.1 InnoDB Locking
读锁(共享锁,S锁(Shared)):针对被锁住的数据,可以被所有事务加读锁,但是不能加写锁,直至读锁被释放。读操作在可重复度级别之前的都不加锁
写锁(排它锁,X锁(eXclusive)):针对被锁住的数据,只可以被当前事务加读锁、写锁,其他事务读锁、写锁都不能加,直至写锁被释放。所有的写操作都会加写锁。
接下来对这些锁进行详细解说和测试。
每次锁住整张表,粒度大,发生锁冲突概率高,并发低;开销小,加锁快;不会出现死锁。
lock table 表名称 read(write),表名称2 read(write);
show open tables;
unlock tables;
例:(注意引擎是InnoDB)
- DROP TABLE IF EXISTS `t_mylock`;
- CREATE TABLE `t_mylock` (
- `id` int(10) unsigned NOT NULL,
- `name` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
- INSERT INTO `t_mylock` VALUES (1, 'a');
- INSERT INTO `t_mylock` VALUES (2, 'b');
- INSERT INTO `t_mylock` VALUES (3, 'c');
- INSERT INTO `t_mylock` VALUES (4, 'd');
测试读锁:
给表加读锁后,当前session和其他session都可以读该表。
当前session插入或者更新锁定的表会报错,其他session插入或更新则会等待。
解锁后可以插入数据。
测试写锁:
上图得知:针对被锁住的数据,可以被当前的事务读和写。
图1:
图2:
图3:
图1、2、3得知:其他session 开启的事务读和写都会被阻塞,默认30秒后自动释放。
注意:仅InnoDB支持事务和行锁;MyIsam不支持事务,只支持表锁。
每次锁住一行数据,粒度小,发生冲突概率低;开销大,加锁慢;会出现死锁。mysiam不支持行锁,innorDB支持行锁。
行锁测试(注意引擎是InnoDB):
修改 id=1 的一行数据,会自动给这行数据自动加上行锁和写锁。
这时候用其他的事务修改 id=1 这行数据会被阻塞。但是可以读这条数据,因为InnoDB的读操作使用mvcc(多版本并发控制)机制,不需要加锁。
这时候修改其他行的数据可以成功,说明只给 id =1 的一行数据加上了行锁。
测试环境:mysql5.7,InnoDB,默认的隔离级别(RR)
在隔离级别为可重复读(Repeatable read)生效。间隙锁会封锁索引的间隙,禁止往索引区间内插入数据,在某些情况下可以解决幻读问题。
使用间隙锁锁住的是一个区间,而不仅仅是这个区间中的每一条数据。
产生间隙锁的条件(RR事务隔离级别下):
测试数据:
- DROP TABLE IF EXISTS `t_mylock`;
- CREATE TABLE `t_mylock` (
- `id` int(10) unsigned NOT NULL,
- `name` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `idx_name` (`name`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (1,'a');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (7,'g');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (10,'j');
在session1下执行SELECT * FROM t_mylock WHERE id BETWEEN 5 and 7 FOR UPDATE; 则其他事务无法添加和修改 (1, 7] 之间的数据。
从id索引分析到 t_mylock 表隐藏间隙:
1.只使用记录锁不会产生间隙锁。
session1:
- begin
- SELECT * FROM t_mylock where id=5 for update;
- -- commit;
sessoin2:
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (4,'a'); -- 成功
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (6,'a'); -- 成功
session1的查询只会锁住一行记录锁,不会产生间隙锁。
还原测试数据继续测试。
2.使用 主键索引/唯一键索引 范围查询产生间隙锁
session1:
- begin;
- SELECT * FROM t_mylock where id between 5 and 7 for update;
- -- commit;
session2:
- insert into t_mylock(id,name) values (3,'a'); -- 成功
- insert into t_mylock(id,name) values (4,'a'); -- 成功
- insert into t_mylock(id,name) values (5,'a'); -- 阻塞
- insert into t_mylock(id,name) values (8,'a'); -- 阻塞
- insert into t_mylock(id,name) values (10,'a'); -- 阻塞
- insert into t_mylock(id,name) values (11,'a'); -- 成功
测试结果:(5, 7]和(7, 10]这两个区间被加了区间锁不能插入数据,其他区间都可以。给[5, 7]区间加锁的时候,会锁住(5, 7]和(7, 10]这两个区间。
还原测试数据继续测试。
3.测试锁住不存在的数据
session1:
- begin;
- SELECT * FROM t_mylock where id = 3 for update;
- -- commit;
session2:
- insert into t_mylock(id,name) values (2,'a'); -- 阻塞
- insert into t_mylock(id,name) values (4,'a'); -- 阻塞
- insert into t_mylock(id,name) values (6,'a'); -- 成功
测试结果:给一条不存在的数据加锁的时候会产生间隙锁。
4.测试普通索引的间隙锁
测试数据脚本:
- DROP TABLE IF EXISTS `t_mylock`;
- CREATE TABLE `t_mylock` (
- `id` int(10) unsigned NOT NULL,
- `name` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`),
- KEY `idx_name` (`name`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (3,'a');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (8,'g');
session1:
- begin;
- select * FROM t_mylock where name='e' for update;
- -- commit;
name索引隐藏间隙:
1.(-∞, a]
2.(a, e]
3.(e, g]
4.(g, +∞]
session2:
- insert into t_mylock(id,name) values (2, 'a'); -- 成功
- insert into t_mylock(id,name) values (4, 'a'); -- 阻塞
- insert into t_mylock(id,name) values (5, 'b'); -- 阻塞
- insert into t_mylock(id,name) values (5, 'e'); -- 阻塞
- insert into t_mylock(id,name) values (6, 'g'); -- 阻塞
- insert into t_mylock(id,name) values (7, 'g'); -- 阻塞
- insert into t_mylock(id,name) values (9, 'g'); -- 成功
- insert into t_mylock(id,name) values (10, 'h'); -- 成功
测试结果:区间 (a, e] 、 (e, g] 被锁住,a到g的数据不能插入。但是有个奇怪的现象, (2, 'a') 和 (9, 'g')却执行成功了,这是怎么回事呢。
下面用一张索引的叶子节点图示意:
InnorDb的行锁是针对索引加的,当name相等的时候,id会按照大小进行排序。锁住e的时候会根据数据形成(a,3)到(g,8)的区间。
结论:
1.在普通索引上只要加锁都会产生间隙锁,而主键索引或唯一索引只有锁住多行才会加索。
2.普通索引上间隙锁,是先根据普通索引排序,然后再根据主键索引排序,锁住原始数据中间的数据。
5.无索引行锁会升级为表锁
- DROP TABLE IF EXISTS `t_mylock`;
- CREATE TABLE `t_mylock` (
- `id` int(10) unsigned NOT NULL,
- `name` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (3,'a');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (5,'e');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (8,'g');
session1:
- begin;
- select * FROM t_mylock where name='e' for update;
- -- commit;
session2:
- insert into t_mylock(id,name) values (1, 'a'); -- 阻塞
- insert into t_mylock(id,name) values (100, 'z'); -- 阻塞
结论:给非索引字段加行锁会升级为表锁。
临键锁,是记录锁与间隙锁的组合,它的封锁范围,既包含索引记录,又包含索引区间。
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况
show status like'innodb_row_lock%';
对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数
查看被锁住的表的sql:
- 查询是否锁表
- show OPEN TABLES where In_use > 0;
- 查看所有进程
- MySQL:
-
- show processlist;
- mariabd:
-
- show full processlist;
- 查询到相对应的进程===然后 kill id
-
- 杀掉指定mysql连接的进程号
- kill $pid
- 查看正在锁的事务
- SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;
- 查看等待锁的事务
- SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
- 查看innodb引擎的运行时信息
- show engine innodb status\G;
- 查看造成死锁的sql语句,分析索引情况,然后优化sql语句;
-
- 查看服务器状态
- show status like '%lock%';
- 查看超时时间:
- show variables like '%timeout%';
测试数据脚本:
- DROP TABLE IF EXISTS `t_mylock`;
- CREATE TABLE `t_mylock` (
- `id` int(10) unsigned NOT NULL,
- `name` varchar(255) DEFAULT NULL,
- PRIMARY KEY (`id`)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (1,'a');
- INSERT INTO `t_mylock` (`id`,`name`) VALUES (2,'b');
死锁定义:两个事务互相持有对方需要的锁,互相等待对方释放,并且都不释放。
例:
session1:
- begin;
- select * from t_mylock where id=1 for update;
- select sleep(10);
- select * from t_mylock where id=2 for update;
- commit;
session2:
- begin;
- select * from t_mylock where id=2 for update;
- select sleep(10);
- select * from t_mylock where id=1 for update;
- commit;
结果:
session1:
session2:
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁。
查看近期死锁日志信息:show engine innodb status;
mysql并发控制有两种方式,一个是MVCC(多版本并发控制),用于读的时候。令一个是2PL(两阶段锁协议),用于写的时候。使用2PL加锁的时候就有可能产生死锁。
2pc分两个阶段:1.获取锁。2.释放锁。
2pc不会一次将所有需要使用的数据加锁,并且在加锁阶段没有顺序要求,所以这种并发控制方式会形成死锁。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。