Mysql-innoDB存储引擎(事物,锁,MVCC)

 innoDB的特性:

  从图中由上至下红色框中的信息是:基于主键的汇集索引 ,数据缓存,外键支持(逻辑上创建外键),行级别锁,MVCC多版本控制,事务支持。这些也是InnoDB最重要的特性。html

事务:

  数据库操做的最小工做单元,是做为单个逻辑工做单元执行的一系列操做;事务是一组不可再分割的操做集合(工做逻辑单元)。典型事务场景(转帐):这是两个事务mysql

  update user_account set balance = balance - 1000 where userID = 3;算法

  update user_account set balance = balance +1000 where userID = 1;sql

mysql中如何开启事务:数据库

  经过navicat使用命令 showvariables like ‘autocommit’; 查看自动提交是否开启。当开启后执行update语句会自动提交,当自动提交是关闭的,能够经过如下方式来建立事务提交:express

BEGIN;-- 这两个二选一开启事务
START TRANSACTION;
-- 这是一个事务
UPDATE ......
UPDATE ......
COMMIT;-- 提交或者回滚 ROLLBACK;

  begin / start transaction -- 手工开启事务。编程

  commit / rollback -- 事务提交或回滚。缓存

  set session autocommit = on/off; -- 从Session的角度设定事务是否自动开启。安全

JDBC 编程:session

  connection.setAutoCommit(boolean);

Spring 事务AOP编程:

  expression=execution(com.gpedu.dao.*.*(..))

事务ACID特性:

  • 原子性(Atomicity):最小的工做单元,整个工做单元要么一块儿提交成功,要么所有失败回滚
  • 一致性(Consistency):事务中操做的数据及状态改变是一致的,即写入资料的结果必须彻底符合预设的规则,不会由于出现系统意外等缘由致使状态的不一致
  • 隔离性(Isolation):数据并发的时候,一个事务所操做的数据在提交以前,对其余事务的可见性设定(通常设定为不可见)
  • 持久性(Durability):事务所作的修改就会永久保存,不会由于系统意外致使数据的丢失

事务并发带来什么问题:

  先来看第一张图:在下图中,一张表中记录只有一条,事务B修改该条记录的 age字段,而此刻 事务A来查询了,得到的age是18,接着事务B 回滚了,这样子就出现了脏读问题。

  再来看第二个图:事务A先查询了数据信息,此刻事务B进行了修改并提交,而后事务A又去查询了一遍,这个时候就会出现不可重复读的问题。

  第三张图:经过范围查询得到一条数据,此刻事务B 插入了一条数据,事务A又去查询得到了两条数据,此刻就发生了幻读。

  综上,事务并发给咱们带来了三个主要问题:脏读,不可重复读,幻读。

事务的隔离级别:

  • Read Uncommitted(未提交读) --未解决并发问题,事务未提交对其余事务也是可见的,脏读(dirty read)。
  • Read Committed(提交读) --解决脏读问题,一个事务开始以后,只能看到本身提交的事务所作的修改,不可重复读(nonrepeatableread)。
  • Repeatable Read (可重复读) --解决不可重复读问题在同一个事务中屡次读取一样的数据结果是同样的,这种隔离级别未定义解决幻读的问题。
  • Serializable(串行化) --解决全部问题,最高的隔离级别,经过强制事务的串行执行。

设置read uncommitted级别:set session transaction isolation level read uncommitted;

 

innoDB对隔离级别的支持程度:

  在InnoDB中隔离级别到底如何实现的呢? --经过锁、MVCC。

InnoDB中的锁:

  锁是用于管理不一样事务对共享资源的并发访问,InnoDB存储引擎支持行锁和表锁(另类的行锁,经过行锁锁住全部的行)。官方文档:https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html。表锁与行锁的区别:

  • 锁定粒度:表锁 > 行锁
  • 加锁效率:表锁 > 行锁
  • 冲突几率:表锁 > 行锁
  • 并发性能:表锁 < 行锁

MYSQL innoDB锁类型:

  • l 共享锁(行锁):Shared Locks
  • l 排它锁(行锁):Exclusive Locks
  • l 意向共享锁(表锁):Intention Shared Locks
  • l 意向排它锁(表锁):Intention Exclusive Locks
  • l 自增锁:AUTO-INC Locks

行锁的算法:

  • l 记录锁 Record Locks
  • l 间隙锁 Gap Locks
  • l 临键锁 Next-key Locks

 共享锁:

  又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据能够共享一把锁,都能访问到数据,可是只能读不能修改,加锁释锁方式:

-- 共享锁加锁
BEGIN
select * from users WHERE id=1 LOCK IN SHARE MODE;
rollback; 
commit; 
-- 在以上的SQL枷锁后未执行提交或者回滚执行其余事务执行
select * from users where id =1; -- 能够执行,共享锁特性
update users set age=19 where id =1;--会阻塞

排他锁:

  又称为写锁,简称X锁,排他锁不能与其余锁并存,如一个事务获取了一个数据行的排他锁,其余事务就不能再获取该行的锁(共享锁、排他锁),只有该获取了排他锁的事务是能够对数据行进行读取和修改,(其余事务要读取数据可来自于快照),加锁释锁方式:delete / update / insert 默认加上X锁。

-- 自动获取排它锁
set
session autocommit = OFF; -- 设置手动提交事务 update users set age = 23 where id =1; --执行该语句后未提交,在其余线程上,执行下列其余事务执行语句会处于阻塞commit; ROLLBACK; -- 手动获取排它锁 set session autocommit = ON; begin select * from users where id =1 for update; commit; -- 其余事务执行 select * from users where id =1 lock in share mode; select * from users where id =1 for update; select * from users where id =1;

innoDB--行锁到底锁了什么?

  首先先来看一下测试表的结构,其中用的是InnoDB引擎,有一个name的惟一索引,主键自增,有3条数据

DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `uname` varchar(32) NOT NULL,
  `userLevel` int(11) NOT NULL,
  `age` int(11) NOT NULL,
  `phoneNum` char(11) NOT NULL,
  `createTime` datetime NOT NULL,
  `lastUpdate` datetime NOT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_name` (`uname`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=100006 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('1', '李二狗', '2', '18', '13666666666', '2018-12-01 15:39:46', '2018-12-01 15:39:50');
INSERT INTO `users` VALUES ('2', '张三丰', '1', '29', '13777777777', '2018-12-01 16:35:41', '2018-12-01 16:35:44');
INSERT INTO `users` VALUES ('3', '武大郎', '2', '44', '13888888888', '2018-12-01 16:36:01', '2018-12-01 16:36:03');

案例1:紧接着在一个事务中执行如下语句:能够发现咱们把事务设置成手动提交,可是我并未提交或者回滚:

set session autocommit = OFF;
update users set lastUpdate=NOW() where phoneNum = '13666666666';

而后在其余事务中执行以下语句:会发现,上述SQL执行修改会得到默认的排它锁,而此刻并未释放,锁的列是ID为1,而后咱们下列要修改ID为2的数据也是出于阻塞,这是为何呢?

update users set lastUpdate=NOW() where id =2;
update users set lastUpdate=NOW() where id =1;

案例2,执行如下语句,能够发现咱们把事务设置成手动提交,可是我并未提交或者回滚:

set session autocommit = OFF;
update users set lastUpdate=NOW() where id = 1;

而后在其余事务上执行:会发现下面2条SQL执行后 第一条会顺利执行,而第二条会被阻塞。

update users set lastUpdate=NOW() where id =2;
update users set lastUpdate=NOW() where id =1;

案例三:执行一下语句:

set session autocommit = OFF;
update users set lastUpdate=NOW() where `name` = '李二狗';

而后在其余事务上执行:会发现前面两条会执行成功,然后面两条执行失败

-- 其余查询执行
update users set lastUpdate=NOW() where `name` = '李二狗';
update users set lastUpdate=NOW() where id =1;
update users set lastUpdate=NOW() where `name` = '张三丰';
update users set lastUpdate=NOW() where id =2;

  InnoDB的行锁是经过给索引上的索引项加锁来实现的。对于二级索引,会对一级索引也加锁。只有经过索引条件进行数据检索,InnoDB才使用行级锁,不然,InnoDB将使用表锁(锁住索引的全部记录)表锁:lock tables xx read/write;

意向共享锁(IS):表示事务准备给数据行加入共享锁,即一个数据行加共享锁前必须先取得该表的IS锁,意向共享锁之间是能够相互兼容的。

意向排它锁(IX):表示事务准备给数据行加入排他锁,即一个数据行加排他锁前必须先取得该表的IX锁,意向排它锁之间是能够相互兼容的。

  意向锁(IS、IX)是InnoDB数据操做以前自动加的,不须要用户干预。

  意义:至关于一个标记flgs,当事务想去进行锁表时,能够先判断意向锁是否存在,存在时则可快速返回该表不能启用表锁。

自增锁 AUTO-INC Locks:

  针对自增列自增加的一个特殊的表级别锁,查看自增锁默认值:show variables like 'innodb_autoinc_lock_mode';默认取值1,表明连续,事务未提交ID永久丢失。当级别为1,执行一下SQL:在插入数据的时候,这个表的ID为自增,连续回滚3次,这3次的ID会永久消失,在下次执行commit的时候ID会在原来的数值上加3.

begin;
insert into users(name , age ,phoneNum ,lastUpdate ) values ('tom2',30,'1344444444',now());
ROLLBACK;

针对行锁的算法:

临键锁 Next-key Locks:

  Next-key locks:InnoDB行锁的默认算法。锁住记录+区间(左开右闭),当sql执行按照索引进行数据的检索时,查询条件为范围查找(between and、<、>等)并有数据命中则此时SQL语句加上的锁为Next-key locks,锁住索引的记录+区间(左开右闭)。先来搞一张表:

DROP TABLE IF EXISTS `test`;
CREATE TABLE `test` (
  `id` int(11) NOT NULL ,
  `name` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `test` VALUES ('1', '1');
INSERT INTO `test` VALUES ('4', '4');
INSERT INTO `test` VALUES ('7', '7');
INSERT INTO `test` VALUES ('10', '10');

  在InnoDB的默认行级算法中会对数据行进划分:能够看到是一个左开右闭的这个一个展示。

  执行如下sql不提交:因为有数据命中则会锁住(4,7](7,10] 两个区间。未提交的状况下执行下列其余事务中前四条所有阻塞而最后一条会成功执行。

begin;
select * from test where id>5 and id<9 for update;
-- 其余事务
select * from test where id=4 for update; -- 阻塞
select * from test where id=7 for update; -- 阻塞
select * from test where id=10 for update; -- 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (9, '9'); -- 阻塞
INSERT INTO `test` (`id`, `name`) VALUES (11, '11');-- 成功

  为何InnoDB要选择(临键锁)Next-key locks做为InnoDB行锁的默认算法?解决幻读,由于B+Tree是有顺序的,从左往右顺序递增,把临键区间也锁住,其余事务要往里插入数据是插不进去的。

间隙锁 Gap Locks:继临键锁要是没有命中数据的状况下:

  Gap锁只在 Repeatable Read (可重复读)  的隔离级别的状况下才存在。

记录锁 Record Locks:继临键锁以后,在条件为精准匹配的时候。

 

那么锁是怎么解决上述产生 脏读,不可重复读,以及幻读的状况呢?

解决脏读:

解决不可重复读:

解决幻读:

死锁:

  • 多个并发事务(2个或者以上);
  • 每一个事务都持有锁(或者是已经在等待锁);
  • 每一个事务都须要再继续持有锁;
  • 事务之间产生加锁的循环等待,造成死锁

避免死锁:

  • 相似的业务逻辑以固定的顺序访问表和行。
  • 大事务拆小。大事务更倾向于死锁,若是业务容许,将大事务拆小。
  • 在同一个事务中,尽量作到一次锁定所须要的全部资源,减小死锁几率。
  • 下降隔离级别,若是业务容许,将隔离级别调低也是较好的选择
  • 为表添加合理的索引。能够看到若是不走索引将会为表的每一行记录添加上锁(或者说是表锁)

Mysql 中MVCC版本控制:

  MVCC是multiversion concurrency control的缩写,并发访问(读或写)数据库时,对正在事务内处理的数据作多版本的管理。以达到用来避免写操做的堵塞,从而引起读操做的并发问题 。提供MySQL事物隔离级别下无锁读,例如一个事物在执行update等修改数据的sql,并未提交时其余事物进行数据读取是不影响的,并且读取内容为数据变动以前的数据。

   MVCC多本版快照由innodb的rollback segment构照的,一个sql进行查找数据当查找到某一个数据须要到回滚段中查找数据时,就会根据当前页上行数据的一个指针到回滚段中查找对应数据,在innodb的表主键中都会存在三个隐藏的字段:

  •     DB_TRX_ID:该字段存储最后一个修改该行数据的事务ID,占用6byte的空间,MySQL的delete操做是标记删除,因此对应行数据的该字段就为一个删除标记。
  •     DB_ROLL_PTR:该字段就记录执行roll segment的指针信息,当事务须要rollback时就经过该字段寻找记录从新构照行数据,该字段占用7byte空间。
  •     DB_ROW_ID:记录每一个行ID,该ID值为单调递增型整数,在innodb表指定了主键以后DB_ROW_ID存在于主键索引上,若是无主键该值就不会存在,占用6byte空间。

   在一个sql进行查询时,读取到一行数据的DB_TRX_ID值和本身事物ID的对比,假如隔离级别为MySQL的默认级别,就只读取该ID值小于自己事物ID的数据,其他数据就须要经过DB_ROLL_PTR的信息到回滚段中读取。MVCC是否起到相应的做用需取决于数据库隔离级别的配置。

   在insert和update、delete的操做是有区别的,一个insert语句插入数据再rollback就是直接对undo log的删除,他并不会影响其余事物的读取操做,而update、delete操做是在原有数据作更改,可能有其余事物在对该行数据作读取操做,因此update、delete产生的undo log数据是由内部线程自动清理,在该数据无任何事务在使用时清理掉,因此在undo log中insert和update、delete产生的数据存于不一样位置。

  下面经过一个案例来熟悉一下MVCC的效果:

-- 数据准备
insert into teacher(name,age) value ('seven',18) ;--假设事务版本为1
insert into teacher(name,age) value ('qing',20) ;--假设事务版本为1
begin;                                 ----------1
select * from users ;                  ----------2

begin;                                 ----------3
update teacher set age =28 where id =1;----------4

  在每一行数据 插入数据表的时候,都会开启一个事务,每一行数据都会保存执行的时候所获取的事务版本号,当进行修改的时候会先copy一份待修改的数据到 Undo 缓冲区,在提交后然写入磁盘,在此过程当中会将原先的数据行的删除版本号置为当前事务ID,而后再在新的数据行把数据行版本号置为当前事务ID。

  当咱们按照 1,2,3,4,2 的顺序去执行的时候,首先执行 1 拿到的事务ID 是2,那么执行2查询出来就是原始数据,这个时候事务并无提交或者回滚,而后执行3开启一个事务拿到的事务ID 为3 ,此刻执行4(在更新操做的时候,采用的是先标记旧的那行记录为已删除,而且删除版本号是事务版本号,而后插入一行新的记录的方式。)进行 update 操做的时候会 copy 数据到Undo 缓冲区,而后将Undo.log的原始数据的删除版本号置为3,把新数据的事务版本号置为3,再执行2的时候因为此刻事务ID 仍是为2,因此根据查询规则查找数据行版本号小于当前事务版本的数据行,查找删除版本号大于当前事务版本的或者删除版本为nul的数据行,因为修改操做未提交,因此最终获得的结果数据仍是原始数据的值,并不会把修改的数据加载回来,解决了不可重复读的问题。

  若是按照这样的逻辑经过 3,4,1,2的顺序去执行,那么首先修改的操做会拿到事务ID为2,将原来的数据行copy出来,将原来的删除版本号置为当前事务ID,接着将备份数据的版本号置为当前版本号,而后执行查询操做再开启一个新事务,拿到的事务ID为3,根据查询规则,拿到的是进行了update 操做但并未提交的新数据,形成了脏读,这是为何呢?那么是由谁去解决这个问题的呢?其实这里面涉及到了 Undo.log的机制以及当前读,快照读的问题,那么接下来看看他们是怎么处理这个问题的 。

Undo Log:

  Undo Log 是什么:undo意为取消,以撤销操做为目的,返回指定某个状态的操做,undo log指事务开始以前,在操做任何数据以前,首先将需操做的数据备份到一个地方 (Undo Log),UndoLog是为了实现事务的原子性而出现的产物。

  Undo Log实现事务原子性:事务处理过程当中若是出现了错误或者用户执行了 ROLLBACK语句,Mysql能够利用Undo Log中的备份将数据恢复到事务开始以前的状态。

  UndoLog在Mysql innodb存储引擎中用来实现多版本并发控制。

  Undo log实现多版本并发控制:事务未提交以前,Undo保存了未提交以前的版本数据,Undo 中的数据可做为数据旧版本快照供其余并发事务进行快照读。

  以下图这样的处理就避免了脏读的问题。

当前读,快照读:

快照读:SQL读取的数据是快照版本,也就是历史版本,普通的SELECT就是快照读innodb快照读,数据的读取将由 cache(本来数据) + undo(事务修改过的数据) 两部分组成

当前读:SQL读取的数据是最新版本。经过锁机制来保证读取的数据没法经过其余事务进行修改UPDATE、DELETE、INSERT、SELECT … LOCK IN SHARE MODE、SELECT … FOR UPDATE都是当前读。

Redo Log:

  Redo Log 是什么:Redo,顾名思义就是重作。以恢复操做为目的,重现操做;Redo log指事务中操做的任何数据,将最新的数据备份到一个地方 (Redo Log)。

  Redo log的持久:不是随着事务的提交才写入的,而是在事务的执行过程当中,便开始写入redo 中。具体的落盘策略能够进行配置.RedoLog是为了实现事务的持久性而出现的产物。

  Redo Log实现事务持久性:防止在发生故障的时间点,尚有脏页未写入磁盘,在重启mysql服务的时候,根据redolog进行重作,从而达到事务的未入磁盘数据进行持久化这一特性。

   流程图以下:

  指定Redo log 记录在{datadir}/ib_logfile1&ib_logfile2 可经过innodb_log_group_home_dir 配置指定目录存储。一旦事务成功提交且数据持久化落盘以后,此时Redo log中的对应事务数据记录就失去了意义,因此Redo log的写入是日志文件循环写入的。

  • 指定Redo log日志文件组中的数量 innodb_log_files_in_group 默认为2
  • 指定Redo log每个日志文件最大存储量innodb_log_file_size 默认48M
  • 指定Redo log在cache/buffer中的buffer池大小innodb_log_buffer_size 默认16M

  Redo buffer 持久化Redo log的策略, Innodb_flush_log_at_trx_commit:

  • 取值 0 每秒提交 Redo buffer --> Redo log OS cache -->flush cache to disk[可能丢失一秒内的事务数据]。
  • 取值 1 默认值,每次事务提交执行Redo buffer --> Redo log OS cache -->flush cache to disk[最安全,性能最差的方式]。
  • 取值 2 每次事务提交执行Redo buffer --> Redo log OS cache 再每一秒执行 ->flush cache todisk操做。
相关文章
相关标签/搜索