官方文档:InnoDB Locking and Transaction Model
本文主要介绍事务的特性、事务并发可能导致的问题、数据库锁机制、事务的隔离级别。
一、事务的ACID特性
事务具有4个特性:原子性、一致性、隔离性、持久性,这4个特性简称为ACID特性。
1.原子性(Atomicity)
组成事务的多个操作必须是一个不可分割的原子操作,要么所有操作都执行成功,整个事务才提交。要么事务中的任何一个操作失败,整个事务都必须回滚,让数据库回到初始状态。
2.一致性(Consistency)
事务的执行使数据从一个状态转换为另一个状态,但是对于整个数据的完整性保持稳定。
比如,A和B两个账户,不管这两个账户相互怎么转账,A账户的钱和B账户的钱加起来的总额是不变的。
3.隔离性(Isolation)
一个事务的执行不能被其它事务干扰。准确的说,并非做到完全无干扰,数据库规定多种事务隔离级别,不同的隔离级别对应不同的干扰程度,隔离级别越高,干扰越小,数据一致性越好,但并发性也越弱。
4.持久性(Durability)
事务执行成功后,它对数据库中数据的改变就是永久性的。即使提交事务后数据库马上崩溃了,在数据库重启时,也能保证能够通过某种机制恢复数据。
事务的隔离性是通过锁来完成的,而原子性、一致性和持久性是通过数据库的redo log和undo log来完成的。
二、事务并发可能导致的问题
通过锁机制可以实现事务隔离性的要求,使得事务可以并发工作。但锁提高了并发,却带来了一些潜在问题。
1.脏读(Dirty Read)
脏数据,指的是未提交的数据。脏读,是指一个事务读取到另一个事务中未提交的数据,也就是脏数据。这明显违反了数据库的隔离性。
在下面场景中,B取款500元,之后又撤销了该动作,而A往B账户中转账100元,由于A读取了B事务未提交的数据,最终导致账户损失500元。
2.不可重复读(NonRepeatable Read)
不可重复读指一个事务读取了另一个事务已提交的更改数据。例如,在下面场景中,B往A账户转账100元,A两次读取账户的余额数据不一致。
不可重复读与脏读的区别是:脏读是读到了未提交的数据,而不可重复读是读到了已经提交的数据。同样也违反了数据库事务一致性的要求。
一般来说,不可重复读的问题是可以接受的,因为其读到的是已经提交的数据,本身并不会带来很大的问题。因此,很多数据库厂商(如Oracle,SQL Server)将其数据库的默认隔离级别设置为READ COMMITED,在这种隔离级别下,允许事务不可重复读的现象。
在InnoDB存储引擎中,通过使用Next-Key Lock算法来避免不可重复读的问题。在Mysql官方文档中,将不可重复读的问题定义为Phantom Problem,即幻读。在Next-Key Lock算法下,对于索引的扫描,不仅是锁住扫描到的索引,而且还锁住这些索引覆盖的范围(Gap)。因此在这个范围内的插入都是不允许的,这样就避免了其它事务在这个范围内插入数据导致不可重复读的问题。
3.幻读(Phantom Read)
官方文档:phantom和Phantom Rows
幻读指一个事务读到了另一个事务新增的数据。InnoDB和XtraDB存储引擎使用MVCC解决了幻读问题。
下面的场景,银行系统在同一事物中两次统计存款账户的总金额,在两次统计之间刚好新增了一个存款账户并存入了100元,这者两次统计的总金额将不一致。
不可重复读和幻读是两个容易混淆的概念。不可重复读指读到了已提交事务更改的数据(更新或删除),而幻读指读到了其它已提交事务的新增数据。
为了避免这两种情况,采取的对策是不同的,防止读到更改数据,只需对操作的数据加行级锁,阻止操作的数据发生变化。而防止读到新增数据,则往往需要添加表级锁-将整张表锁定,防止新增数据(oracle使用多版本数据的方式实现)。
(官方)Phantom Rows问题
在同一事务下,连续执行两次相同的SQL查询可能返回不同的结果,第二次可能会返回之前不存在的行,即该行是“幻像”行。
假设在子表的id列上有一个索引,并且您想从该表中读取并锁定id值大于100的所有行,以期稍后更新所选行中的某些列:
SELECT * FROM child WHERE id > 100 FOR UPDATE;
该查询从id大于100的第一条记录开始扫描索引。假设表包含id值分别为90和102的行。如果在扫描范围内对索引记录设置的锁定不锁定在索引范围内进行的插入间隔(在这种情况下,为90和102之间的间隔),另一个会话可以将一个新行插入ID为101的表中。如果您要在同一事务中执行相同的SELECT,则会看到一个新行,其中包含查询返回的结果集中的id为101(“幻影”)。如果我们将一组行视为数据项,那么新的幻影子级将违反事务应能够运行的事务隔离原则,以使已读取的数据在事务期间不会更改。
4.丢失更新
①第一类丢失更新(事务提交-->覆盖)
一个事务覆盖了另一个事务已更新的数据,导致后者所做操作丢失。(简而言之:两个事务同时更新,后更新的覆盖了先更新的数据)
下面的场景,支票事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元。相反,如果转账事务先提交,那么用户账户将损失100元。
②第二类丢失更新(事务回滚-->覆盖)
一个事务撤销时,把已提交的另一个事务的更新数据覆盖了。(简而言之:两个事务同时更新,后一个事务回滚,覆盖了先更新的数据)
下面的场景,A事务在撤销时,不小心将B事务已经转入账户的金额抹去了。
需要说明的是,在任何隔离级别下,都不会导致数据库理论意义上的丢失更新问题。这是因为,即使是READ UNCOMMITTED事务隔离级别,对于行的DML操作,需要对行或其它粗粒度级别的对象加锁。因此在上面的例子中,两种场景下事务B分别把余额改为900和改为1100的操作会被阻塞。
虽然说数据库层面能阻止丢失更新问题的产生,但是在应用程序中还是会出现逻辑意义上的丢失更新问题。比如:
1)事务T1查询到一行数据,放入本地内存,并显示给终端用户User1
2)事务T2也查询到该行数据,并将取得的数据显示给终端用户User2
3)User1修改了这行记录,更新数据库并提交。
4)User2修改了这行记录,更新数据库并提交。
这个过程,User1的修改更新操作就会被User2的修改操作覆盖,而造成丢失。要避免丢失更新的发生,需要将事务串行化,而非并行。
三、SQL标准定义的事务隔离级别
1.ANSI/ISO SQL92标准定义的4个事务隔离级别
如果不采取有效的隔离,多个事务并发执行就可能会出现前面提到的问题。ANSI/ISO SQL92标准定义了4个等级的事务隔离级别。虽然如此,但数据库厂商并未完全遵循这些标准。比如,Oracle就不支持READ UNCOMMITTED和REPEATABLE READ的事务隔离级别。
SQL标准定义的四个隔离级别如下表所示:
事务的隔离级别和数据库的并发性是对立的。一般来说,使用READ_UNCOMMITTED隔离级别的拥有最高的并发性和吞吐量,而使用SERIALIZABLE隔离级别并发性最低。
2.Oracle的事务隔离级别
ORACLE提供了SQL92标准中的read commited和serializable,同时提供了非SQL92标准的read-only。
- Read committed (读已提交)
- Serializable(序列化)
- Read only(只读)
Oracle使用多版本机制彻底解决了在非阻塞读时读到脏数据的问题并保证读的一致性,所以Oracle的READ COMMITED隔离级别就已经满足了SQL92标准的REPEATABLE READ隔离级别。
四、MySQL的事务隔离级别
官方文档:14.7.2.1 Transaction Isolation Levels
Mysql有如下四种事务的隔离级别
- Read uncommitted (读未提交):可以读到其它事务未提交的数据。最低级别,任何情况都无法保证。
- Read committed (读已提交):只能读到已经提交的数据。可避免脏读的发生。这是大多数数据库默认的隔离级别(mysql默认隔离级别为可重复读)。
- Repeatable read (可重复读):可以重复读。可避免脏读、不可重复读的发生。不能避免幻读。
- Serializable (串行化):最高的隔离级别。可避免脏读、不可重复读、幻读的发生。通过强制事务串行执行,避免了幻读的发生。
可能导致大量的超时和锁争用问题,所以实际应用中很少用到这个隔离级别。
下图中的可能/不可能表示是否可能发生。
读已提交(Read Committed)是大多数数据库默认的隔离级别,而MySQL默认的事务隔离级别为可重复读(Repeatable-read)。
可以通过以下方式来设置mysql事务的隔离级别:
//使用命令设置 SET SESSION|GLOBAL TRANSACTION ISOLATION LEVEL; //在mysql配置中设置 [mysqld] transaction-isolation=READ-COMMITED //查看[当前会话]的事务隔离级别 SELECT @@tx_isolation; //查看[全局]的事务隔离级别 SELECT @@global.tx_isolation;
1.读未提交(Read uncommitted)
SELECT语句以非锁定方式执行,但是可能会使用行的早期版本。因此,使用此隔离级别,读操作是不一致读,所以称为脏读。而与脏读相反的就是一致性读,包括了一致性锁定读和一致性非锁定读。
2.读已提交(Read committed)
即使在同一事务中,每个一致性读都将设置并读取其自己的新快照。
对于锁定读(使用FOR UPDATE或LOCK IN SHARE MODE进行锁定的读),UPDATE语句和DELETE语句,InnoDB仅锁定索引记录而不锁定它们之间的间隙,因此允许在靠近锁定记录的位置自由插入新记录。间隙锁定仅用于外键约束检查和重复键检查。
由于禁用了间隙锁定,因此可能会产生幻影问题,因为其他会话可以在间隙中插入新行。有关幻像的信息,请参见第14.7.4节“幻像行”。
READ COMMITTED隔离级别仅支持基于行的二进制日志记录。如果将READ COMMITTED与binlog_format = MIXED一起使用,则服务器将自动使用基于行的日志记录。
使用读已提交有些额外的影响:
- 对于UPDATE或DELETE语句,InnoDB仅对其更新或删除的行持有锁。MySQL评估WHERE条件后,将释放不匹配行的记录锁。这大大降低了死锁的可能性,但是仍然可能发生。
- 对于UPDATE语句,如果某行已被锁定,则InnoDB将执行半一致(semi-consistent)读,将最新的提交版本返回给MySQL,以便MySQL决定该行是否与UPDATE的WHERE条件匹配。如果该行匹配(必须被更新),MySQL会再次读取该行时InnoDB会对其进行锁定或等待对其进行锁定。
看看下面的实例。
CREATE TABLE t ( a INT NOT NULL, b INT ) ENGINE = InnoDB; INSERT INTO t VALUES (1,2),(2,3),(3,2),(4,3),(5,2); COMMIT;
在这种情况下,表没有索引,因此搜索和索引扫描使用隐式的聚集索引进行记录锁定,而不是使用索引列。
假设一个会话使用以下语句执行UPDATE:
# Session A START TRANSACTION; UPDATE t SET b = 5 WHERE b = 3;
紧接着,第二个会话使用以下语句执行UPDATE:
# Session B UPDATE t SET b = 4 WHERE b = 2;
当InnoDB执行每个UPDATE时,它首先会为读取到的每一行设置一个排他锁,然后决定是否对其进行修改。如果InnoDB不修改该行,它将释放锁定。否则,InnoDB将保留锁,直到事务结束。这会影响事务处理,如下所示。
(1).当使用默认的REPEATABLE READ隔离级别时,第一个UPDATE在它读取的每一行上获取一个x锁,并且不释放其中任何一个:
x-lock(1,2); retain x-lock x-lock(2,3); update(2,3) to (2,5); retain x-lock x-lock(3,2); retain x-lock x-lock(4,3); update(4,3) to (4,5); retain x-lock x-lock(5,2); retain x-lock
第二个UPDATE在尝试获取任何锁时立即阻塞(因为第一个更新已在所有行上保留了锁),并且直到第一个UPDATE提交或回滚后才继续进行:
x-lock(1,2); block and wait for first UPDATE to commit or roll back
(2).而当使用READ COMMITTED隔离级别时,则第一个UPDATE在它读取的每一行上获取一个x锁,并释放未修改的行的x锁:
x-lock(1,2); unlock(1,2) x-lock(2,3); update(2,3) to (2,5); retain x-lock x-lock(3,2); unlock(3,2) x-lock(4,3); update(4,3) to (4,5); retain x-lock x-lock(5,2); unlock(5,2)
对于第二个UPDATE,InnoDB进行“半一致性”读,将它读取的每一行的最新提交版本返回给MySQL,以便MySQL可以决定该行是否与UPDATE的WHERE条件匹配:
x-lock(1,2); update(1,2) to (1,4); retain x-lock x-lock(2,3); unlock(2,3) x-lock(3,2); update(3,2) to (3,4); retain x-lock x-lock(4,3); unlock(4,3) x-lock(5,2); update(5,2) to (5,4); retain x-lock
但是,如果WHERE条件包含索引列,并且InnoDB使用索引,则在获取和保留记录锁时仅考虑索引列。在下面的示例中,第一个UPDATE在b = 2的每一行上获取并保留一个x锁。第二个UPDATE在尝试获取同一记录上的x锁时会阻塞,因为它也使用在b列上定义的索引。
CREATE TABLE t ( a INT NOT NULL, b INT, c INT, INDEX (b)) ENGINE = InnoDB; INSERT INTO t VALUES (1,2,3),(2,2,4); COMMIT; # Session A START TRANSACTION; UPDATE t SET b = 3 WHERE b = 2 AND c = 3; # Session B UPDATE t SET b = 4 WHERE b = 2 AND c = 4;
使用READ COMMITTED隔离级别的效果与启用已弃用的innodb_locks_unsafe_for_binlog配置选项相同,但以下情况除外:
- 启用innodb_locks_unsafe_for_binlog是全局设置,会影响所有会话,而隔离级别可以为所有会话全局设置,也可以针对每个会话单独设置。
- 只能在服务器启动时设置innodb_locks_unsafe_for_binlog,而隔离级别可以在启动时设置或在运行时更改。
因此,READ COMMITTED比innodb_locks_unsafe_for_binlog提供更好,更灵活的控制。
3.可重复读(Repeatable read)
是InnoDB的默认隔离级别。同一事务中的一致读取将读取第一次读取建立的快照。这意味着,如果您在同一事务中发出多个普通(非锁定)SELECT语句,则这些SELECT语句彼此之间也是一致的。请参见第14.7.2.3节“一致的非锁定读取”。
对于锁定读取(使用FOR UPDATE或LOCK IN SHARE MODE进行锁定的SELECT),UPDATE和DELETE语句,锁定取决于该语句是使用具有唯一搜索条件的唯一索引还是范围类型搜索条件。
- 对于具有唯一搜索条件的唯一索引,InnoDB仅锁定找到的索引记录,而不锁定其前的间隙。
- 对于其他搜索条件,InnoDB使用间隔锁定或下一键锁定来锁定扫描的索引范围,以阻止其他会话插入该范围覆盖的间隔。有关间隙锁定和下一键锁定的信息,请参见第14.7.1节“ InnoDB锁定”。
4.串行化(Serializable)
最高的隔离级别。可避免脏读、不可重复读、幻读的发生。通过强制事务串行执行,避免了幻读的发生。可能导致大量的超时和锁争用问题,所以实际应用中很少用到这个隔离级别。在Serializable的事务隔离级别下,InnoDB存储引擎会对每个Select语句后自动加上LOCK IN SHARE MODE,即为每个读操作加一个共享锁。
此级别类似于REPEATABLE READ,但如果禁用了自动提交,则InnoDB会将所有普通的SELECT语句隐式转换为SELECT ... LOCK IN SHARE MODE。如果启用了自动提交,则SELECT是它自己的事务。因此,它被认为是只读的,如果作为一致性(非锁定)读取执行并且不需要阻塞其他事务,则可以序列化。(如果其他事务已修改所选行,则要强制普通SELECT阻止,请禁用自动提交。)
五、为什么Mysql默认隔离级别为可重复读(Repeatable Read)?
大多数数据库默认的隔离级别为读已提交(Read committed),而Mysql的默认隔离级别为可重复读(Repeatable Read)。这是为什么呢?实际上这是由于历史原因造成的。
READ-COMMITTED下主从复制数据不一致问题(mysql5.0)
Mysql主从复制是基于binlog复制的!binlog有三种格式,分别是:
- STATEMENT:记录的是修改SQL语句
- ROW:记录的是每行数据的变更
- MIXED:STATEMENT和ROW模式的混合
然而,Mysql在5.0版本的binlog只支持STATEMENT
这种格式!而这种格式在读已提交(Read Commited)隔离级别下会造成Mysql主从复制的主库和从库数据不一致现象。
例如,在READ-COMMITTED隔离级别下,数据库中已经有了以下数据。
接下来进行以下操作:
结果:最后在主库上查询得到结果b为3,在从库查询b为空,产生了数据不一致的问题。
分析:
会话1先锁定b=1,2,4,5这4行,然后会话2插入,因为2和4之间的间隙未被锁定,所以插入成功。之后会话1提交,将b=1,2,4,5四行删除,所以最后查询能查到b=3。
而由于binlog记录的语句的顺序与commit的顺序一致,所以在slave上会先执行插入,再执行删除,将所有行清空。因为mysql5.0只支持Statement的复制,所以在RC隔离级别下,就无法避免主库和从库就不一致的问题。因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!
在Mysql5.1版本中,READ-COMMITTED事务隔离级别默认只能工作在ROW模式下,如果工作在STATEMENT模式下则会报错,如下所示:
//创建表 CREATE TABLE a( b INT,PRIMARY KEY(b) )ENGINE=INNODB; //设置隔离级别为读已提交 SET @@tx_isolation='READ-COMMITTED' //插入数据 BEGIN; INSERT INTO a SELECT 1;
会有个警告,提示在READ-COMMITTED事务隔离模式下,基于STATEMENT模式的binlog是不安全。
Mysql在默认的Repeatable Read隔离级别下,使用了Next-Key Lock锁算法,避免了幻读的产生。所以InnoDB存储引擎在默认的Repeatable Read隔离级别下已经能完全保证事务的隔离性要求,达到了SQL标准的Serializable隔离级别。
据了解,大部分的人质疑SERIALIIZABLE隔离级别的带来的性能问题,但是根据JIM Gray在《Transaction Processing》一书中指出,两者的开销几乎是一样的,甚至SERIALIIZABLE性能可能更优!!!因此在InnoDB存储引擎中选择REPEABLE READ的事务级别并不会有任何性能的损失。同样地,即使使用READ COMMITTED的隔离级别,用户也不会得到性能的大幅度提升。 --------from《MySQL技术内幕.InnoDB存储引擎》第2版P331
六、互联网项目为什么将隔离级别设为读已提交(Read Commited)?
首先,项目中是不会用读未提交(Read Uncommitted)和串行化(Serializable)两个隔离级别。如果采用读未提交(Read UnCommitted),一个事务能读到另一个事务未提交读数据(脏数据),这显然是不合适。而序列化隔离级别会将事务串行执行,一般只有在分布式事务中才会采用,基于数据库的XA实现,因为是强一致性,所以互联网项目中的分布式解决方案一般也不用XA方案。排除了这两者后,就只剩下读已提交和可重复读了。
我们知道Mysql在读已提交隔离级别下,存在不可重复读和幻读问题,而在默认的可重复读隔离级别下,不可重复读和幻读问题都已经解决了,按理说我们在项目中直接采用默认隔离级别就可以了。但实际上,并不一定是这样的。
主要有以下几个原因
因为在不可重复读下不会使用间隙锁定,在默认隔离级别下会采用了间隙锁定,而间隙锁定可能会导致死锁问题。
1.间隙锁可能导致死锁问题
默认事务隔离级别下会使用间隙锁,而间隙锁会导致死锁问题。而在读已提交隔离级别不存在间隙锁定,所以不会因间隙锁而导致死锁。
例如,假设表结构和初始数据如下()
CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, `d` int(11) DEFAULT NULL, PRIMARY KEY (`id`), KEY `c` (`c`) ) ENGINE=InnoDB; insert into t values(0,0,0),(5,5,5),(10,10,10),(15,15,15),(20,20,20),(25,25,25);
现有如下操作(默认隔离级别)
我们按语句执行顺序来分析一下:
-
session A 执行select ... for update语句,由于id=9这一行并不存在,因此会加上间隙锁(5,10);
-
session B 执行select ... for update语句,同样会加上间隙锁(5,10),间隙锁之间不会冲突,间隙锁仅阻塞在间隙上的插入操作,因此这个语句可以执行成功;
-
session B 试图插入一行(9,9,9),被session A的间隙锁挡住了,只好进入等待;
-
session A试图插入一行(9,9,9),被session B的间隙锁挡住了。
至此,两个session进入互相等待状态,形成死锁。当然,InnoDB的死锁检测马上就发现了这对死锁关系,让session A的insert语句报错返回了。
2.RC隔离级别下的update语句,使用半一致性读,提前释放锁
半一致性读(semi consistent),是指在READ COMMITTED隔离级别下(或innodb_locks_unsafe_for_binlog被设为true时),对于update语句,当其读到一行被加锁的记录后,此时会读取记录最新的提交版本,然后Mysql上层会判断其是否会满足update语句的where条件,若满足则表明该行需要更新,则Mysql会再次发起一次读操作,此时会读取该行的最新版并加锁。另外,启用innodb_locks_unsafe_for_binlog配置也会达到相同的目的,但是mysql8.0已将该配置弃用。
下面是官方文档:InnoDB Locking 和 semi-consistent read。
使用READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog还有其他效果。MySQL评估WHERE条件后,将释放不匹配行的记录锁。
对于UPDATE语句,InnoDB进行“半一致”读取,以便将最新的提交版本返回给MySQL,以便MySQL可以确定该行是否与UPDATE的WHERE条件匹配。
一种用于UPDATE语句的读取操作,是READ COMMITTED和一致读取的组合。 当UPDATE语句检查已被锁定的行时,InnoDB会将最新的提交版本返回给MySQL,以便MySQL可以确定该行是否与UPDATE的WHERE条件匹配。 如果该行匹配(表示必须更新),则MySQL会再次读取该行,这一次InnoDB将其锁定或等待对其进行锁定。 仅当事务具有READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog选项时,才会发生这种类型的读取操作。在MySQL 8.0中删除了innodb_locks_unsafe_for_binlog。
极客时间:丁奇《Mysql实战45讲》-21讲:为什么我只改一行的语句,锁这么多? 小结部分,也提到了半一致性读。
极客时间:丁奇《Mysql实战45讲》-21讲:为什么我只改一行的语句,锁这么多 小结部分
在读提交隔离级别下还有一个优化,即:语句执行过程中加上的行锁,在语句执行完成后,就要把“不满足条件的行”上的行锁直接释放了,不需要等到事务提交(两阶段锁协议规定锁需要在事务提交时才释放)。
也就是说,读提交隔离级别下,锁的范围更小,锁的时间更短,这也是不少业务都默认使用读提交隔离级别的原因。
3.RC隔离级别下条件列未命中索引,会提前释放不符合条件行上的锁
假设表结构和数据如下
CREATE TABLE `test` ( `id` int(11) NOT NULL, `color` varchar(20) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB insert into test values(1,red); insert into test values(2,white); insert into test values(5,red); insert into test values(7,white);
先执行下列更新语句
update test set color = 'blue' where color = 'white';
在RR隔离级别下,由于使用非索引列来搜索,所以会进行全表扫描,因此将所有行包括间隙全部锁定。
而RC隔离级别下,同样会进行全表扫描,也会先将所有行锁定,之后会将不符合条件的行锁提前释放掉。
七、多版本并发控制(MVCC)和乐观并发控制(乐观锁)
需要说明的是,多版本并发控制(MVCC)和乐观并发控制(乐观锁)这是两个概念,可以参考这个问题:乐观锁和 MVCC 的区别?
多版本并发控制(MVCC)解决的是读-写并发问题:由于写操作会加锁,从而阻塞读,读要等待,读并发岂不是很低。怎么办?那就读快照吧,不进行阻塞等待。
乐观并发控制(乐观锁)解决的是写-写并发问题:传统悲观思想认为,后一个写操作会覆盖前一个写操作的结果,造成丢失更新,所以一定要加锁。乐观思想认为,当前写操作并不一定会有其他写操作来干扰。
1.乐观并发控制(乐观锁)
并发控制的主要技术有加锁、时间戳、乐观控制和多版本并发控制(MVCC)等。其中加锁机制是数据库最主要并发控制方法。乐观并发控制有以下几种实现方式:
时间戳排序法
是给每一个事务盖上一个时标,即事务开始执行的时间。每个事务具有唯一的时间戳,并按照这个时间戳来解决事务的冲突操作。如果发生冲突操作,就回滚具有较早时间戳的事务,以保证其它事务的正常执行,被回滚的事务被赋予新的时间戳并从头开始执行。
乐观控制法
加锁机制和时间戳排序是悲观的,因为当他们检测到一个冲突时,会强迫事务等待或回滚,即使该调度有可能是冲突可串行化的。
乐观控制法认为事务执行时很少发生冲突,因此不对事务进行特殊的管制,而是让它自由执行,事务提交时才会正式对数据的冲突与否进行检测。如果检查后发现该事务执行中出现过冲突并影响了可串行性,则拒绝提交并回滚该事务。
实现乐观锁一般来说有以下2种方式:
1)使用数据版本(Version)记录机制实现,这是乐观锁最常用的一种实现方式。
何谓数据版本?即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现。当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一。当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据。
2)使用时间戳(timestamp)
这种方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似,也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突。
2.多版本并发控制(MVCC)
官方文档:InnoDB Multi-Versioning和Consistent Nonlocking Reads
参考书籍:《数据库系统概论》第5版11.8小节 《高性能mysql》P13
MVCC是指在数据库中通过维护数据对象的多个版本信息来实现高效并发控制的一种策略。MVCC并没有一个统一的实现标准,不同的存储引擎的MVCC实现是不同的,典型的有乐观并发控制和悲观并发控制。
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现的,这两个列一个保存行的创建时间,一个保存行的过期时间(或删除时间)。当然存储的并不是实际的时间值,而是系统版本号。
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。其它两个隔离级别和MVCC不兼容,因为READ UNCOMMITTED总是读取最新的数据行,而不是当前事务版本的数据行。而SERIALIZABLE则会对所有的读取的行加锁(IN SHARE MODE)。
总结:
1.事务的ACID特性
2.事务的隔离级别?
3.mysql的默认事务隔离级别?为什么?
4.互联网项目如何设置事务隔离级别?为什么?
5.InnoDB存储引擎在默认隔离级别下,如何解决不可重复读问题?
使用MVCC机制,默认隔离级别下,读到的始终都是事务开始时的行快照,也就是说每次读取相同的行,也就解决了不可重复读问题。
网上以下几种说法,实际上都是说的通的。
- InnoDB是通过行锁解决不可重复读问题的。(实际上MVCC可以看作是行锁的一个变种,这里的行锁是指通过版本号实现的。)
- InnoDB是通过版本控制解决不可重复读问题的
- InnoDB是通过MVCC解决不可重复读问题的
5.InnoDB存储引擎在默认隔离级别下,如何解决幻读问题?
使用间隙锁,将间隙锁住,就能阻挡其它事务在间隙上的插入操作,解决幻读问题。
6.事务的4个特性是如何实现的?
事务的隔离性是通过锁来完成的,而原子性、一致性和持久性是通过数据库的redo log和undo log来完成的。
7.InnoDB在读已提交和可重复读隔离级别下,使用一致性非锁定读,即读不会被阻塞,会读取行快照。
读已提交:读快照,读取的是快照的最新版本
可重复读:读快照,读取的是事务在开始时的快炸版本(第一次select时)。
8.其它:
假设t表中有id,name,age三个字段,id为主键,name上建立了索引。在默认隔离级别下,有以下查询语句。
#id是主键 update t set age=20 where id=5; #会通过主键索引来查找,如果命中id=5这行,则会为该行加Record lock。如果没有命中,假设表中存在id=3和id=6的行,则对(3,6)加间隙锁。 #name加了索引 update t set age=20 where name='zhangsan'; #会通过age上的索引来查找,最后还要回表在主键索引上查找。【???暂时不清楚聚集索引上是如何加锁的???】 #age没加索引 update t set name='lisi' where age=18; #没利用索引来查找,所以走全表扫描,会锁定所有的行