文章目录
- 原子性
原子性经典的表现是在火车票售卖:
数据库因为是网络服务,所以同一时间一定会有多个客户端访问
当还剩下一张票时,客户端A选择购买,还没有执行更新数据库时,客户端B检查了票数,发现还有一张票,于是也购买了火车票,执行数据库票数更新。然后A也进行票数更新。
这样就导致一张票卖给了两个人,这显然是不行的
从火车票售卖中,我们认识到,对于数据库数据的访问:
- 对数据的操作需要是原子的(原子性)
- 对数据操作前和操作后,都是确定的状态(一致性)
- 访问数据库不能互相影响(隔离性)
- 对数据的操作是永久有效的(持久性)
一. 什么是事务
事务主要用于处理操作量大,复杂度高的数据。
事务其实就是一组DML语句集合,这些语句在逻辑上有一定的相关性。这一组DML语句,要么全部执行成功,要么全部失败。
比如,注销某社交平台的账号,该社交平台需要将账号信息,消费信息,发表的留言等都删除,这一系列删除的DML语句,就可以构成一个事务
同时,正如同上述所说的,数据库同一时间会有多个客户访问,也就会有多个事务。所以事务也需要满足4个属性
:
- 原子性(Atomicity,或称不可分割性):一个事务(transaction)中的所有操作,要么全部完成,要么全部失败。不会结束再中间的某个环节。事务在执行过程中发生错误,会被回滚(rollback)到事务开始前的状态,就像这个事务没有被执行过一样
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏。写入的数据必须完全符合所有的预设规则(约束),比如数据的精确度,串联性以及后续数据库可以自发性的完成预定的工作
- 隔离性(Isolation,或称独立性):数据库允许多个并发事物同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行由于交叉执行而导致的数据不一致的问题。事务隔离分为不同级别,包括:读未提交(
Read Uncommitted
),读提交(Read Committed
),可重复读(Repeatable Read
),串行化(Serializable
) - 持久性(Durability):事务处理结束后,对数据的修改是永久性的,无法回滚,即使系统故障也不会丢失
这四个属性,简称为ACID
二. 事务的作用
设计事务的本质是为了,简化应用程序访问数据库时的编程模型,我们不需要去顾忌潜在错误,并发问题,网络异常等。事务内的DML语句要么全部成功,要么全部失败。因此事务本质是为了应用层服务的
MySQL的一行信息,称为一行记录
三. 事务的版本支持
在MySQL中,只有InnoDB
数据库引擎的数据库或表才支持事务,MyISAM等其他引擎不支持
四. 事务提交方式
1. 查看与设置隔离级别
查看隔离级别
select @@global.tx_isolation; --查看全局隔离级别
select @@session.tx_isolation; --查看当前会话隔离级别
select @@tx_isolation; --同session.tx_isolation
查看全局使用global.tx_isolation
查看当前,因为二者效果相同,所以建议使用短一点的tx_isolation
启动mysql客户端时,当前会话的隔离级别和全局相同
产生效果的是当前会话的隔离级别
mysql> select @@tx_isolation;
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ | 默认为可重复读
+-----------------+
设置隔离级别
设置全局的隔离性级别
mysql> set global transaction isolation level READ UNCOMMITTED;
mysql> select @@global.tx_isolation;
+------------------+
| @@global.tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
设置当前会话的隔离级别
mysql> set transaction isolation level READ COMMITTED;
Query OK, 0 rows affected (0.00 sec)
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
注意,更改当前会话隔离级别需要重启才会生效
事务的常见提交方式有两种:
- 自动提交
- 手动提交
2. 手动提交
语法:
-
begin
(或start transaction) 开启事务
事务的起点 -
commit
提交事务
事务的结束 -
rollback
回滚事务
配合savepoint使用
我们先将隔离性设置为读未提交
,方便查看结果
设置全局的隔离性级别,需要重启客户端
mysql> set global transaction isolation level READ UNCOMMITTED;
mysql> select @@global.tx_isolation;
+------------------+
| @@global.tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
准备如下表:
mysql> create table if not exists account(
-> id int primary key,
-> name varchar(30) not null default '',
-> blance decimal(10,2) not null default 0.0 comment '薪资'
-> )engine=InnoDB default charset=UTF8;
简单使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
mysql> savepoint s1;--第一个保存点 |
|
mysql> insert into account values (1,'张三',4000); | mysql> select * from account;
插入第一条记录。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| +----+--------+---------+
mysql> savepoint s2;--第二个保存点 |
|
mysql> insert into account values (2,'李四',5000); | mysql> select * from account;
插入第二条记录。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> rollback to s2; | mysql> select * from account;
回滚到s2,李四的记录没了。 | +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| +----+--------+---------+
|
mysql> rollback; | mysql> select * from account;
回滚到最开始,没有记录。 | Empty set (0.00 sec)
- commit提交后,记录就会实现持久化
- 如果未commit,客户端崩溃,那么MySQL会自动回滚到最开始,即什么都没做
3. 自动提交
查看自动提交状态
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | 自动提交 开启
+---------------+-------+
可以使用set开启或关闭事务的自动提交
mysql> set autocommit=0;
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF | 自动提交 关闭
+---------------+-------+
mysql> set autocommit=1;
mysql> show variables like 'autocommit';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON | 自动提交 开启
+---------------+-------+
autocommit=0代表关闭自动提交
autocommit=1代表开启自动提交
其实,我们之前编写的单个DML语句就是一个事务
,只不过因为开启了自动提交,所以每个DML语句都会commit
接下来,我们进行验证
首先将自动提交关闭
set autocommit=0;
此时account表中只有张三一条记录
我们开启两个mysql客户端,一个进行insert,一个读取
可以看到,客户端1插入数据后,客户端2并没有看到新的数据,此时客户端1再commit,客户端2才能看到数据
小总结
- 事务begin(开始)后,只有commit提交,才会持久化,与autocommit无关。autocommit与单DMl语句的提交有关
- 事务可以手动回滚,当操作异常时,MySQL会自动回滚
- 对于InnoDB,每一条DML语句都默认封装成事务,自动提交
- 上述操作体现原子性(回滚),持久性(commit)
事务操作注意事项
- 如果没有设置保存点,也可以回滚,只能回滚到事务的开始,直接使用rollback(事务未提交)
事务提交后,不可回滚
- 可以选择回滚到某个保存点
InnoDB
支持事务,MyISAM
不支持事务- 建议使用
begin
开启事务,比较简短
五. 四种隔离级别
- MySQL是网络进程,可能同时被多个客户端线程访问,访问的方式以实物方式进行
- 一个事务可能由一条或多条SQL构成,这意味着,任何一个事务,都有执行前,执行中,执行后的截断。而原子性,其实就是让用户层,要么看到执行前,要么看到执行后,执行中出现问题,可以随时回滚
- MySQL中,为了保证事务执行过程中尽量不受干扰,就有了一个重要特性:
隔离性
。同时MySQL允许事务受到不同程度的干扰,就有了一种重要特征:隔离级别
隔离级别
-
读未提交【Read Uncommitted】
在该隔离级别,所有事务都可以看到其他事务没有提交
的执行结果,这相当于没有隔离性,会出现很多问题,比如:脏读,幻读,不可重复读 -
读提交【Read Committed】
该隔离级别是大多数数据库默认的隔离级别(不是MySQL默认的)。一个事务只能看到其他的已经提交
的事物的执行结果。这种隔离级别会出现不可重复读的问题,即多次select 出现不同的结果 -
可重复读【Repeatable Read】
该隔离级别是MySQL默认的隔离级别,它确保同一个事物,在执行中,多次读取操作数据时,看到同样的数据行,但会有幻读问题 -
串行话【Serializable】
该隔离级别是最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读问题,它在每个读的数据行上加上共享锁,但是可能会导致超时和锁竞争(效率低
)
隔离基本都是通过锁实现的,不同的隔离级别,锁的使用不同。常见的有:表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等
接下来介绍隔离级别间的效果差异
客户端B的事务均在客户端A的事务后启动
1. 读未提交
几乎没有加锁,虽然效率高,但是问题太多,非常不建议使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4000.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> update account set blance=4444 where id=1; |
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
可以看到,客户端A未提交的事务,其执行结果,客户端B可以看见
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但未commit得数据,这种现象就叫做脏读
2. 读提交
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> update account set name='zhangsan' where id=1; | mysql> select * from account;
| +----+--------+---------+
| | id | name | blance |
| +----+--------+---------+
| | 1 | 张三 | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+--------+---------+
|
mysql> commit; | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
只有当客户端Acommit提交了事务,其执行结果,客户端B才能看到
但这就造成了
同一个事物内,同样的select,在不同时间查看到的数据不同,这叫做不可重复读
不可重复读也是问题
比如老师根据学生的成绩发放不同奖励,比如60-70是一个等级,71-80是一个等级
我们统计完了60-70,但此时一个同学的成绩出错了,少加了十分,原先是63,改为73,此时数据库select会在71~80读到他的名字,这样他就会出现两个等级中
3. 可重复读
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
| mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
mysql> update account set name='王五' where id=2; |
mysql> insert into account values (3,'赵六',7000); | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
mysql> commit; | mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 李四 | 5000.00 |
| +----+----------+---------+
|
| mysql> commit;
|
| mysql> select * from account;
| +----+----------+---------+
| | id | name | blance |
| +----+----------+---------+
| | 1 | zhangsan | 4444.00 |
| | 2 | 王五 | 5000.00 |
| | 3 | 赵六 | 7000.00 |
| +----+----------+---------+
在repeatabel read隔离级别下
客户端B的事务无法看到客户端A事务的执行结果,无论是update还是insert,即使客户端A提交也无法查看
这就叫做可重复读
但是,一般的数据库在可重复读状态下,无法屏蔽其他事务insert数据(因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大部分内容是不会变的,但是新insert的数据也会被读出,导致多次查询时,会多出查找的记录。这种现象叫做幻读
MySQL在repeatable read级别下,解决了幻读的问题(使用了Next-Key锁。即GAP+行锁)
关于锁的介绍可以参看Innodb中的事务隔离级别和锁的关系
4. 串行化
对所有操作加锁,进行串行化,不会有问题,但是效率很低,几乎不使用
客户端A | 客户端B
mysql> begin; | mysql> begin;
|
mysql> select * from account; | mysql> select * from account;
+----+----------+---------+ | +----+----------+---------+
| id | name | blance | | | id | name | blance |
+----+----------+---------+ | +----+----------+---------+
| 1 | zhangsan | 4444.00 | | | 1 | zhangsan | 4444.00 |
| 2 | 王五 | 5000.00 | | | 2 | 王五 | 5000.00 |
| 3 | 赵六 | 7000.00 | | | 3 | 赵六 | 7000.00 |
+----+----------+---------+ | +----+----------+---------+
|
mysql> update account set name='张三' where id=1; |
ERROR 1205 (HY000): Lock wait timeout exceeded; try |
restarting transaction |
客户端Aupdate阻塞了,长时间阻塞会超时终止 |
|
mysql> begin; |
mysql> update account set name='张三' where id=1; | mysql> update account set name='张三'
|
Query OK, 1 row affected (23.44 sec) | where id=1;
| ERROR 1213 (40001): Deadlock found c
| when trying to get lock;
| try restarting transaction
|
mysql> begin; | mysql> begin;
update account set name='张三炮' where id=1; |
| mysql> commit;
Query OK, 1 row affected (23.44 sec)
- 两个事物
读取不会串行化
,共享锁 - 事务Aupdate或insert,如果还有其他事务也在执行中,则事务A会
阻塞
,如果超过一定时间,会超时终止
- 如果事务A因为update或insert阻塞,此时事务B也update或insert,事务B会直接报错终止
- 当除事务A以外的其他事务都commit提交了,事务A的update或insert才会执行成功
总结
- 隔离级别越严格,安全性越高,但数据库的并发性也就越低,往往需要再两者之间找一个平衡点
- 不可重复读的重点在修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了。
- 幻读的重点在于新增:同样的条件,第一次和第二次读取的记录数不一样
MySQL默认的隔离级别是可重复读
- 事务有长事务短事务。事务之间互相影响,指的是事务在并发执行的时候,即都没有commit时,影响比较大
六. 一致性
- 事务执行的结果,必须使数据库从一个一致性状态,变成另一个一致性状态。当前数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果程序运行中断,某个事务尚未完成而被迫中断,而未完成的事务对数据库所做的修改被写入数据库,这就是一种不一致(不正确)的状态,所以一致性是通过原子性保证的
- 其实一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性需要用户业务逻辑做支撑,也就是,
一致性是由用户决定的
结束语
如果觉得本篇文章对你有所帮助的话,不妨点个赞支持一下博主,拜托啦,这对我真的很重要。