InnoDB与MyISAM的最大不同有两点:一是支持事务(TRANSACTION);二是采用了行级锁
事务是为了实现业务上完整性而实现了,他可以由多条sql语句组成,这些语句要么全部成功,否则发生任何错误都将会回滚。事务具有4个属性。称为事务的ACID属性
相对于串行处理来说,并发事务处理能大大增加数据库资源的利用率,提高数据库系统的事务吞吐量,从而可以支持可以支持更多的用户。但并发事务处理也会带来一些问题,主要包括以下几种情况。
不可重复读和幻读都是两次的查询结果不同,但是不可重复读是倾向于行数据被修改,这个更好解决,对该行加锁即可。而幻读强调的是执行相同的查询时增加或者删除了一整行的数据。处理这个问题锁单行是无法解决的,需要通过锁表实现。这也是两个问题分开提出的原因。
上面的问题在所有关系型数据中都是存在,所以在ISO/ANSI SQL92标准中定义了4个事务隔离级别来分别解决以上的问题。
隔离级别 |
脏读 |
不可重复读 |
幻读 |
读未提交 |
存在 |
存在 |
存在 |
读已提交 |
解决 |
存在 |
存在 |
可重复读 |
解决 |
解决 |
存在 |
序列化 |
解决 |
解决 |
解决 |
MySQL的隔离级别的具体实现和上面sql标准中定义的有一个区别的地方,可重复读的隔离级别下,MySQL解决了幻读的问题,而MySQL的默认隔离级别为可重复读,该级别可以避免上述的所有问题。
实现事务的隔离级别,一般可以通过两种方式实现
innodb实现了两种类型的行锁:
一个事务中通过update,insert,delete等语句去修改一行数据,MySQL都会自动尝试的去获取一个排他性的写锁,这个获取锁的操作就会与其他获取同一个锁的事务竞争,只会有一个事务得到锁而执行操作,另一个继续竞争。而直接使用select 语句进行查询是不用获取锁的,可以随时进行查询,但是这也可能会造成获取到其他事务正在修改的数据(如果并不关心这一点可以使用该方式)。也可以在查询数据时使用锁去获取。
select * from table ..... for update
:对获取的这些数据添加一个排他的写锁,在事务提交前,其他事务都不能读写这些被加锁的数据。
select * from table ..... lock in share mode
:对获取的这些数据添加一个共享读锁,其他事务可以读取,但是不能修改数据。
MySQL行锁是通过给索引上的索引项加锁来实现的,这一点与Oracle不同,后者是通过在数据块中对相应数据行加锁来实现的。MySQL这种行锁实现特点意味着:只有通过索引条件检索数据,才使用行级锁,否则,InnoDB将使用表锁!
例如 select * from table where name=tom
,如果name字段没有索引,将会进行全表扫描来获得数据,innodb在扫描之前将会锁住整个表,直到事务提交。如果name字段有索引,MySQL实际会在索引上锁,但是有多个索引时,也会对数据进行上锁,以避免使用不同的索引获取值时出现数据的并发访问问题。
但是并不是使用了索引字段进行查询就能保证查询一定走了索引,某些情况下,使用索引可能没有直接全表扫描的效率高,应该使用explain查看查询计划
意向锁也分为两种:
意向锁是InnoDB自动加的,不需用户干预。例如进行全表扫描时,将会自动意向锁加锁。同时获取表锁前,也会确保该表中不存在行锁才能获取,否则任然可能会造成锁冲突。这四种锁的冲突与兼容情况如下:
锁 |
X |
IX |
S |
IS |
X |
冲突 |
冲突 |
冲突 |
冲突 |
IX |
冲突 |
兼容 |
冲突 |
兼容 |
S |
冲突 |
冲突 |
兼容 |
兼容 |
IS |
冲突 |
兼容 |
兼容 |
兼容 |
其他事务想要获取的锁的类型,必须和当前事务以获取锁的类型相互兼容,其他事务才能正常的获取,否则将会阻塞等待锁释放。
当我们用范围条件而不是相等条件检索数据,并请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的 索引项加锁;对于键值在条件范围内但并不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁 (Next-Key锁)。
例如我们进行范围查询时候, select * from id > 20
此时我们指定的范围是id > 20,表中的这些数据都会被加锁而避免被修改,同时,他们这些数据之间间隙也是会被加锁的,如果插入一个id = 30的数据(表中没有改数据),人仍然会被阻塞而禁止插入,这也是解决幻读问题一种实现。
所以避免范围查询也是一个数据库优化方式,如果可以,应该尽量的减小这个范围, 最好使用相等的条件查询。
以上的现象是在MySQL的默认隔离级别repeated read下,使用read commited隔离级别时有不同表现。
read commited 隔离下锁的表现:
repeated read隔离级别下锁表现
同时还需要注意以下问题:
where name=tom and age=18
和 where name=tom and age=20
,如果name字段建立了索引,虽然查询行数据时两行数据,但是由于使用的索引字段的条件时相同的,所以任然会锁冲突。通过一个MySQL内部状态量来查看争用情况show status like ‘innodb_row_lock%‘;
+-------------------------------+-------+ | Variable_name | Value | +-------------------------------+-------+ | Innodb_row_lock_current_waits | 0 | | Innodb_row_lock_time | 45336 | | Innodb_row_lock_time_avg | 6476 | | Innodb_row_lock_time_max | 12442 | | Innodb_row_lock_waits | 7 | +-------------------------------+-------+
如果InnoDB_row_lock_waits和InnoDB_row_lock_time_avg的值比较高,说明锁竞争越严重(以上数据是测试了许多锁竞争后情况后的数值),我们需要分析锁竞争的原因,使用方案优化
仅仅使用上面的锁方案,在实现各个级别隔离性前提下,只能做到多个事务同时并发读取,而多个事务同时读写或多个事务同时写都会触发锁竞争,使得这些事务串行化,从而降低了并发量。而使用mvcc版本,可以实现同时读写时的并发执行。
mvcc想要实现的是读写之间的并发,也就是一个事务在读取内容的时候,可以有其他的一个写操作的事务(仅此一个,否则多个写操作并发)在执行,并且这不会破坏数据的一致性,同时达到隔离级别的要求。
数据库的更改会在undo log中形成一个版本链的关系,每次insert 或 update一个数据,都会创建该记录的新的版本 (delete也看作update操作,他会把改变记录的deleted位标记),并在该记录的行信息中修改该记录的事务的唯一id,将来作为事务可见性的判断。基于这个版本链,在MVCC并发控制中,每次写操作会创建一个新的版本,而读操作可以分成两类:快照读 (snapshot read)与当前读 (current read)。
快照读:当开启一个事务之后,第一次执行普通的select语句时(不加锁的select语句),会将本次在版本连中最新的一次已提交的记录做一次快照并暂存,且这个快照只有该事务可见,其他事务可以在原始的版本链中修改数据,但该事务的快照内容不会修改。
当前读:读取的是版本链中记录的最新的已提交的版本,并且,当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
快照读和当前读分别包括以下语句:
快照读:使用普通的 select 语句(不包括 select ... lock in share mode, select ... for update)都是快照读。
当前读:select ... lock in share mode
,select ... for update,insert
,update
,delete
语句,这些语句获取的是数据库中的最新数据。
InnoDB存储引擎默认事务隔离级别是RR(可重复读),是通过 "行级锁+MVCC"一起实现的,正常读的时候不加锁,写的时候加锁。而 MCVV 的实现依赖:隐藏字段、Read View、Undo log。
列名 |
是否必须 |
占用空间 |
描述 |
row_id |
否 |
6 |
行唯一标识,递增 |
transaction_id |
是 |
6 |
表示最近一次对本记录行作修改(insert | update)的事务ID,delete操作是一个update操作,非真删除,会更改删除位,deleted |
roll_pointer |
是 |
7 |
回滚指针,指向当前记录行的undo log信息 |
这是一个读视图,也就是一个快照,当开启一个事务之后,使用普通的select第一次读取内容时,就会创建一个readview快照信息,记录当前时间点一些信息。主要包括
Read view主要配合undo log版本链做数据的可见性分析。
undo log使用一个版本链来记录当前版本的数据和历史版本的数据,事务可以进行回滚也是利用undo log来实现的。
undo log主要会记录数据的insert和update操作,至于delete也是update操作,是将标记删除位的假删除。每次操作都会创建创建一个新的版本数据,并在数据的隐藏字段中记录了修改该数据事务id。这样即使该事务操过程中其他的事务对数据进行了修改,该线程比较这个事务id的不同,可以判断出哪个版本的数据是在本事务开启后才进行修改,从而不去读取哪些数据,也就是那些数据对本事务来说是不可见的,然后从版本链中找到由本事务最新修改的内容,在该事务读取数据的时候,不用对数据加锁。
简要的过程为
undo log中记录了一个版本链信息,数据的每一个版本都记录了修改他的事务id,当在一个事务中使用普通的查询语句时,将会生成一个Readview信息,这里有两种情况:
原文:https://www.cnblogs.com/k5210202/p/13073899.html