概述
我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能 就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了
事务隔离机制、锁机制、MVCC多版本并发控制隔离机制
,用一整套机制来
解决多事务并发问题。
事务及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID。
原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
实现方式:通过undo日志实现。
一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规 则都必须应用于事务的修改,以保持数据的完整性。(可以理解为抽象出来的内容同现实内容的相互映射关系要一致)。
C是目的,其他三个AID属性是手段。
隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独
立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
通过读写锁+MVCC机制实现的。
持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。
通过redo日志实现
(例如WAL预写式日志)
并发事务处理带来的问题
更新丢失(Lost Update)或脏写
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存 在,就会发生丢失更新问题–最后的更新覆盖了由其他事务所做的更新
。
脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这 时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的 处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
事务A读取到了事务B已经修改但尚未提交的数据
,还在这个数据基础上做了操作。此时,如果B 事务rollback,A读取的数据无效,不符合一致性要求。
不可重读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改 变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性
幻读(Phantom Reads)
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务
插入
了满足其查询条件的新数 据,这种现象就称为“幻读”。
事务A读取到了事务B提交的新增数据,不符合隔离性
Mysql默认的事务隔离级别是RR,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔 离级别,如果Spring设置了就用已经设置的隔离级别 ,通过MVCC机制,个事务之间读取到的数据都是不会变动的,但如果sql执行语句有对已经更改的数据进行了sql层面运算,会自动取目前最新的数据来进行运算;通过Gap-Lock机制,
一定程度的解决了幻读问题。但一般底层还是能感知到数据的变动,并未完全解决幻读问题。RR隔离级别相对于串行化隔离级别,效率更高。
# 查看当前数据库的事务隔离级别
show variables like 'tx_isolation';
# 设置事务隔离级别
set tx_isolation='REPEATABLE-READ';
锁 (锁是计算机协调多个进程或线程并发访问某一资源的机制)
如何保证数据并发访问的一致性,有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的重要因素
锁分类
1.从性能上分为乐观锁(用版本对比来实现)和悲观锁(一旦有加锁,其他的访问阻塞等待)
2.
从对数据库操作的类型分,分为
读锁和写锁
(都属于悲观锁);
读锁(共享锁,S锁,Shared):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排他锁,X锁,eXclusive):当前写操作没有完成前,会阻断其他写锁和读锁
3.从对数据操作粒度分,分为表锁和行锁。
表锁:
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低; 一般用在整表数据迁移的场景。MyISAM存储引擎就是用的表锁且不支持事务(
对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当 读锁释放后,才会执行其它进程的写操作。 对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进 程的读写操作)
行锁:
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最 高;InnoDB支持事务(TRANSACTION),支持行级锁。(
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
)
锁总结:
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
简而言之,就是
读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞
。
事务隔离级别与行锁分析
读未提交:
set tx_isolation='
read-uncommitted
';
读已提交:set tx_isolation='read-committed';
3.这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,
解决了脏读问题
:
可重复读(MySQL默认隔离级别) :set tx_isolation='repeatable-read';
串行化:set tx_isolation='serializable';
表面上看起来是多并发,但是一旦多个事务操作的数据有相同的,就串行处理事务,阻塞等待。
间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的空隙。Mysql默认级别是repeatable-read,在某些情况下可以解决幻读问题。
临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。
无索引行锁会升级为表锁:
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
session1 执行:update account set balance = 800 where name = 'lilei';
session2 对该表任一行操作都会阻塞住
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为
表锁
。
如下图所示,此时已经升级为了表锁,在事务提交或者回滚之前,整张表无法进行其他的写操作。
锁定某一行还可以用lock in share mode(共享锁) 和for update(排它锁),例如:select * from
test_innodb_lock where a = 2 for update; 这样其他session只能读这行数据,修改则会被阻塞,直到锁定 行的session提交;
结论
Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更 高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb 的整体性能和MYISAM相比就会有比较明显的优势了。
但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现 不仅不能比MYISAM高,甚至可能会更差。
死锁:
大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁
锁优化建议
尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
合理设计索引,尽量缩小锁的范围
尽可能减少检索条件范围,避免间隙锁
尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
尽可能低级别事务隔离