数据库中通常会存在并发执行多个事务的情况,而这些并发的事务如果操作的有相同的数据,就会导致脏写、脏读等问题。MySQL为了解决数据库并发事务导致的脏写、脏读、不可重复读、幻读等问题,设计了事务隔离等级、锁机制和MVCC(多版本并发控制)机制,通过一整套的机制来解决多事务并发问题。
一、数据库事务
1.1 事务特性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下四大特性,称为事务的ACID特性:
- 原子性:事务是一个原子操作单元,其内部的SQL语句要么全部执行成功,要么都不执行
- 一致性:在事务开始和完成时,数据都必须保证一致状态。所有相关的数据规则都应用于事务的修改,以保证数据的完整性。比如:银行卡转账操作,操作前后,两个银行卡账户的总额应该是一致的。
- 隔离性:事务之间的操作是隔离的,某个事务处理过程的中间状态对外部是不可见的,同时外部状态对该事务也是不可见的。
- 持久性:事务完成之后,对数据的修改是永久性的,即使出现系统故障也能够保证数据不丢失。
1.2 并发事务问题
多个事务同时执行时,难免会出现一些事务的并发问题,主要包括下面几种情况:
脏写
当多个事务选择同一行进行更新或删除操作,由于每个事务都不直到其他事务的存在,就导致最后的操作覆盖了前面其他事务的操作
脏读
一个事务对某条记录做了修改,但该事务还没有提交,在事务提交之前,数据处于不一致状态;这时有另外一个事务也读取到了该记录,此时拿到的就是“脏数据”。简单来说就是事务A读取到了事务B已修改但未提交的数据,还在已修改的基础上做了进一步操作。如果此时事务B回滚,事务A之前读取到的就是无效的数据,不符合一致性要求。
不可重复读
一个事务在事务内的不同时间,执行了相同的查询语句,而得到的结果却发生了变化,这种现象就是“不可重复读”,不符合隔离性。
幻读
事务A读取到了事务B提交的新增数据,不符合隔离性。这里说的事务A读取并不是简单的快照读,而是insert/update/delete操作中的读
注意:不可重复读,侧重的是相同的读操作,也就是两次读操作的结果不一致。而幻读侧重于先读,后写,比如同时开始了事务A和事务B,然后事务B中插入了一条id=5的记录,而在事务A中查询时,发现没有该记录,于是就也插入一条id=5的记录,但事务A插入失败了。这才是幻读的真正场景。
二、事务隔离级别
脏读、不可重复读、幻读都是数据库的读一致性问题,可以通过一定的事务隔离机制来控制,InnoDB存储引擎提供了四种隔离级别,这四种隔离级别与并发问题的关系如下表所示:
隔离级别 | 脏读(DirtyRead) | 不可重复读(Norepeatable Read) | 幻读(Phantom Read) |
---|---|---|---|
读未提交(Read uncommitted) | 可能 | 可能 | 可能 |
读已提交(Read committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable read) | 不可能 | 不可能 | 可能 |
可串行化(Serializable) | 不可能 | 不可能 | 不可能 |
读已提交采用MVCC机制实现,可重复读采用MVCC+锁来实现,而串行化是通过对所有操作加锁(包括读操作)来实现。
注意:事务的隔离等级越严格,并发的问题就越少,但付出的代价也就越大;因为事务隔离实质上就是使事务在一定程度上“串行化”。同时不同的应用对事务的读一致性和事务隔离等级也是不同的,比如很多应用对“不可重读”和“幻读”并不敏感,但更关注数据并发访问的能力
查看当前数据库事务等级:
show variables like 'tx_isolation';
设置数据库事务等级:
set tx_isolation='REPEATABLE-READ';
Mysql默认的事务隔离级别是可重复读
三、数据库中锁
在数据库中,数据也是一种共享资源。保证数据并发访问的一致性、有效性是所有数据库都需要解决的一个问题,而锁冲突也是影响数据库并发访问性能的一个重要因素。
3.1 锁分类
3.1.1 从性能上
乐观锁和悲观锁,乐观锁通过版本比对实现,不需要加锁阻塞,而悲观锁需要进行线程阻塞。
3.1.2 从数据操作类型上
读锁、写锁和意向锁,而意向锁又分为意向读锁和意向写锁。
-
读锁
读锁属于共享锁(Shared),针对同一份数据,多个读操作可以同时进行而不会相互影响,而对于写操作,则会进行阻塞。
读锁通常出现在一致性锁定读
的情况下,通过下面的SQL可以获取一把共享锁:select ... Lock in share mode
-
写锁
写锁属于排他锁(eXclusive),当前事物的写操作没有完成前,其他事务不能对锁定行加任何锁,加锁会被阻塞。
再一致性锁定读
中,可以通过下面的SQL加排它锁:select ... for update;
再
可重复读
的事务隔离级别下,为了解决不可重复读,对数据的修改操作,也会加排它锁,阻塞其他事务中的修改。 -
意向锁
在Innodb存储引擎中,意向锁是一个表级别的锁,而它又分为两种:
1、意向共享锁(IS Lock):事务想要获得一张表中某几行的共享锁
2、意向排它锁(IX Lock):事务想要获得一张表中某几行的排它锁
Innodb引擎支持的是行级别的锁,因此意向锁虽然是表锁,但它并不会阻塞除全表扫描以外其他的任何请求。
那为什么需要设计意向锁?(个人理解)
比如事务A需要加表锁,那么它就需要判断表中的每一行记录是否有锁。而引入意向锁后,其他事务在加行锁之前,需要先获取意向锁,比如对行加读锁,就需要先获取表的意向读锁。此时事务A再来加表锁时,就会判断该表上是否有意向锁,有意向锁的情况下,就不能加表锁了。
3.1.3 从数据操作粒度上
行锁、页锁和表锁,在InnoDB中,页锁并不支持,这里只是做一个比较。
-
行锁
每次操作锁定一行数据。开销大,加锁慢;会出现死锁;但是锁粒度最小,发生锁冲突的概率最低,并发度最高。
-
页锁
在意向锁中体现页锁的概念,在意向锁中,如果对一个记录进行加锁,则需要对粗粒度的对象进行加锁,依次是数据库加锁、数据表加锁、数据页加锁和记录加锁。
但Innodb的意向锁实现比较简介,其意向锁即为表级别的锁。 -
表锁
每次操作需要锁住整张表,开销小,加锁快;不会出现死锁,锁粒度大,发生冲突的概率最高,并发度最低;一般用在数据迁移的场景。
注意:这里说表锁不会出现死锁,应该是针对MyISAM存储引擎说的,该引擎不支持事务,自然不会死锁。但在Innodb引擎中,是有可能会出现表锁的死锁的(个人观点)
手动增加表锁:lock table 表名 read(write);
查看表上是否有锁:
show open tables;
删除表锁:
unlock tables;
对MyISAM表的读操作(加读锁) ,不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作。
在InnoDB引擎中,如果加了表锁后,并不是需要一直等到事务执行完才释放,而是每比对完成一行记录,就释放该记录的锁。
3.1.4 从锁范围上
间隙锁(Gap Lock)和临建锁(Next-key Locks)。
-
间隙锁(Gap Lock)
间隙锁,顾名思义,锁的就是两个记录之间的间隙,但不包含记录本身
以account
这个表记录为例:
间隙就有(3,15)、(15,20)、(20,∞)
这三个间隙
现在事务A要执行下面的SQL:update account set name = 'lizhi' where id >10 and id < 18;
上面的SQL跨越了上面前两个区间,所以其他事务在(3,20]整个区间内,不能插入或修改任何数据,最后的20也是包含在内的。
所以,在可重复读
的事务隔离级别下,间隙锁在某些情况下,可以解决幻读的问题。
注:间隙锁只有在可重复读隔离界别下才生效 -
临建锁(Next-key Locks)
Next-key Locks
其实是行锁于间隙锁的组合。像上面间隙锁的例子中,(3,20]的整个区间可以叫做间隙锁。
在无索引的情况下,行锁可能会升级为表锁。(可重复读隔离级别会升级为表锁,读已提交隔离级别不会升级为表锁)
在Innodb引擎中,行锁一般都是加在索引上,如果对非索引字段进行修改,可能就会导致行锁升级为表锁。
比如,事务A执行下面的代码:update account set balance = 9000 where name = 'lizhi';
其他所有事务对该表的操作都会被阻塞
注:InnoDB中的行锁是针对索引加的锁,不是针对记录加锁。并且索引不能失效,否则行锁就会变为表表锁
3.2 行锁分析
通过检查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: 系统启动到现在总共等待的次数
对于这五个状态变量,比较重要的是:平均等待时长、等待总次数、等待总时长
尤其是当等待次数很高,而且每次等待时长也不小的时候,就需要分析系统中为什么会出现如此多的等待。
3.3 查看锁信息
查看INFORMATION_SCHEMA
系统库锁相关数据表
-- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
-- 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看锁等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id
-- 查看锁等待详细信息
show engine innodb status;
3.4 锁优化
对于锁优化的建议:
1、尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
2、合理设计索引,尽量缩小锁的范围
3、尽可能减少检索条件范围,避免间隙锁
4、尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的SQL尽量放在事务最后执行
5、尽可能使用低级别事务隔离
四、行锁与事务隔离级别分析
4.1 读未提交
现在有事务A和事务B两个事务,事务A将balance
从450改为400,事务B可以看见balance
改了400,出现了脏读。
此时,事务A回滚了,而事务B再将balance
减去50,按照我们的理解,读取的是400,再减去50,结果应该是350。
但结果依然是400,因为事务A回滚后,数据库中balance
的值仍然为450,而事务B在进行更新是,会从数据库中取出最先的数据再进行更新。
4.2 读已提交
事务A和事务B,事务A将balance
从450改为了400,但此时事务B中查看到的balance
依然是450,脏读被解决了。事务A提交后,事务B再次查询,发现balance
变成了400,事务B中两次相同的查询,却得到了不同的结果,说明依然存在“不可重复读”问题。
4.3 可重复读
事务A和事务B,事务A将balance
从450改成了400,而事务B在事务A提交前后,查询到的balance
值都是450,说明可重复读解决了脏读和不可重读的问题。接着事务B将balance
再减去50,发现最终的结果为350,这是因为在可重复读的隔离级别下,使用MVCC(一致性不加锁读)机制,select操作不会更新版本后,读的是历史版本数据。但insert、update、delete操作会更新版本好,是当前读。
重新打开事务A,然后插入一条新的id=4
数据,但是在事务B中,并不能看到新插入的记录,此时,事务B也插入一条id=4
的记录却失败了,这就表明幻读现象依然存在。
4.4 可串行化
事务A和事务B,事务A查看id=1
的记录,然后事务B更新id=1
的记录时,则会处于等待状态,在可串行化的隔离等级下,读也会加锁。
如果事务A执行的是一个范围查询,那么该范围所有的行,包括所有行的间隙都会被加锁,此时事务B在想插入或修改数据,都会被阻塞,所以就避免了幻读。
注:上面的几个例子可以通过开启两个MySQL客户端,设置不同的事务隔离级别,然后同时开启两个事务来进行验证,这里就不把具体的截图展示出来。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/153600.html