ACID 是事务的四大特性(Atomicity 原子性
、Consistency 一致性
、Isolation 隔离性
、Durability 持久性
),其实主要是事务具备隔离性,隔离性能够保证各个线程在运行期间不会相互影响,独自完成各自的事情,有了隔离性也就能保证程序的原子性,就可以让一个线程相当于串行的执行业务逻辑,有了事物的原子性,从而可以保证数据的一致性,最后把一致性数据以一种B+Tree
数据结构持久化到磁盘,这就是事物四大特性,可以看见隔离性是个重量级单位,今天我们就来分析下 I (Isolation) 也就是事物的4种隔离级别。
1、原子性(Atomicity)
一个原子事务要么完整执行,要么干脆不执行。这意味着,工作单元中的每项任务都必须正确执行。如果有任一任务执行失败,则整个工作单元或事务就会被终止。即此前对数据所作的任何修改都将被撤销。如果所有任务都被成功执行,事务就会被提交,即对数据所作的修改将会是永久性的。
2. 一致性(Consistency)
一致性代表了底层数据存储的完整性。它必须由事务系统和应用开发人员共同来保证。事务系统通过保证事务的原子性,隔离性和持久性来满足这一要求; 应用开发人员则需要保证数据库有适当的约束(主键,引用完整性等),并且工作单元中所实现的业务逻辑不会导致数据的不一致(即,数据预期所表达的现实业务情况不相一致)。例如,在一次转账过程中,从某一账户中扣除的金额必须与另一账户中存入的金额相等。
3. 隔离性(Isolation)
隔离性意味着事务必须在不干扰其他进程或事务的前提下独立执行。换言之,在事务或工作单元执行完毕之前,其所访问的数据不能受系统其他部分的影响。
4. 持久性(Durability)
持久性表示在某个事务的执行过程中,对数据所作的所有改动都必须在事务成功结束前保存至某种物理存储设备。这样可以保证,所作的修改在任何系统瘫痪时不至于丢失。
四种隔离级别依次向下隔离(其实就是隔离线程间操作)性越强!
# MySQL5.7 版本之前的使用以下命令
select @@tx_isolation;
# MySQL5.7 (新)版本之后的使用以下命令
select @@transaction_isolation;
注意高版本(MySQL5.7 之后)才用,命令如下:
# 设置读未提交
set session transaction isolation level read uncommitted;
# 设置读已提交
set session transaction isolation level read committed;
# 设置可重复读
set session transaction isolation level repeatable read;
# 可串行化
set session transaction isolation level serializable;
session
级别的设置只针对当前窗口设置有效,全局设置可以改成 global
即可。另外一种修改方式看下面:
SET @@gloabl.tx_isolation = 0;
SET @@gloabl.tx_isolation = 'READ-UNCOMMITTED';
SET @@gloabl.tx_isolation = 1;
SET @@gloabl.tx_isolation = 'READ-COMMITTED';
SET @@gloabl.tx_isolation = 2;
SET @@gloabl.tx_isolation = 'REPEATABLE-READ';
SET @@gloabl.tx_isolation = 3;
SET @@gloabl.tx_isolation = 'SERIALIZABLE';
具体哪种设置有用可以试试就知道了。
# 查看是自动还是手动提交
show variables like 'autocommit';
# 设置为 0/false 表示手动提交,1/true 表示自动提交
set autocommit=0;
这里使用的是 begin-rollback-commit
命令级别的测试,因为有时这个自动提交设置过了一会又会变成 on
开启状态,不方便测试。
分析之前要有一点意识,就是隔离性针对的肯定是多线程情况下来分析的,因为多线程并发才会引发数据安全性。现在来演示一下什么是读未提交,演示前首先要修改数据库默认隔离级别(REPEATABLE-READ
),修改成读未提交,然后再打开两个 session
窗口,并且把事物设置成手动提交
,或者 begin-rollback-commit
命令也行,提前做好了这两个准备接下开始分析什么是读未提交
?
打开两个 session
窗口,线程A、B分别对不同的窗口执行 SQL 语句,如图示:
窗口1每次都去修改 id=1 的 account
账户,而且没有提交,或者是操作回滚了,对于窗口2线程B去查询都是可以看得到,线程A已经对 id=1 的账户充钱和扣钱,线程B也能读取到数据,对于线程B是非常不友好的,这个就是线程B读取到脏数据了,也就是我们常说的脏读
,那么如何规避这个问题呢?可以把线程的隔离级别设置成读已提交
级别。
上述读未提交会产生脏读,然后通过升级隔离级别进行规避,通过命令修改隔离级别之后,然后看图示,如下:
成功修改了隔离级别,然后先执行以下两个窗口的查询操作,如下图是:
可以看到都是 account=30
,现在让窗口1把 id=1 的 account
更新成 100,窗口1先不提交,然后窗口1、2先查询下 account 值为多少,如下图:
很明显窗口2读取到的值还是之前原来的 30,也就是说窗口1只要不提交,你就不会影响我窗口2的数据,然后提交窗口1执行提交操作,在去查看两个窗口的 account
值,如下图示:
发现窗口2的 account
值从 30 变成了 100,因为窗口1提交了修改操作,现在问题又来了,窗口2前后两次读取的 account
值明显不一样,那么值钱的业务逻辑是不是就白做了呢,很扯淡,所以这里就又引发了一个问题,读取数据前后不一致,这就是数据不可重复读问题。那么怎么解决呢?还是一样的方式,升级隔离级别,升级为可重复读级别
上述读已提交隔离级别,会造成数据不可重复读问题,那么现在升级隔离级别为可重复读,如图示:
升级好隔离级别后,同样的操作再来一遍,先执行窗口1、2的查询操作,查看账户的值是多少?如下图所示:
发现查询出来的 account
值都是为 100,现在窗口1,把 account
更新成 200,先不提交,然后两个窗口执行查询操作,如下图所示:
发现此时窗口1查询出来的结果为 200, 窗口2结果为100,现在开始让窗口1开始执行提交操作,然后窗口1、2在此查询结果,如下图示:
窗口1虽然执行了提交操作,但是窗口2查询到的值还是上一次查询到的 100 的值,这就让它在一个事物中数据是可以重复读取的,那么什么时候窗口2可以读取到最新的 200 的值呢? 只能运行完此次事物,也就是提交完本次事物就可以读取到最新的值 200,好,现在窗口2执行提交操作,然后再去查询一下 account
的结果,如下图示:
可以看到此时的窗口2可以读取到 200 的值。
至于串行化,其实就是有窗口1再执行操作,其他窗口不能执行,只能阻塞等待窗口1执行提交其他窗口才能够执行,其实就是相当于 Java 中的同步锁 synchornized
。
升级隔离级别为串行化,如下图示:
然后让同样的操作再来一遍,窗口1、2先查看 account
的值是多少?如下图示:
查询到 account
都是为200,现在让窗口1开始修改 account
值 修改成 500,先不要提交,然后两个窗口都去执行查询操作,如下图示:
可以看到窗口1值已经修改成了500,窗口2查询 id=1 的记录,已经被窗口1锁住了,现在对 id=1 记录的任何操作都不能执行,只有等待窗口1 把锁释放,也就是执行提交操作,如下图示,窗口2的查询操作将被会阻塞住,因为窗口1没有执行提交操作,如下图示:
可以看到此时窗口2一直在转圈圈,以为窗口2拿不到这行数据的行锁,所以不能操作,只有等窗口1提交执行完就可以执行成功,那么现在让窗口1执行提交操作,如下图示:
会发现窗口1一执行完提交操作,窗口的查询就能查询出来数据,这还只是一个查询操作,都要等待这么久,那么其他的更新操作岂不完蛋,所以串行化的隔离级别太强了,保证了数据的高安全性,但是同事性能也是最低的,同步锁就是这样,一次性操作只能有一个线程操作,不管你是读还是写。
上述需要注意窗口1只是锁住了 id=1 的行记录,对于其他的行是可以操作的,因为 id =1 的行记录被加了行级锁,而且这把行级锁还是一把独占式的排它锁(X锁)
,至于为什么会加锁,因为 id=1 走了索引,MySQL 数据库是会对走了索引的数据加行锁,否则都是表锁。
以上只是个人对数据库隔离级别的浅解读。