数据库隔离性

概念

事务隔离级别是数据库事务处理的基础,ACID 中 I,即 Isolation,指的就是事务的隔离性。

隔离性是指,多个用户的并发事务访问同一个数据库时,一个用户的事务不应该被其他用户的事务干扰,多个并发事务之间要相互隔离。

sql 92 标准

sql 92 标准定义了 4 种隔离级别,读未提交、读已提交、可重复读、串行化,见下表。

  • ANSI SQL 隔离级级别
Isolation Level Dirty Read Nonrepeatable Read Phantom Read Serialization Anomaly
Read Uncommitted Possible Possible Possible Possible
Read Committed Not possible Possible Possible Possible
Repeatable Read Not possible Not possible Possible Possible
Serializable Not possible Not possible Not possible Not possible

未提交读(Read Uncommitted)

允许脏读,也就是可能读取到其他会话中未提交事务修改的数据

提交读(Read Committed)

只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)

可重复读(Repeated Read)

可重复读。在同一个事务内的查询都是事务开始时刻一致的,Mysql/InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻读。

串行读(Serializable)

完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。

例子解释

mysql 为例子。

mysql 相关

查看

  • 在 MySQL 数据库中查看当前事务的隔离级别:
select @@tx_isolation;

设置

  • 在 MySQL 数据库中设置事务的隔离级别:
set  [glogal | session]  transaction isolation level 隔离级别名称;
set tx_isolation=’隔离级别名称;

脏读

脏读就是指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。

不可重复读

是指在一个事务内,多次读同一数据。

在这个事务还没有结束时,另外一个事务也访问该同一数据。

那么,在第一个事务中的两次读数据之间,由于第二个事务的修改,那么第一个事务两次读到的的数据可能是不一样的。

这样就发生了在一个事务内两次读到的数据是不一样的,因此称为是不可重复读。

可重复读

session 1:
mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
row in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from ttd;
+------+
| id   |
+------+
|    1 |
|    2 |
+------+
rows in set (0.00 sec)

session 2 :

mysql> select @@session.tx_isolation;
+------------------------+
| @@session.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
row in set (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into ttd values(3);
Query OK, 1 row affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.03 sec)

session 2 提交后,查看session 1 的结果;

session 1:

mysql> select * from ttd;
+------+
| id   |
+------+
|    1 |                                      --------和第一次的结果一样,REPEATABLE-READ级别出现了重复读
|    2 |
+------+
rows in set (0.00 sec)

(commit session 1 之后 再 select * from ttd 可以看到session 2 插入的数据3)

幻读

第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。

同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。

那么,以后就会发生操作第一个事务的用户无法发现事务2中提交的事务,就好象发生了幻觉一样。

mysql>CREATE TABLE `t_bitfly` (
`id` bigint(20) NOT NULL default '0',
`value` varchar(32) default NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB

mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation  |
+-----------------------+-----------------+
| REPEATABLE-READ       | REPEATABLE-READ |
+-----------------------+-----------------+

实验一

t Session A                   Session B
|
| START TRANSACTION;          START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| empty set
|                             INSERT INTO t_bitfly
|                             VALUES (1, 'a');
|
| SELECT * FROM t_bitfly;
| empty set
|                             COMMIT;
|
| SELECT * FROM t_bitfly;
| empty set
|
| INSERT INTO t_bitfly VALUES (1, 'a');
| ERROR 1062 (23000):
| Duplicate entry '1' for key 1

刚刚明明告诉我没有这条记录的,可是却。。。

如此就出现了幻读,以为表里没有数据,其实数据已经存在了,傻乎乎的提交后,才发现数据冲突了。

实验二

t Session A                  Session B
|
| START TRANSACTION;         START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                            INSERT INTO t_bitfly
|                            VALUES (2, 'b');
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                            COMMIT;
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|
| UPDATE t_bitfly SET value='z';
| Rows matched: 2  Changed: 2  Warnings: 0
| (怎么多出来一行)
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | z     |
| |    2 | z     |
| +------+-------+

实验三

再看一个实验,要注意,表 t_bitfly 里的 id 为主键字段。

t Session A                 Session B
|
| START TRANSACTION;        START TRANSACTION;
|
| SELECT * FROM t_bitfly
| WHERE id<=1
| FOR UPDATE;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                           INSERT INTO t_bitfly
|                           VALUES (2, 'b');
|                           Query OK, 1 row affected
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |
| +------+-------+
| |    1 | a     |
| +------+-------+
|                           INSERT INTO t_bitfly
|                           VALUES (0, '0');
|                           (waiting for lock ...then timeout)
|                           ERROR 1205 (HY000):
|                           Lock wait timeout exceeded;
|                           try restarting transaction
|
| SELECT * FROM t_bitfly;
| +------+-------+
| | id   | value |

可以看到,用id<=1加的锁,只锁住了id<=1的范围,可以成功添加id为2的记录,添加id为0的记录时就会等待锁的释放。

实验四

一致性读和提交读

t Session A                      Session B
|
| START TRANSACTION;             START TRANSACTION;
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 | a     |
| +----+-------+
|                                INSERT INTO t_bitfly
|                                VALUES (2, 'b');
|                                COMMIT;
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 | a     |
| +----+-------+
|
| SELECT * FROM t_bitfly LOCK IN SHARE MODE;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 | a     |
| |  2 | b     |
| +----+-------+
|
| SELECT * FROM t_bitfly FOR UPDATE;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 | a     |
| |  2 | b     |
| +----+-------+
|
| SELECT * FROM t_bitfly;
| +----+-------+
| | id | value |
| +----+-------+
| |  1 | a     |
| +----+-------+

如果使用普通的读,会得到一致性的结果,如果使用了加锁的读,就会读到“最新的”“提交”读的结果。

本身,可重复读和提交读是矛盾的。在同一个事务里,如果保证了可重复读,就会看不到其他事务的提交,违背了提交读;如果保证了提交读,就会导致前后两次读到的结果不一致,违背了可重复读。

可以这么讲,InnoDB 提供了这样的机制,在默认的可重复读的隔离级别里,可以使用加锁读去查询最新的数据(提交读)。 MySQL InnoDB 的可重复读并不保证避免幻读,需要应用使用加锁读来保证。

而这个加锁度使用到的机制就是 next-key locks。

小结

隔离级别越高,安全性越高,但是性能越差。

mysql 隔离性实现原理

实现方式

我们知道,如果要实现数据库事务最高隔离性,也就是最安全的隔离性,有个显而易见的实现就是当一个事务在执行的时候,其他全部事务都阻塞,等待这个事务执行完再执行,这在现代多核CPU环境下显然非常浪费计算资源。为了充分利用资源,必须支持并发,这里就涉及并发控制(Concurrency control) 这是一个非常大的主题,关系到数据库,有两个比较重要的方法,一个是用锁(lock),一个是称为多版本并发控制(MVCC)的方法。

锁的方式

悲观锁

读写锁

读写锁的概念很平常,当你在读取数据的时候,应该先加读锁,读取完之后的某个时间再解开读锁,那么加了读锁的数据,应该需要有什么特性呢,应该只能读,不能写,因为加了读锁,说明有事务准备读取这个数据,如果被别的事务重写这个事务,那数据就不准确了。所以一个事务给这个数据加了读锁,别的事务也可以对这个数据加读锁,因为大家都是只读不写。

写锁则具有排他性(exclusive lock),当一个事务准备对一个数据进行写操作的时候,先要对数据加写锁,那么数据就是可变的,这时候,其他事务就无法对这个数据加读锁了,除非这个写锁释放。

两端式提交锁(Two-phase locking)

两段式提交分为两步:

  1. 这个阶段只加锁,或者释放锁(读写锁)

  2. 这个阶段只会释放锁

下面对应于不同隔离级别对加锁方式进一步分析:

未提交读(read uncommitted):这个级别加锁,其实并不需要用两端式加锁,每一个具体操作执行完,锁就可以释放了。

提交读(read committed):这个阶段其实也可以按照每个操作执行前加锁,执行之后释放锁的方式。

可重复读(repeatable read) : 这个级别,就要求读锁必须,到事务结束前最后时刻才能释放,这样才能保证读取到数据是不可变的,可重复读的。但是这样会阻塞其他事务对加锁的数据的写操作。

序列化读(serializable):这个级别要求,两段式提交的第一步,要在事务开始的时候,原子性的把需要的锁全部加好(这显然很难估算,除非很大力度的锁),在事务结束前最后时刻,把全部锁一次性释放。这样做的结果就是使很多数据在事务执行期能都被加锁,无法被其他事务所使用。

MVCC

乐观锁

概念

加锁的方式处理事务一个比较大的问题就是会造成死锁(dead lock),原因就是一个事务加锁的数据并不止只有一行。

事务A对行C加写锁,事务B对行D加写锁,接着事务希望获取行D的锁,事务B希望获取行C的锁,这样很容就死锁了。

使用MVCC就可以避免很多情况下的加锁操作,使用数据冗余的方式来实现事务隔离(这真是个很好的设计啊)

MVCC提供的只是一种思路,具体的实现比较多样化。 大体的思路是每一行保存冗余数据,读写的时间戳,也可以称为版本号,在对某一行数据继续update或者delete的时候,并不直接操作,而是复制多一份副本进行操作,这个就是所谓多版本(multiversion)。

mysql innodb 对于 MVCC 的实现

innodb对每一行保存两个系统版本号,一个更新操作的版本号,一个删除操作的版本号,这两个版本号的来源是事务的ID(transaction id),也就是说,当某个事务对这一行数据进行update,或者删除的时候,相应会把它的事务ID写入这行数据的更新操作的版本号,删除操作的版本号中。

事务ID是随时间推移而增长,而且不可重复的。一个事务打开之后:

对于select操作:每次只会select具有比当前事务ID更小的更新操作版本号的数据,而且这些数据要保证删除版本号为空,或者删除版本号大于当前事务ID。

对于update操作:对该行数据复制出一份副本,同时在更新操作版本号写入当前事务ID,同时把当前事务ID写入之前的删除操作的版本号中。

对于insert操作:写入新行,同时在更新操作版本号写入当前事务ID

对于delete操作:在删除操作版本号写入当前事务ID

mysql官方innodb的实现是用MVCC,官方声称默认的innodb的隔离级别是可重复读。

但是mysql是保证不会出现幻读的,因为它每次select只会读取在事务开始时候的snapshot,并且忽略在这个时刻之后提交的所有变更。

consistent read

mysql官方文档提到串行隔离级别要在原来的基础说对每一个select操作执行 SELECT … LOCK IN SHARE MODE

这样就可以读取的数据加读锁了,那么其他试图写入数据都必须阻塞。那么就可以实现序列化串行了。

参考文档

  • sql

sql-1992

  • 隔离级别

https://github.com/pingcap/docs-cn/blob/master/sql/transaction-isolation.md

http://www.cnblogs.com/zhoujinyi/p/3437475.html

https://www.jianshu.com/p/06f3542240b4

https://zh.wikipedia.org/wiki/%E4%BA%8B%E5%8B%99%E9%9A%94%E9%9B%A2

  • mysql InnoDB LOCK

https://mp.weixin.qq.com/s/x_7E2R2i27Ci5O7kLQF0UA

http://www.cnblogs.com/zhoujinyi/p/3435982.html

http://hedengcheng.com/?p=771

https://www.jianshu.com/p/5734c143c0bc

  • MVCC

http://blog.51cto.com/donghui/692586

https://draveness.me/database-concurrency-control

https://segmentfault.com/a/1190000012650596

  • oracle

https://www.2cto.com/database/201306/223404.html