概述
数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。
这些问题的本质就是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。
事务
事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。
| 属性 | 描述 |
|---|---|
| 原子性(Atomicity) | 事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。 |
| 一致性(Consistent) | 在事务开始和结束时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。 |
| 隔离性(Isolation) | 数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行,这意味着事务处理过程中的中间状态对外部是不可见的。 |
| 持久性(Durable) | 事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。 |
并发事务处理带来的问题
脏写(更新丢失 Lost Update)
当两个或多个事务选择同一行数据,然后基于最初选定的值修改这一行时,由于每个事务都不知道其它事务的存在,就会发生丢失更新问题,最后的更新覆盖了其它事务所做的更新。
脏读(Dirty Reads)
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做 “脏读”。
简单理解:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。
不可重复读(Non-Repeatable Reads)
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做 “不可重复读”。
简单理解:事务A内部的相同的查询语句,在不同时刻读出的结果不一致,不符合隔离性。
幻读(Phantom Reads)
一个事务按照相同的查询条件重新读取以前检索过的数据, 却发现其它事务插入了满足其查询条件的新数据, 这种情况称为 “幻读”。
简单理解:事务A读取到了事务B提交的新增数据,不符合隔离性。
事务隔离级别
“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| 读未提交(ru) | 可能 | 可能 | 可能 |
| 读已提交(rc) | 不可能 | 可能 | 可能 |
| 可重复读(rr) | 不可能 | 不可能 | 可能 |
| 可串行化 | 不可能 | 不可能 | 不可能 |
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。
同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对"不可重复读"和"幻读"并不敏感,可能更关心数据库并发访问的能力。
查看当前数据的事务隔离级别:show variables like 'transaction_isolation';
设置事务隔离界别:set transaction_isolation = 'REPEATABLE-READ';
mysql默认的事务隔离级别是可重复读,用spring开发程序时,如果不设置隔离级别,默认使用mysql设置的隔离级别,如果spring设置了就用spring设置的隔离级别。
事务隔离级别验证
读未提交
设置隔离级别:
set transaction_isolation = 'read-uncommitted';
验证:事务A读取到了事务B已经修改但尚未提交的数据,产生了脏读
结论:无MVCC机制,不用readview,所有语句都不加锁
-
客户端A:开启事务表数据

-
客户端B:更新account表数据,但未提交事务

-
客户端A:再次查询表数据,发现已经能够查到客户端B事务修改但还未提交的数据(
如果客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据)
读已提交
设置隔离级别:
set transaction_isolation = 'read-committed';
验证:事务A只能读取事务B已经修改并提交的数据,解决了脏读,但产生了不可重复读
结论:有MVCC,每次select读的都是一个新的视图,所以会产生不可重复读
-
客户端A:开启事务,查询表数据

-
客户端B:更新account表数据,未提交事务,此时客户端A已经不能查询B未提交的数据,解决了
脏读
-
客户端B:提交事务

-
客户端A:再次查询表数据,能够查询到其它事务修改提交的数据,但是在客户端A事务中前后查询的内容不一致,产生了
不可重复读的问题,不符合事务的隔离性。
可重复读
设置隔离级别:
set transaction_isolation = 'repeatable-read';
验证:事务A内部相同的查询语句,在不同时刻读出的结果一致,不受其它事务提交的数据影响,解决了不可重复读,但未解决幻读
结论:有MVCC,每次select读的都是同一个视图(快照、历史版本),解决了不可重复读,insert、update、delete操作会更新版本号,读的是最新版本,所以会产生幻读
-
客户端A:开启事务,查询表数据

-
客户端B:更新account表数据,并提交事务

-
客户端A:再次查询表数据,发现客户端A前后读的数据一致,不受客户端B修改提交的数据所影响,解决了
不可重复读,但没有解决幻读
-
客户端B:新增一条记录,客户端A修改新增的数据,再次查询,发现新增的数据能够读出来
(验证幻读)
可串行化
设置隔离级别:
set transaction_isolation = 'serializable';
验证:解决幻读问题
结论:无MVCC,不用readview,select读的时候会将读出来的数据加锁,其它事务不能操作这些数据,会阻塞等待,
-
客户端A:开启事务,查询表数据

-
客户端B:修改或新增数据,发现都会被阻塞等待,因为在serializable模式下,客户端A事务里面所查询的数据都被加了锁,所以就避免了
幻读,这种隔离级别并发性极低,开发中很少会用到。
评论区