In a database management system, a transaction is a single unit of logic or work, sometimes made up of multiple operations. Any logical calculation done in a consistent mode in a database is known as a transaction. One example is a transfer from one bank account to another: the complete transaction requires subtracting the amount to be transferred from one account and adding that same amount to the other.
A database transaction, by definition, must be atomic (it must either complete in its entirety or have no effect whatsoever), consistent (it must conform to existing constraints in the database), isolated (it must not affect other transactions) and durable (it must get written to persistent storage).[1] Database practitioners often refer to these properties of database transactions using the acronym ACID.
以上是维基百科关于数据库事务的描述,可以简述为 多个操作组成的逻辑单元,具有原子性(操作全部成功或全部失败),一致性(符合一致性约束),隔离性(事务之间相互隔离)和持久性(成功/失败都需持久化写)。
本文仅对隔离性进行探讨。
事务之间的完全隔离是数据库产品的理想状态,只有在事务串行化执行时才能实现,但大多数的系统的读写符合28原则,及20%的写请求与80%的读请求,如果所有的请求都串行化执行,数据库的性能是非常差的,为了兼顾性能与隔离性,衍生出了4种隔离级别(isolation level),读未提交(Read Uncommited),读已提交(Read Commited),可重复读(Repeatable Read),可串行化(Serializable),四种级别隔离性递增,并发性递减,主流数据库产品都可以针对不同的业务场景使用不同的隔离级别,SQL server默认使用RC,MySQL innoDB默认使用RR。
后文以MySQL(innoDB引擎)为例简述隔离级别及MySQL底层实现原理。
1.数据库
MySQL 5.7+
2.常用命令
SELECT @@global.tx_isolation;-- 查看全局隔离级别
SELECT @@tx_isolation;-- 查看当前会话数据库隔离级别
SET global transaction isolation level read committed; -- 设置全局隔离级别
SET session transaction isolation level read committed; -- 设置当前会话隔离级别
BEGIN;-- 开启事务
COMMIT;-- 提交事务
ROLLBACK;-- 回滚事务
SET AUTOCOMMIT=0;//关闭事务自动提交
SET AUTOCOMMIT=1;//开启事务自动提交
3.初始化数据
DROP TABLE IF EXISTS `t`;
CREATE TABLE `t` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`value` varchar(10) NOT NULL COMMENT 'value',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
设置全局隔离级别
SET global transaction isolation level read uncommitted;
SESSION1 | SESSION2 |
BEGIN; | |
SELECT * FROM t; | |
BEGIN; |
|
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
ROLLBACK; | |
SELECT * FROM t; | |
COMMIT; |
根据上表中的操作流程可以看到,SESSION1中可以读取到其他事务中尚未提交的数据,这种现象叫做脏读,MySQL官方释义如下:
An operation that retrieves unreliable data, data that was updated by another transaction but not yet committed. It is only possible with the isolation level known as read uncommitted.
SET global transaction isolation level read committed;
SESSION1 | SESSION2 |
BEGIN; | |
SELECT * FROM t; | |
BEGIN; |
|
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
COMMIT; | |
SELECT * FROM t; | |
COMMIT; |
根据上表中的操作流程,可以看到 在SESSION2中的事务提交之前,SESSION1中读取到的数据始终是一致的,但是在SESSION1的事务中,两次读取到的数据是不一致的,这种现象叫做不可重复读,MySQL官方释义如下:
The situation when a query retrieves data, and a later query within the same transaction retrieves what should be the same data, but the queries return different results (changed by another transaction committing in the meantime).
SET global transaction isolation level repeatable read;
SESSION1 | SESSION2 |
BEGIN; | |
SELECT * FROM t; | |
BEGIN; |
|
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
COMMIT; | |
SELECT * FROM t; | |
COMMIT; |
根据上述表中的操作,SESSION1中的事务在任意时刻读到的数据都是一致的,避免了之前的不可重复读问题,两个事务看起来相安无事,似乎达到了我们对事务特性的预期(ACID),但是真的是这样吗
SESSION1 | SESSION2 |
BEGIN; | |
SELECT * FROM t; | |
BEGIN; |
|
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
COMMIT; | |
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
COMMIT; |
我们在第二次查询后增加了一个写入操作,会报以下错误(调整SESSION1的insert语句和SESSION2的commit语句顺序,可以看到不一样的效果,会在后续关于锁的文章中讲解)
ERROR 1062 (23000): Duplicate entry '1' for key 'PRIMARY'
查询时没有数据,写入时又报主键重复Error,这类现象称之为幻读phantom read,MySQL官方释义为
A row that appears in the result set of a query, but not in the result set of an earlier query. For example, if a query is run twice within a transaction, and in the meantime, another transaction commits after inserting a new row or updating a row so that it matches the
WHERE
clause of the query.
大意为在一个事务的两次查询之间有另外一个事务提交了新增或修改(包括删除)某一行的操作,这一行匹配当前事务的where条件,就会发生幻读。幻读在select操作下是感知不到的,受益于MySQL的mvcc机制,但严格来说幻读也发生了,什么都没读到,但是在修改的时候发生错误。
SET global transaction isolation level serializable;
SESSION1 | SESSION2 |
BEGIN; | |
SELECT * FROM t; | |
BEGIN; |
|
INSERT INTO t(id, value) VALUES(1,'a'); | |
SELECT * FROM t; | |
COMMIT; | |
SELECT * FROM t; | |
COMMIT; |
根据上表中的操作,不同的事务在同时开启的情况下,对一张表的操作读/写并发时会根据先后顺序串行化执行,InnoDB的实现是给所有的select查询添加FOR SHARE MODE,是增加了读意向锁,可以提高读的并发性,这种优化仅在autocommit=0时有效,官方文档:
This level is like
REPEATABLE READ
, butInnoDB
implicitly converts all plainSELECT
statements toSELECT ... FOR SHARE
ifautocommit
is disabled. Ifautocommit
is enabled, theSELECT
is its own transaction.
MySQL InnoDB存储引擎提供了四种隔离级别:读未提交(RU) 读已提交(RC) 可重复读(RR) 可串行化(S) 解决的问题以及存在的问题如下表。
脏读 | 不可重复读 | 幻读 | |
读未提交 | ✔️ | ✔️ | ✔️ |
读已提交 | × | ✔️ | ✔️ |
可重复读 | × | × | ✔️ |
可串行化 | × | × | × |
系统可以根据自身对并发性能与数据一致性的容忍程度选择相应的隔离级别,然后通过一定的手段(如加锁)来弥补一致性上的缺失。