随笔分类
事务
事务就是一组原子性的 SQL查询,或者说一个独立的工作单元
事务内的语句,要么全部执行成功,要么全部执行失败.
数据库执行引擎会对数据库应用改组查询的全部语句
一个运行良好的事务处理系统,应当具备 ACID
标准特征:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability).
-
原子性:一个事务必须视为不可分割的工作单元. 对于一个事务而言,不可能只执行其中的一部分操作.
-
一致性:数据库总是从一个一致的状态转换到另一个一致的状态.
一致性是
事务执行的最终目的
,一致性即满足因果关系的守恒定律,原子性、隔离性、持久性都是为了实现一致性而存在的. -
隔离性:一个事务所做的修改在最终提交之前,对其它的事务都是不可见的
-
持久性:一旦事务提交,所做的修改便会永久的保存到数据库中
这里的持久化其实是个模糊的概念,实际上持久化也是分很多不同的等级的,有的持久化策略提供非常强的安全保障,而有些则未必.
且不可能做到 100%的持久性保证的策略存在(否则又何来备份能够增加持久性之说.)
就像锁粒度的升级会增加系统开销一样,这种事务处理过程中额外的安全性,也会需要数据库系统做更多的额外工作.
一个实现了 ACID的数据库,通常会需要更强的 CPU处理能力、更大的内存和更多的存储空间
.
隔离级别
在 SQL标准中定义了 4种
隔离级别,每一种级别都规定了一个事务中所做的修改,哪些在事务内和事务间是可见的,哪些是不可见的.
较低级别的隔离通常可以执行更高的并发
,系统的开销也更低.
每种存储引擎实现的隔离级别不进相同.
查看数据库默认的事务隔离级别
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ | # 可见,MySQL默认的事务隔离级别是可重复读.
+-----------------------+-----------------+
1 row in set (0.00 sec)
mysql>
-
READ UNCOMMITTED(未提交读)
在 READ UNCOMMITTED级别,事务中所做的修改即便没有提交,对其它事务也是可见的
事务可以读取未提交的数据,这也叫脏读(Dirty Read)
脏读会导致许多问题,且未提交读实际上并没有比其它级别好太多,却少了其它级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用. -
READ COMMITTED(提交读)
大多数数据库系统的默认隔离级别都是提交读(MySQL不是). 即一个事务从开始直到提交之前,所作的任何修改对其它事务是不可读. 这也就
避免了脏读
这个级别有时也叫
不可重复读(nonrepeatable read)
,因为两次同样的查询,可能会得到不一样的结果(因为在提交之前,所作的修改对其它事务实际上是透明的). -
REPEATABLE READ 可重复读
可重复读
解决了脏读和不可重复读
的问题. 这个级别保证了同一个事务中多次读取同样的记录
的结果是一致的.但可重复读并没有解决幻读的问题,所谓幻读,指的是事务多次进行
同一范围内的数据查询
时,查出的数据记录数是不一样的,即出现了幻行(Phantom Row)InnoDB 和 XtraDB通过
多版本并发控制
(MVCC,Multiversion Concurrrncy Control)来解决了幻读的问题可重复读是 MySQL 的默认事务隔离级别
. -
SERIALIZABLE READ可串行化读
事务的最高级别隔离,通过强制事务串行执行,来避免了前面所说的幻读的问题.
可串行化读会在
读取的每一行数据上都加锁
,所以可能导致大量的超时和锁争用的问题.只有在非常需要确保数据的一致性并且可以接受没有并发的情况下,才考虑去采用该级别.
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁表 |
---|---|---|---|---|
未提交读 | √ | √ | √ | × |
提交读 | × | √ | √ | × |
可重复读 | × | × | √ | × |
串行化读 | × | × | × | √ |
案例使用
DataBase基本使用
进入后开启MySQL
root@a43fc0a13a0b:/# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 16
Server version: 8.0.22 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
查看数据库及其对应表
mysql> show databases; # 查看当前有哪些数据库
+--------------------+
| Database |
+--------------------+
| information_schema |
| mysql |
| performance_schema |
| sky |
| sys |
+--------------------+
5 rows in set (0.01 sec)
mysql> use sky; # 使用名为sky的数据库
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed # 数据库成功切换
mysql> show tables; # 查看当前数据库的表
+---------------+
| Tables_in_sky |
+---------------+
| user | # 可见有一张user表
+---------------+
1 row in set (0.00 sec)
脏读
设置隔离级别为未提交读
mysql> set session transaction isolation level read uncommitted;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'transaction_isolation';
+-----------------------+------------------+
| Variable_name | Value |
+-----------------------+------------------+
| transaction_isolation | READ-UNCOMMITTED | # 可见当前的隔离级别为未提交读
+-----------------------+------------------+
1 row in set (0.01 sec)
可见,事务一读取到了事务二未提交的修改数据,这便是脏读了(若事务二发生了回滚而导致数据恢复,想想这是什么后果?).
不可重复读
设置会话隔离级别为提交读
mysql> set session transaction isolation level read committed;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'transaction_isolation';
+-----------------------+----------------+
| Variable_name | Value |
+-----------------------+----------------+
| transaction_isolation | READ-COMMITTED | # 可见,当前事务隔离级别为提交读.
+-----------------------+----------------+
1 row in set (0.00 sec)
mysql>
提交读的隔离级别虽然是解决了脏读的问题,但是并没有解决不可重复读的问题.
可见,在事务一先后读取同一记录的期间,有别的事务对此记录进行了修改且提交了事务,此时事务一再去查询时便会发现记录的值发生了变化,这便是 不可重复读.
解决
将隔离级别设置为可重复读便可以解决不可重复读的问题了
mysql> set session transaction isolation level repeatable read;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ | # 当前会话隔离级别为可重复读
+-----------------------+-----------------+
1 row in set (0.00 sec)
mysql>
新的问题
可重复读虽然是解决了不可重复读的问题,但是并没有解决换读的问题
可重复读级别会事务开始期间对读取的数据进行加锁,但是并没有解决事务期间新的数据记录的新增问题,这也就意味着当事务进行范围化记录查询时,便有可能会出现前后查到的记录行数不一致问题,这也便是所谓的 幻读了
幻读
MySQL默认的事务隔离级别是 repeatable read,此级别虽然并没有解决 幻读,但 MySQL的默认存储引擎 InnoDB使用了 MVCC来解决了这个问题,此处便不进行展示了.
死锁
死锁是指两个或者多个事务在同一资源上相互占用,并且请求锁定对方占用的资源,从而导致而恶性循环的现象.
当多个事务试图以不同的顺序
去访问资源,或者是多个事务同时锁定同一资源
时,也会产生死锁.
为了解决死锁的问题,数据库系统实现了各种死锁监测
和死锁超时机制
. 越复杂的系统,比如 InnoDB存储引擎,越能检测到死锁的循环依赖,这种解决方案非常有效,不然死锁便有可能会导致非常慢的查询.
还有一种不太友好的解决方案,便是当查询的时间达到锁等待超时的设定后,便放弃此次查询.
InnnDB目前处理死锁的方法便是,将持有最少行级排它锁的事务进行回滚
,这也是相对比较简单的死锁回滚
算法.
锁的行为和顺序是和存储引擎相关的,以同样的顺序执行语句,有的存储引擎会发生死锁,而有的不会.
死锁的产生有双重原因
:有的是因为真正的数据冲突,这种情况难以避免;而有的便是存储引擎的实现方式导致的.
死锁发生后,只有部分或者完全回滚其中一个事务,才能打破死锁
.
对于事务型的系统而言,这是无法避免的,所以应用程序在设计时必须要考虑如何去处理死锁. 大多数情况下,只需要重新执行因死锁回滚的事务
即可.
日志
事务日志可以帮助提高事务的效率
. 使用事务日志,存储引用修改表数据时便不用将修改后的数据本身持久化,只需要修改其内存备份,再把此修改数据的行为记录到持久在磁盘的事务日志中
即可.
事务日志采用的是追加
的方式,因此写日志的方式是磁盘上一小块区域的顺序 I/O,而不像随机 I/O一样在多个地方移动磁头
,所以采用事务日志的方式要快得多.
事务日志持久化之后,内存中被修改的数据可以在后台慢慢地刷回到磁盘. 目前大多数存储引擎都是这样的实现方式,即预写式日志(Write-Ahead Logging)
,修改数据需要写两次磁盘
.
如果数据的修改已经记录到事务日志并持久化,但数据本身还没有写回到磁盘,此时系统崩溃了,存储引擎在重启时能够自动恢复这部分修改的数据.
MySQL
MySQL提供了两种事务型的存储引擎:InnoDB和 NDB Cluster.
自动提交(AUTOCOMMIT)
MySQL默认采用自动开启模式
,也就是说如果不是显示地去开启一个事务,则每个查询都被当做一个事务执行提交操作.
在当前连接中,可以通过设置 AUTOCOMMIT变量来启用或者禁用自动提交模式:
mysql> show variables like 'AUTOCOMMIT' # 查看当前AUTOCOMMIT变量的值
-> ;
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | ON |
+---------------+-------+
1 row in set (0.01 sec)
mysql> set AUTOCOMMIT = 1 # 1表示ON开启,0表示OFF关闭
当没有开启自动提交时,那么所有的查询都会在一个事务中,直到显示地执行 COMMIT提交或者 ROLLBACK回滚,该事务结束,同时将开启一个新的事务
修改 AUTOCOMMIT对非事务型的表,比如 MYISAM或者内存表,不会有任何影响. 对于这类表而言,没有 COMMIT或者 ROLLBACK的概念,也可以说是一直处于 AUTOCOMMIT启用的模式
还有一些命令,在执行之前会强制执行 COMMIT提交当前的活动事务(比如涉及了大量数据改变的语句命令 --> 在 DDL中)
MySQL可以通过 SET TRANSACTION ISOLATION LEVEL
命令来设置事务的隔离级别,新的隔离级别会在会在下一个事务生效.
可以在配置文件中设置整个数据库的隔离级别,也可以只改变当前会话的隔离级别.
MySQL能够识别 4个 ANSI隔离级别,InnoDB也支持所有的隔离级别.
- set session transaction isolation level 来设置当前会话的事务隔离级别,不会影响到其它会话
- set global transaction isolationlevel 来设置全局的事务隔离级别,该设置不会影响到当前已经连接的会话,而新打开的会话,将使用新设置的事务隔离级别.
在事务中混合使用存储引擎
MySQL的服务器层是不直接去管理事务的,事务是由下层的存储引擎来实现的. 所以同一种事务中,是不会有多种存储引擎的.
如果一个事务中混合使用了多种存储引擎(比如 InnoDB 和 MyISAM),一般情况下是不会有什么问题的. 但若事务发生回滚,对于非事务型表上的操作是不能撤销的,此时的数据库便处于 不一致的状态,这种情况 难以修复,事务的 最终结果也难以确定.
所以,为一个事务选择合适的存储引擎十分重要.
在非事务型的表上执行事务操作时,MySQL通常不会发出提醒,也不会进行报错. 有时候只会在非事务型的 执行回滚操作 时才会发出一个警告:"某些非事务型的表上的变更不能被回滚,但是 大多数情况下,对非事务型的表的操作都不会有提示."
隐式和显式锁定
InnoDB采用的是 两阶段锁协议(two-phase locking )
- 两阶段锁协议:整个事务分为了两个阶段,前一个阶段加锁,后一个阶段解锁. 在加锁阶段,只能加锁,还可以来操作数据,但是不能进行解锁. 在解锁阶段,只能解锁,可以来操作数据,但是不能加锁. 两阶段锁协议使事务具有更高的并发性,因为解锁不必发生在事务结尾.
在事务的执行过程中,随时可以执行锁定,锁只有在 COMMIT 或者 ROLLBACK时才会被释放,并且所有的锁是在同一时刻被释放.
InnoDB会根据事务的隔离级别在适当的时候自动加锁(隐式锁定)
InnoDB也支持通过特定的语句进行 显式锁定,这些语句不属于SQL规范.
- select ... lock in share mode
- select ... for update
MySQL也支持 lock tables 和 unlock tables,这是在服务器层实现的,和存储引擎无关. 它们有自己的用途,并不能来替代事务处理. 如果应用需要哟用到事务,还是应该选择事务型存储引擎.
实际上 lock tables十分影响性能,而 InnoDB的行级锁工作已经做得很好了.
多版本并发控制
MySQL的大多数事务型存储引擎实现的都不是简单的行级锁,基于提升并发性能的考虑,它们一般都同时实现了多版本并发控制(MVCC,Multi-version Concurrency Control),MVCC没有一个统一的实现标准.
可以认为 MVCC是行级锁的一个变种,但是它在很多情况下 避免了加锁操作,因此开销更低. 虽然 MVCC的具体有所差别,但它们大多都实现了非阻塞的读操作,写操作只来锁定必要的行.
MVCC的实现,是通过保存数据在某个时间的快照实现的,这也就保证了事务在执行过程中看到的数据都是一致的.
InnoDB的 MVCC,是通过在每行记录的后面保存两个隐藏的列来实现的. 这两个列,一个列保存了 记录的创建时间,一个保存了 记录的过期时间(删除时间). 当然,存储的并非是真实的时间值,而是 系统的版本号(System Version Number).
事务开始时刻的系统版本号会作为事务的版本号(每开启一个新的事务时,系统的版本号都会自动递增),用来和查询的每行记录的版本号进行比较.
Repeatable Read 隔离级别下的,MVCC具体的操作:
-
SELECT
InnoDB会根据以下 两个条件检查每行记录:
- InnoDB只查找版本早于当前事务版本的数据行(即,行的系统版本号小于或者等于事务的系统版本号),这样可以确保事务读取到的行,要么是在事务开始之前就已经存在,要么是事务自身插入或者修改的.
- 行的删除版本要么未定义,要么大于当前的事务版本号,确保事务读取到的行,在事务开始之前并没有被删除.
-
INSERT
InnoDB会把当前的系统版本号作为新插入的每一行的版本号
-
DELETE
InnoDB会把当前系统的版本号作为所删除行的删除版本号
-
UPDATE
InnoDB会插入一行新纪录,保存当前系统的版本号作为行版本号,同时会保存当前系统的版本号作为原来行的删除标识.
保留着两个额外的系统版本号,使大多数读操作都不需要加锁. 这样设计使得读数据操作简单,性能很好,并且也能够保证只会读取到符合标准的行.
缺点便是,每行记录都需要额外的存储空间,需要更多的行检查工作,以及一些额外的维护工作.
MVCC只在Repeatable Read 和 Read Committed两个隔离级别下工作,其它两个隔离级别和 MVCC冲突
- Read Uncommitted只会读取到最新的行数据,而不是符合当前事务版本的行数据
- Serialiable则是会对所有读取的行都加锁.