如有错误,望不吝斧正!
文章目录
数据库事务
事务的定义
数据库事务是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
数据库事务通常包含了一个序列的对数据库的读/写操作。包含有以下两个目的:
- 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
- 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。
当事务被提交给了数据库管理系统(DBMS),则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要手动把多条SQL语句作为一个事务执行,使用 BEGIN
开启一个事务,使用 COMMIT
提交一个事务,这种事务被称为显式事务。
数据库特性ACID
原子性(Atomicity)
事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency)
事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
我认为这是事务特性中比较难理解的一点,我找了一些认为比较合适的解释:
- 什么是一致性:数据库始终都是描述的真实的现实世界,具体来说就是满足业务上的一些一致性需求,譬如,银行系统内部转账转来转去,金额总和是不变的,也没有负数余额的账户等等
- 事务的一致性C是基于A,I,D的,即不考虑事务的并发(I)和数据库的可靠性(D),事务操作都是原子的情况下(A),如何实现数据的一致性:
- 完整性约束:可以通过SQL构建,比如not null, 主键约束,参照约束,check约束等,这类约束特点是:数据表设计者建立——数据库自动检查——比较简单,不构成性能问题,但是无法胜任复杂的约束
- 程序的一致性约束:取决于业务的需求,即事务中间进行的多个操作的要保证业务上的一致性约束需求。譬如:简单的打款,你可以通过建表时设置账户余额字段不能为负数来使得余额不足时无法打款,这是完整性约束;但是,你也可以不设置账户不允许负数的规则,而是在事务执行过程里自己做判断来实现这一一致性约束。如果是比打款更加复杂的一致性(取决于业务),显然业务有各种各样的一致性约束需求,如果都通过数据库定义来实现,一是过于复杂的约束检查在性能上可能会有问题,二来也使得数据库设计有些臃肿:业务耦合太多,因此,这部分的一致性需要应用程序设计者自己来实现,即事务里具体执行的操作。
结论:事务的一致性,由数据库自动检查和程序员两方面来保障。
事务的一致性定义基本可以理解为是事务对数据完整性约束的遵循。这些约束可能包括主键约束、外键约束或是一些用户自定义约束。事务执行的前后都是合法的数据状态,不会违背任何的数据完整性,这就是“一致”的意思。
当然这个含义中也隐含着对开发者的要求,就是不能写出错误的事务逻辑,比如银行的转账不能只加钱不减钱,这是应用层面的一致性要求。
隔离性(Isolation)
多个事务并发执行时,一个事务的执行不应影响其他事务的执行。配合隔离级别使用。
持久性(Durability)
已被提交的事务对数据库的修改应该永久保存在数据库中。
事务隔离级别
对于两个并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题。因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别,避免数据不一致的问题。
READ UNCOMMITTED(读未提交)
在READ UNCOMMITTED级别,事务中的修改,即使没有提交,对其他事务也都是可见的。**事务可以读取未提交的数据,这也被称为脏读(DirtyRead)。**这个级别会导致很多问题,从性能上来说,READUNCOMMITTED不会比其他的级别好太多,但却缺乏其他级别的很多好处,除非真的有非常必要的理由,在实际应用中一般很少使用。
如果会话 2 更新 age 为 10,但是在 commit 之前,会话 1 希望得到 age,那么会获得的值就是更新前的值。或者如果会话 2 更新了值但是执行了 rollback,而会话 1 拿到的仍是 10。这就是脏读。
READ COMMITTED(读提交)
大多数数据库系统的默认隔离级别都是READ COMMITTED(但MySQL不是)。READ COMMITTED满足前面提到的隔离性的简单定义:一个事务开始时,只能“看见”已经提交的事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatableread),因为两次执行同样的查询,可能会得到不一样的结果。
由于在读取中间变更了数据,所以会话 1 事务查询期间的得到的结果就不一样了。
REPEATABLE READ(可重复读)
REPEATABLE READ解决了脏读的问题。该级别保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,可重复读隔离级别还是无法解决另外一个幻读(Phantom Read)的问题。所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行(PhantomRow)。InnoDB和XtraDB存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)解决了幻读的问题。
可重复读是MySQL的默认事务隔离级别。
由于在会话 1 之间插入了一个新的值,所以得到的两次数据就不一样了。
MVCC
MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。
可以认为MVCC是行级锁的一个变种,但是它在很多情况下避免了加锁操作,因此开销更低。虽然实现机制有所不同,但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
MVCC的实现,是通过保存数据在某个时间点的快照来实现的。也就是说,不管需要执行多长时间,每个事务看到的数据都是一致的。根据事务开始的时间不同,每个事务对同一张表,同一时刻看到的数据可能是不一样的。
InnoDB向数据库中存储的每一行添加三个隐藏字段:
- DB_TRX_ID(事务id):占6 字节,表示插入或更新该行的最后一个事务的事务标识符。此外,删除在内部被视为更新,在该更新中,行中的特殊位被设置为已删除标记(记录头信息有一个字节存储是否删除的标记)。
- DB_ROLL_PTR(回滚指针):占7字节,回滚指针指向被写在rollback segment中的undo log记录。在该行数据被更新的时候,undo log 会记录能重建该行修改之前数据所必需的信息。
- DB_ROW_ID (行id):占6 字节,就像自增主键一样随着插入新数据自增。如果表中不存主键或者唯一索引,那么数据库 就会采用DB_ROW_ID生成聚簇索引,否则DB_ROW_ID不会出现在索引中。
在MySQL中的实现思路
在MySQL中是依靠undo log和read view来实现的
undo log
记录能重建该行修改之前数据所必需的信息。在InnoDB存储引擎中,undo log分为:
- insert undo log
insert undo log是指在insert操作中产生的undo log。因为insert操作的记录,只对事务本身可见,对其他事务不可见(这是事务隔离性的要求),故该undo log可以在事务提交后直接删除。不需要进行purge操作。 - update undo log
update undo log记录的是对delete和update操作产生的undo log。该undo log可能需要提供MVCC机制,因此不能在事务提交时就进行删除。提交时放入undo log链表,等待purge线程进行最后的删除。
purge用于最终完成delete和update操作。这样设计是因为InnoDB存储引擎支持MVCC,所以记录不能在事务提交时立即进行处理。这时其他事物可能正在引用这行,故InnoDB存储引擎需要保存记录之前的版本。而是否可以删除该条记录通过purge来进行判断。若该行记录已不被任何其他事务引用,那么就可以进行真正的delete操作。可见,purge操作是清理之前的delete和update操作,将上述操作“最终”完成。而实际执行的操作为delete操作,清理之前行记录的版本。
ReadView
MySQL中的事务在开始到提交这段过程中,都会被保存到一个叫trx_sys的事务链表中,这是一个基本的链表结构。事务链表中保存的都是还未提交的事务,事务一旦被提交,则会被从事务链表中摘除。
ReadView是一个数据结构,在SQL开始的时候被创建。这个数据结构中包含了3个主要的成员:ReadView{low_trx_id, up_trx_id, trx_ids}。
- low_trx_id表示该SQL启动时,当前事务链表中最大的事务id编号,也就是最近创建的除自身以外最大事务编号;
- up_trx_id表示该SQL启动时,当前事务链表中最小的事务id编号,也就是当前系统中创建最早但还未提交的事务;
- trx_ids表示所有事务链表中事务的id集合。
对于查询时的版本链数据是否看见的判断逻辑:
查询sql时的场景 | READ COMMITTED是否适用 | REPEATABLE READ是否适用 |
---|---|---|
DB_TRX_ID <up_trx_id :说明修改该行的事务在当前事务开启之前都已经提交完成,所以对当前事务来说,都是可见的 | 是 | 是 |
[up_trx_id,low_trx_id]:RC级别下,需要判断一下 DB_TRX_ID属性值是不是在 trx_ids 列表中,如果在,说明创建 ReadView 时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建 ReadView 时生成该版本的事务已经被提交,该版本可以被访问。RR级别下:都不可见 | 是 | 否 |
low_trx_id<DB_TRX_ID:表明生成该版本的事务在生成 ReadView 后才生成,所以该版本不可以被当前事务访问 | 是 | 否 |
ReadView是与SQL绑定的,而并不是事务,所以即使在同一个事务中,每次SQL启动时构造的ReadView的up_trx_id和low_trx_id也都是不一样的,至于DATA_TRX_ID大于low_trx_id本身出现也只有当多个SQL并发的时候,在一个SQL构造完ReadView之后,另外一个SQL修改了数据后又进行了提交,对于这种情况,数据其实是不可见的。
- 使用READ COMMITTED隔离级别的事务在每次查询开始时都会生成一个独立的 ReadView。
- 使用REPEATABLE READ隔离级别的事务在查询开始时只会生成一个 ReadView。
在 MySQL 中, READ COMMITTED 和 REPEATABLE READ 隔离级别的的一个非常大的区别就是它们生成 ReadView 的时机不同。在 READ COMMITTED 中每次查询都会生成一个实时的 ReadView,做到保证每次提交后的数据是处于当前的可见状态。而 REPEATABLE READ 中,在当前事务第一次查询时生成当前的 ReadView,并且当前的 ReadView 会一直沿用到当前事务提交,以此来保证可重复读(REPEATABLE READ)。
SERIALIZABLE(序列化)
SERIALIZABLE是最高的隔离级别。它通过强制事务串行执行,避免了前面说的幻读的问题。简单来说,**SERIALIZABLE会在读取的每一行数据上都加锁,所以可能导致大量的超时和锁争用的问题。**实际应用中也很少用到这个隔离级别,只有在非常需要确保数据的一致性而且可以接受没有并发的情况下,才考虑采用该级别。
修改事务隔离级别
全局修改,修改mysql.ini配置文件,在最后加上:
#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = REPEATABLE-READ
查询当前事务级别:
SELECT @@TX_ISOLATION;
修改当前session事务级别:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
或
SET @@session.transaction_isolation = 'READ-UNCOMMITTED';
LBCC 解决数据丢失
LBCC,基于锁的并发控制,Lock Based Concurrency Control。
使用锁的机制,在当前事务需要对数据修改时,将当前事务加上锁,同一个时间只允许一条事务修改当前数据,其他事务必须等待锁释放之后才可以操作。
参考:
MySQL 事务隔离级别和锁
事务
数据库事务
如何理解数据库事务中的一致性的概念
浅谈事务与一致性问题
快速理解脏读、不可重复读、幻读和MVCC