概述
我们的数据库一半都会并发执行多个事务,多个事务可能会并发的对想通的一批数据进行增删改查等操作,这样就会存在脏写、脏读、不可重复读、幻读等问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制解决事务并发问题,接下来,我们深入探讨一下这些机制,理解数据库内部的执行原理。
事务及其ACID属性
事务是由一组SQL语句组成的逻辑处理单元,事务具有一下4个属性,通常简称为事务的ACID属性。
原子性(Atomicity):事务是一个原子操作单位,其对数据的修改,要么全部执行(成功),要么全部不执行(失败)。
一致性(Consistent):在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则必须应用于事务的修改,保持数据的完整性。
隔离性(Isolation):数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。意味着事务处理过程中的中间状态对外部是不可见的。
持久性(Durable):事务完成之后,对于数据库的修改是永久的,即使出现故障也能保存。
为什么说出现故障也能保存,其实这是事务在执行过程中会在磁盘中顺序写入redo文件,会把修改记录保存下来,即使宕机,修复好后还可以通过redo日志文件写入磁盘,这个具体的操作原理后续再讲。
事务并发处理带来的问题
更新丢失或脏写
当两个或多个事务选择同一行,然后基于最初选定的值更新改行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后到更新覆盖了由其他事务所做的更新
脏读(Drity Reads)
一个事务正在对一条记录做修改,在这个事务完成提交前,这条记录的数据就处于不一致的状态,这时另一个事务也来读取同一条记录,如果不进行控制,第二个事务读取了这些“脏”数据,并对此数据进行了处理,就会产生未提交的数据依赖关系。这种现象就叫“脏读”。
一句话解释:事务A读取到了事务B已经修改但尚未提交的数据,并在这个数的基础上做了操作,如果事务B事务回滚,事务A读取的数据无效,不符合一致性要求。
不可重复度(Non-Repeatable Reads)
一个事务在读取数某些数据后的某个时间,再次读取以前读过的数据,却发现其°到数据已经发生了改变,或者某些记录已经被删除了,这种现象叫做“不可重复读”。
一句话解释:事务A内部相同查询语句在不同时刻堵出的结果不一致,不符合隔离性。
幻读(Phantom Reads)
一个是我按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象叫做“幻读”。
一句话解释:事务A读取到了事务B提交的新增数据,不符合隔离性。
事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库一致性问题,必须由数据库提供一定的食物隔离机制来解决。
隔离级别 | 脏读 | 不可重复度 | 幻读 |
---|---|---|---|
读未提交 (Read-Uncomitted) | √ | √ | √ |
读已提交 (Read-committed) | × | √ | √ |
可重复读 (RepeatableRead) | × | × | √ |
串行 (Serializable) | × | × | × |
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问能力。
Mysql默认的事务隔离界别是可重复读,用spring开发程序时,如果不设置隔离级别,默认用Mysql设置的合理级别,如果Spring设置了就用已经设置的隔离级别。
事务隔离级别演示
查看当前数据库的事务隔离级别:show variables like 'tx_isolation';
设置事务隔离级别:set tx_isolation='REPEATABLE-READ';
创建表:
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `account` (`name`, `balance`) VALUES ('lucy', '2400');
读未提交
初始数据:
演示脏读:
1.事务A将id=1的数据balance设置为0并且不提交数据。
2. 事务B读取id为1的数据
3.由于某种原因事务A需要回滚,并提交数据,此时数据库数据id=1的数据应该还是450
4. 此时事务B用查到的数据做balance加500的操作,事务B读取到的balance为0,加500等于500,并提交数据,此时数据库由于事务A回滚,实际balance=450,那么事务B读的其实就是脏数据了,用脏数据操作数据库,不满足一致性
读已提交
初始化数据:
演示不可重复读
1. 事务A查询id=1的数据,不提交事务
2. 事务B修改id=1的数据,执行balance=banlance-50的操作,并提交事务
3. 事务A继续读取id=1的数据,同一个事务中读取相同的数据,读取到的数据不一致,数据不可重复读取
该隔离级别只能读取到已经提交了的事务,解决了脏读问题。
可重复读
初始数据:
演示幻读:
1.事务A读取account表所有的数据,不提交事务
2. 事务B对id=1的数据修改:balance=balance-50,并提交事务
3. 事务A继续读取account的数据,和第一次读取的数据是一致的,和事务B的数据具有隔离性,是因为在事务开始时数据库相当于对当前事务的当前时间节点数据进行了快照(机制相当于快照,实际不是快照,后续会讲),所以数据可以重复读
4. 事务B开启新的数据,新增一条数据并提交事务
5. 事务A继续读取account表数据,数据没变
此时,就会存在疑问,上述降到可重复读隔离级别不是会有幻读的情况,幻读是只事务A读取到事务B提交的新增的数据,这也没有啊,是不是可重复读不会造成幻读啊,接下来我们验证幻读。
验证幻读
6. 事务A执行更新操作,将id=4的数据:balance=balance+500,然后查询数据
事务b提交的数据可以查询到了,满足幻读。
串行化
初始数据:
1. 事务A查询id=1的数据,不提交事务
2. 事务B修改id=1的数据,无法修改,因为事务A没提交,进行锁等待
3. 提交事务A
4. 再次执行事务B的修改操作,可以修改成功。
这种隔离级别并发性很低,开发中极少会用到。