天天看點

【高性能Mysql】讀書筆記及實踐總結

前言:

        最近看了一系列mysql文章。对Mysql的基础知识理解有了一些与实践相符合的认知。mark一下。

正文:

        MySql基础知识分为四大点:一、并发控制(读/写锁);二、事务;三、多版本并发控制(MVCC);四、存储引擎。

一、并发控制:

        总的来说,只存在以下两种锁:

        1)、共享锁/读锁

        2)、排他锁/写锁

     平时我们经常听说各种各样的锁:例如InnoDB共有七种类型的锁如下。都能归入到共享/排他锁两种类型当中。

  • 共享/排它锁(Shared and Exclusive Locks)
  • 意向锁(Intention Locks)
  • 间隙锁(Gap Locks)
  • 记录锁(Record Locks)
  • 临键锁(Next-key Locks)
  • 插入意向锁(Insert Intention Locks)
  • 自增锁(Auto-inc Locks)       

二、几个并发的例子(此处引擎为InnoDB且隔离级别均为默认隔离级别RR)。

例一、插入并发:

初始化:表结构

【高性能Mysql】讀書筆記及實踐總結
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `test`
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=latin1;

-- ----------------------------
--  Records of `test`
-- ----------------------------
BEGIN;
INSERT INTO `test` VALUES ('1', 'bobo'), ('2', 'chenghe'), ('3', 'lisi');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
           

事务A:insert into test(name) values('xxx');

set session autocommit=0;
start transaction;
insert into test(name) values('xxx');
           

事务B:insert into test(name) values('ooo');

set session autocommit=0;
start transaction;
insert into test(name) values('ooo');
           

测试顺序:事务A——》事务B:隔离级别RR下,事务B未被A阻塞。

实际情况究竟是什么样子的?假如再执行如下即可知:

事务C:

set session autocommit=0;
start transaction;
insert into test(name) values('qqq');
commit;
select * from test;
           

展现结果如下:中间id为4,5缺失。

【高性能Mysql】讀書筆記及實踐總結

注意:并且就算将id为4,5的数据回滚,此处位置也不会被后续追加插入。

例二、插入并发(补充):

初始化:表结构

【高性能Mysql】讀書筆記及實踐總結
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `test`
-- ----------------------------
DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `un_id` (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

-- ----------------------------
--  Records of `test`
-- ----------------------------
BEGIN;
INSERT INTO `test` VALUES ('10', 'bobo'), ('20', 'chenghe'), ('30', 'lisi');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
           

事务A:insert into test(id,name) values (11,'xxxxx');

set session autocommit = 0;
start TRANSACTION;
insert into test(id,name) values (11,'xxxxx');
           

事务B:insert into test(id,name) values (12,'yyyyy');

set session autocommit = 0;
start TRANSACTION;
insert into test(id,name) values (12,'yyyyy');
           

测试顺序:事务A——》事务B:测试结果:事务B不被阻塞。

总结:由例一、例二可知:当插入并发的时候,锁的是主键id所在行。

例三、读写并发

 

create table `t_2`(
	`id` int(10) not null,
	`name` VARCHAR(10) not null default '',
	`sex` VARCHAR(10) not null default '',
	PRIMARY key(`id`)
)ENGINE = INNODB DEFAULT CHARACTER SET utf8;

insert into `t_2` VALUES(1,'bobo','male'),(3,'hebe','female'),
(5,'selina','female'),(9,'ella','female'),(10,'robot','male');
           
【高性能Mysql】讀書筆記及實踐總結

事务A:读事务:select * from t_2 where id BETWEEN 1 and 7 lock in share mode;(手动加s锁/读锁,后续会一并讲读写锁)

【高性能Mysql】讀書筆記及實踐總結
set session autocommit = 0;
start TRANSACTION;
select * from t_2 where id BETWEEN 1 and 7 lock in share mode;
           

事务B:写事务:insert into `t_2` VALUES(2,'robot','male');

set session autocommit = 0;
start TRANSACTION;
insert into `t_2` VALUES(2,'robot','male');
           

执行顺序:事务A——》事务B:结果事务B被阻塞。

    猜想1:此处被事务A锁全表。导致事务B不可执行;

    猜想2:此处被事务A锁了id区间段(1-7);

为了证明以上两个猜想,我做了几个实验,发现不仅1-7被锁住,甚至连id为8也无法插入。然而,id=10却可以插入。实际上事务A锁区间并不是id(1-7)而是id(1-9),如下图;

【高性能Mysql】讀書筆記及實踐總結

总结:锁住id为1-9区间段。原因是事务A执行的时候,id=7这条记录在表中找不到。所以锁住从id=1到后一条记录即id=9的区间。也就是说假如我事务A查询范围为id(1,13),当id=13找不到且13大于最大id时,会锁住1到正无穷。反之也是如此。画一张图方便理解。

【高性能Mysql】讀書筆記及實踐總結

例四、读写并发(续)

表结构:表还是例三的表。

【高性能Mysql】讀書筆記及實踐總結

事务A:select * from t_2 where id=14 lock in share mode;加S锁查询一条不存在的数据。

set session autocommit = 0;
start TRANSACTION;
select * from t_2 where id=14 lock in share mode;
           

事务B:insert into `t_2` VALUES(11,'robot','male');插入一条数据id=11。

set session autocommit = 0;
start TRANSACTION;
insert into `t_2` VALUES(11,'robot','male');
           

执行顺序:事务A——》事务B:事务B被阻塞。

结论:同例三上,锁住10<id区间。

补充:不知道有没有小伙伴发现,例三、例四的查询我都手动加S锁/读锁。假如不加S锁会怎样。在innodb引擎中,不加锁会因为多版本并发控制(MVCC)快照读。之后会细说快照读,现在只需要理解为,在查询结果是查询时间点查询数据库的一份快照,非原数据,不会阻塞写操作。

三、脏读/不可重复读/幻读/加锁读的几个例子来分析事务四个隔离级别RU/RC/RR/S。

此处附上修改事务隔离级别和问题关系及代码:

【高性能Mysql】讀書筆記及實踐總結
000、查询隔离级别:修改隔离级别:
select @@global.tx_isolation,@@tx_isolation;
set global transaction isolation level read committed;
set session transaction isolation level read committed;
set global transaction isolation level repeatable read;
set session transaction isolation level repeatable read;
           

脏读:事务可以读取未提交的数据。

隔离级别为READ UNCOMMITTED才会出现。举个例子:

表结构上例三如下:

【高性能Mysql】讀書筆記及實踐總結

事务A:插入未提交。

set global transaction isolation level read UNCOMMITTED;
set session transaction isolation level read UNCOMMITTED;
select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
insert into `t_2` VALUES(11,'robot','male');
           

事务B:查询id>10。

set global transaction isolation level read UNCOMMITTED;
set session transaction isolation level read UNCOMMITTED;
select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_2 where id>9;
           

执行顺序:事务A——》事务B:事务B能查到如下:产生脏读。

【高性能Mysql】讀書筆記及實踐總結

实际工作中很少有这样的情况,毕竟使用读未提交隔离级别的真心没见过。

不可重复读:因为两次执行同样的查询,可能会得到不一样的结果。

隔离级别低于或等于READ COMMITTED才会出现。这个概念比较难理解的地方在于,你会默认想当然的认为只要已经提交commit,就应该能被其它事务看到。实际上,这样就会造成同一个事务,两次查询得到不一样的结果。为了更好理解,因为RR隔离级别能避免这种情况,所以我做了一个对比。

举个例子:

事务RR隔离级别:

set global transaction isolation level REPEATABLE read ;
set session transaction isolation level REPEATABLE read;
select @@global.tx_isolation,@@tx_isolation;
           

事务A:两条查询语句;

set session autocommit = 0;
start TRANSACTION;
select * from t_2 where id>9;
--中间有延时--- 
select * from t_2 where id>9;
           

事务B:插入并提交。

set session autocommit = 0;
start TRANSACTION;
insert into `t_2` VALUES(11,'robot','male');
commit;
           

执行顺序:事务A执行到第三行查询,事务B执行完,事务A执行第5行查询。查看两次结果。均看不到id=11的结果。同一事务,两次查询中没有修改,查询结果相同。完美!

将隔离级别改为,事务RC隔离级别:

set global transaction isolation level read COMMITTED;
set session transaction isolation level read COMMITTED;
select @@global.tx_isolation,@@tx_isolation;
           

执行顺序:事务A执行到第三行查询,事务B执行完,事务A执行第5行查询。(同上)查看两次结果。第一次看不到,第二次能看到id=11的结果。同一事务,两次查询中没有修改,查询结果不相同。不可重复读。

PS:其实还是看需求,看同一事务中,是否需要看到已提交的结果。

幻读:当事务A读取某个范围记录时,事务B又在该范围插入新纪录,事务A再次读同一范围记录时,会产生幻行。

隔离级别为低于等于REPEATABLE READ都会出现。(也就是基本上没法避免,除非使用最后一种隔离级别)

其实在一开始例一,已经见到了幻读。只是当时没有在意。在此拎出来单独讲一下。

初始化:表结构(id自增)

【高性能Mysql】讀書筆記及實踐總結
SET NAMES utf8;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
--  Table structure for `test`
-- ----------------------------
DROP TABLE IF EXISTS `t_3`;
CREATE TABLE `t_3` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(50) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=0 DEFAULT CHARSET=latin1;

-- ----------------------------
--  Records of `test`
-- ----------------------------
BEGIN;
INSERT INTO `t_3` VALUES ('1', 'bobo'), ('2', 'chenghe'), ('3', 'lisi');
COMMIT;

SET FOREIGN_KEY_CHECKS = 1;
           

因为不可重复读这个特性,在rc级别已经被处理。但是,当插入之后。还是会出现一些很奇怪的事。

事务A:

set global transaction isolation level REPEATABLE read ;
set session transaction isolation level REPEATABLE read;
select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
insert into t_3(name) values('simayi');
--中间有延时:其他事务插入--- 
insert into t_3(name) values('zhugeliang');
select * from t_3;
commit;
           

事务B:

set global transaction isolation level REPEATABLE read ;
set session transaction isolation level REPEATABLE read;
select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
insert into t_3(name) values('caocao');
           

执行顺序:事务B在事务A两个插入之间延时处。

查询结果发现,同一个事务两个连续的插入,中间居然有空行。id=5呢?被事务B吃掉了么?虽然事务B连提交都没有,但是还是吃掉了这个id。幻读由此发生。

加锁读:读取的每一行记录都加锁。

加锁读能解决前面所有的问题:脏读/不可重复读/幻读。但,它自身就是一个问题。每一个都事务都加锁了。那肯定在并发的时候会出现性能瓶颈。所以根据需求权衡是否要采用这种方式。

set global transaction isolation level SERIALIZABLE;
set session transaction isolation level SERIALIZABLE;
select @@global.tx_isolation,@@tx_isolation;
           

四、多版本并发控制(MVCC):

书中MVCC对快照读的定义:只查找早于当前事务版本的数据行。

前面一直在说快照读,快照读到底是怎么一回事。快照读只在RR/RC隔离级别下工作。而且之前读写并发的时候,读写并发的读都不是快照读,说明快照读是一种非阻塞的读。举个例子。

初始化:表结构

【高性能Mysql】讀書筆記及實踐總結

例一、同一事务内:

事务A-1:

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
           

通过以下代码查看当前事务id/事务版本号:执行完事务A-1,事务id=15800

SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX;
           

事务A-2:

-----接事务A-1
insert into t_3(name) values('simayi');
select * from t_3;
           

通过上面代码查看当前事务id/事务版本号:执行完事务A-2,事务id依然是=15800

执行顺序:事务A-1-->事务A-2。执行结果。事务A-1:查询结果和表结构相同。事务A-2:查询结果多插入了一条数据。

结论:同一事务不同语句事务版本号唯一。

例二、不同事务内(默认隔离级别RR):

事务A:

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
----延时事务B执行在此-----
select * from t_3;
           

通过代码查看当前事务id/事务版本号:执行完事务A,事务id=15805;

事务B:

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
insert into t_3(name) values('simayi');
select * from t_3;
           

通过代码查看当前事务id/事务版本号:执行事务B,未提交时,事务id=15806;

执行顺序:事务A——》事务B (+commit)——》事务A,发现事务A始终事务id=15805,所以不会查到比事务id15805大的数据。

事务C:

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3;
           

执行顺序:再执行事务C:事务C事务ID为15807。可以看到事务B提交的结果。

画一张图吧,干巴巴的代码看起来很令人费解。

【高性能Mysql】讀書筆記及實踐總結

 

PS:假如隔离级别为RC:那么事务A两条查询结果将不同。第二条查询语句将会查询到新插入的数据。因为RR级别总是读事务开始时刻的事务快照(15805)。RC级别总是读当前数据行的最新快照(15807)。

此处还有一个知识点:UNDO、REDO日志。略。

五、存储引擎:

存储引擎不打算多讲。大概就是面试题的套路。常用两种InnoDB和MyisAm引擎。两种存储引擎的区别和优缺点及应用场景。默认平时使用InnoDB就够了。略。

六、聚集索引:

首先不打算讲聚集索引和普通索引的区别和联系,打算讲一下有索引和没有索引的区别。虽然我们平时因为MVCC快照读,导致读写不会互相阻塞。但是假如我们使用的是S锁的读。举个例子如下:

初始化:数据表:id为primary key,name 为普通字段,未加索引。(RR隔离级别)

【高性能Mysql】讀書筆記及實踐總結

事务A:S锁查询一条不存在的数据。

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
select * from t_3 where name = 'zhugeliang' lock in share mode;
           

事务B:任何对已知的索引有读写的操作都执行不了。例如增加一行,需要增加索引。删除一行,需要删除索引。更新一行,需要锁定当前索引行。

select @@global.tx_isolation,@@tx_isolation;
set session autocommit = 0;
start TRANSACTION;
insert into t_3(name) values('caocao');
           

结论:当s锁读一条不存在的记录,当条件字段没索引的时候,会索已知整表索引。凡是写操作(增删改)都将被阻塞。

解决方案:给name字段加普通索引。这样当查找name=‘不存在的记录’ 的索引时查不到,也就不会索对应的聚集索引。

反之引出聚集索引的使用。当查询条件为普通索引的时候,会先根据普通索引查到对应的聚集索引,再通过聚集索引查到value值。假如name为普通索引,id为聚集索引。查询条件为select * from t_3 where name = 'bobo';

【高性能Mysql】讀書筆記及實踐總結

如上,左图为普通索引bobo查找到聚集索引id=1,然后通过聚集索引id=1查找聚集索引上该条数据的其他信息。

写的有点累,有问题再说。~over

 

 

 

Â