数据库 - 事务(Transaction)

数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成

事务的特性

在事务的定义上,其主要有四大特性:原子性、一致性、隔离性和持久性,简称为ACID。这四大特性的含义如下:

  • 原子性:在一个事务中的操作都是一个逻辑的单元,在执行事务序列时,这些操作要么全部成功,要么全部失败;
  • 一致性:对于事务操作,其始终能够保证在事务操作成功或者失败回滚时能够达到一种一致的状态,简而言之,事务中的操作要么全部成功,要么全部失败;
  • 隔离性:各个事务之间的执行是相互不可见的,在事务还未执行成功时,其他的事务只能看到当前事务开始执行的状态,只有在当前事务执行完成之后才能看到执行之后的状态;
  • 持久性:事务的持久性指的是事务一旦执行成功,那么其所做的修改将永久的保存在数据库中,此时即使数据库崩溃,修改的数据也不会丢失。

有关事务的隔离性,事务规范为事务提供了四种隔离级别:Read uncommitted、Read committed、Repeatable readSerializable

关于这四种隔离级别,其主要区别在于三个点:脏读、不可重复读幻读

  • 脏读:脏读表示一个事务能够读取另一个事务中还未提交的数据。比如,某个事务尝试插入记录A,此时该事务还未提交,然后另一个事务尝试读取记录A,这时其是会成功读取到记录A的;
  • 不可重复读:不可重复读表示当前事务对同一记录的两次重复读取结果不一致。比如一个事务首先读取一条记录A,读完之后另一个事务将该记录修改并且成功提交了,然后当前事务再次读取记录A,此时该事务会发现两次读取的结果不一致;
  • 幻读:幻读指的是一个事务在进行一次查询之后发现某个记录不存在,然后会根据这个结果进行下一步操作,此时如果另一个事务成功插入了该记录,那么对于第一个事务而言,其进行下一步操作(比如插入该记录)的时候很可能会报错。从事务使用的角度来看,在检查一条记录不存在之后,其进行插入应该完全没问题的,但是这里却抛出主键冲突的异常。

关于事务的四种隔离级别,其主要区别点也就在于是否能够解决这三个问题。这四种事务的隔离级别主要区别如下:

  • Read uncommitted:这是隔离性最低的一种隔离级别,在这种隔离级别下,当前事务能够读取到其他事务已经更改但还未提交的记录,也就是脏读;
  • Read committed:顾名思义,这种隔离级别只能读取到其他事务已经提交的数据,也就解决了脏读的问题,但是其无法解决不可重复读和幻读的问题;
  • Repeatable read:从事务的定义上,这种隔离级别能够解决脏读和不可重复读的问题,但是其无法解决幻读的问题;
  • Serializable:也称为序列化读,这是隔离性最高的一种隔离级别,所有的事务执行(包括查询)都会为所处理的数据加锁,操作同一数据的事务将会串行的等待。

实例

关于四种事务隔离级别的演示,我们主要使用MySql客户端进行。这里首先需要说明的几个命令是关于事务的几个基本操作命令:

# 设置当前会话的事务隔离级别,需要严格注意区分命令中的大小写,这里四种隔离级别分别是:
# Read uncommitted,Read committed,Repeatable read,Serializable
SET session TRANSACTION ISOLATION LEVEL Read uncommitted;

# 查看当前会话的事务隔离级别
show variables like 'transaction_isolation';

# 开始一个事务
start transaction;

# 回滚当前事务
rollback;

# 提交当前事务
commit;

首先我们建立如下的数据库表结构:

create table user(
  id bigint auto_increment comment '主键',
  name varchar(20) not null default '' comment '名称',
  age int(3) not null default 0 comment '年龄',
  primary key(id)
);
Read uncommitted

首先我们开启两个Mysql命令行,并且设置事务隔离级别为Read uncommitted。对于Read uncommitted,理论上在一个会话中开启事务之后,另一个会话插入一条未提交的数据,当前会话是可以读取到这条记录的。这里我们首先在会话A中执行如下命令:

# 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

然后在会话B中开启事务,并插入一条记录:

# 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(id, name, age) value (1, 'Mary', 23);
Query OK, 1 row affected (0.00 sec)

此时再在会话A中尝试读取该记录:

# 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在A和B两个会话事务都未提交的情况下,会话A中的事务是能够成功读取到会话B中事务未提交的数据的,这也就产生了脏读的问题。

Read committed

关于Read committed,其表示当前事务只能读取其他事务已经提交的数据,但是无法解决不可重复读的问题。

  • 读取已提交数据

这里的演示方式与脏读类似,首先在会话A中开启事务,然后在会话B中也开启事务,并且插入一条数据,此时在会话A中尝试读取该记录,应该是无法读取到结果的,如果在会话B中提交该事务之后,会话A中则应该可以读取到这条记录。

# 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

然后在会话B中开启事务并插入一条记录:

# 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user(id, name, age) value (1, 'Mary', 23);
Query OK, 1 row affected (0.01 sec)

此时在会话A中读取该记录应该是无法读取到的:

# 会话A
mysql> select * from user where id=1;
Empty set (0.00 sec)

可以看到,这里会话A中的事务是无法读取到会话B中事务插入的还未提交的数据的。此时我们提交会话B中的事务,并且再次在会话A中尝试读取该记录:

# 会话B
mysql> commit;
Query OK, 0 rows affected (0.06 sec)

# 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在会话B提交了事务之后,会话A是能够获取到会话B进行的修改的。

  • 不可重复读

关于不可重复读,理论上,一个事务中,在对同一条记录的多次重复读取,得到的结果应该是始终一致的。这里Read committed隔离级别是没有这个特性的,因而如果我们在会话A中读取一条记录,然后在会话B中修改该记录并且提交,接着在会话A中再次进行读取,那么此时会话A中读取到的应该是修改之后的值。首先我们在会话A中开启事务,并且读取一条记录:

# 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   23 |
+----+------+------+
1 row in set (0.00 sec)

然后在会话B中开启事务,修改一条记录,并且提交:

# 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=25 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

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

然后在会话A中再次读取该记录:

# 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,会话A在事务还未提交的情况下,其重复读取同一条记录,两次读取的结果居然不一致,这也就是不可重复读。

Repeatable read

关于Repeatable read,在定义上,其解决了不可重复读的问题,但是没解决幻读的问题,这里由于MySql使用了Next key-lock算法,因而在这个隔离级别下,其也解决了幻读的问题。这里我们会对着两种情况依次进行演示。

  • 可重复读

这里可重复读的演示与上面不可重复读的演示方式是一样的,只是这里将隔离级别设置为Repeatable read。

# 会话A
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

然后在会话B中开启事务,修改该记录,并且提交:

# 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> update user set age=30 where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0

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

这里会话B在事务中修改了id为1的记录的值,此时我们再次在会话A中读取该记录:

# 会话A
mysql> select * from user where id=1;
+----+------+------+
| id | name | age  |
+----+------+------+
|  1 | Mary |   25 |
+----+------+------+
1 row in set (0.00 sec)

可以看到,在会话A还未提交的时候,其读取的结果始终是一致的,并未受到会话B中已经提交的事务的影响。

  • 幻读

关于幻读,最典型的示例就是在一个事务中进行数据插入时,MySQL首先会先检查该数据是否存在,如果不存在则插入数据,这个过程中,如果另一个事务也插入了同样的数据,那么这个事务是会被阻塞的,如果当前事务提交了,那么另一个事务就会抛出主键冲突的异常。

# 会话A
mysql> select * from user where id=2 for update;
Empty set (0.01 sec)

mysql> insert into user (id, name, age) value (2, 'Jack', 24);
Query OK, 1 row affected (0.00 sec)

此时在会话B中开启事务,并且尝试插入同一条数据,那么其是会被阻塞的:

# 会话B
mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into user (id, name, age) value (2, 'Bob', 28);

可以看到,这里会话B中的插入操作是被阻塞了的。如果此时将会话A中的事务提交,那么会话B中将会抛出异常:

# 会话B
mysql> insert into user (id, name, age) value (2, 'Bob', 28);
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'

这里关于幻读需要说明的一点是,MySql在Repeatable read级别解决的幻读问题是在MySQL级别处理的,比如上面的示例中,我们在会话A中查询时加上了for update,该命令会针对目标记录加锁,如果目标记录不存在,则会加上Gap锁,这样后面在同一事物中插入是可以成功的。如果在同一事物中只是单纯的查询,然后进行插入,那么还是会出现幻读的问题的。也就是说上面的示例中,如果将for update去掉,那么其还是会出现幻读的问题的。

Serializable

关于序列化读,这里就比较简单。对于一个事务而言,其所有的操作都会锁定所操作的数据和Gap,此时另外的事务只能等待该事务完成才能进行下一步操作。这里我们以两个事务同时查询同一事务中的同一记录为例进行展示:

# 会话A
mysql> select * from user where id=2;
+----+------+------+
| id | name | age  |
+----+------+------+
|  2 | Jack |   24 |
+----+------+------+
1 row in set (0.00 sec)

这里会话A是可以正常读取记录的,此时我们在会话B中尝试使用加锁的方式读取同一记录:

# 会话B
mysql> select * from user where id=2 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

可以看到,会话B中的事务尝试加锁是失败的,因为目标记录在会话A中已经被锁定了。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345