Transaction作为关系型数据库的核心组成,在数据安全方面有着非常重要的作用,本文会一步步解析事务的核心特性,以获得对事务更深的理解。
数据库几乎是所有系统的核心模块,它将数据有条理地保存在储存介质(磁盘)中,
并在逻辑上,将数据以结构化的形态呈现给用户。支持数据的增、删、改、查,并在过程中保障数据的正确且可靠。
要做到这点并非易事,常见的例子就是银行转账,A账户给B账户转账一个亿(T1),买一块地盖房子。在这种交易的过程中,有几个问题值得思考:
要保证交易正常可靠地进行,数据库就得解决上面的四个问题,这也就是事务
诞生的背景,它能解决上面的四个问题,对应地,它拥有四大特性:
要么全部完成,要么全部取消
。 如果事务崩溃,状态回到事务之前(事务回滚)。接下来详细地了解这四大特性:
可以看出,原子性、隔离性、一致性的根本问题,是不同的事务同时对同一份数据(A账户)进行写操作
(修改、删除、新增),如果事务中都只是读数据的话,那么它们可以随意地同时进行,反正读到的数据都是一样的。
如果,几个互不知晓的事务在同时修改同一份数据,那么很容易出现后完成的事务覆盖了前面的事务的结果,导致不一致。 事务在最终提交之前都有可能会回滚,撤销所有修改:
1. 如果T1事务修改了A账户的数据,
2. 这时T2事务读到了更新后的A账户数据,并进行下一步操作,
3. 但此时T1事务却回滚了,撤销了对A账户的修改,
4. 那么T2读取到的A账户数据就是非法的,这会导致数据不一致。
这些问题都是事务需要避免的。
begin; -- 开始一个事务
update table set A = A - 1亿; -- 伪sql,仅作示意
update table set B = B + 1亿;
-- 其他读写操作
commit; -- 提交事务
要保证上面操作的原子性, 就得等begin
和commit
之间的操作全部成功完成后,才将结果统一提交给数据库保存,如果途中任意一个操作失败,就撤销前面的操作,且操作不会提交数据库保存,这样就保证了同生共死
。
原子性的问题解决了,但是如果有另外的事务在同时修改数据A怎么办呢? 虽然可以保证事务的同生共死,但是数据一致性会被破坏。 此时需要引入数据的隔离机制,确保同时只能有一个事务在修改A,一个修改完了,另一个才来修改。 这需要对数据A加上互斥锁:
以上面的事务为例,称作T1,T1在更新A的时候,会给A加上互斥锁,保证同时只能有一个事务在修改A。 那么这个锁什么时候释放呢? 当A更新完毕后,正在更新B时(T1还没有提交),有另外一个事务T2想要更新A,它能获取到A的互斥锁吗?
答案是不能, 如果T1在更新完A后,就释放了互斥锁,此时T2获取到T1的最新值,并做修改, 如果一且正常,则万事大吉。 但是如果在T2更新A时,T1因为后面的语句执行失败而回滚了呢?
1. 此时T1会撤销对A的修改,
2. T2得到的A数据就是脏数据,更新脏数据就会导致数据不一致。
所以,在事务中更新某条数据获得的互斥锁,只有在事务提交或失败之后才会释放,在此之前,其他事务是只能读,不能写这条数据的。
这就是隔离性的关键,针对隔离性的强度,有以下四的级别(引用自这篇文章):
接下来详细解释,假设有下面两个事务同时执行:
begin; -- 事务1
insert into table1 (somevaue); -- 随意写的伪sql
update table2 set aa = aa + 1 where id = 1;
commit;
begin; -- 事务2
select count(*) from table1; -- 第一次读count
select aa from table2 where id = 1; -- 第一次读aa
-- 假设在这个点 事务1成功提交
select count(*) from table1; -- 第二次读count
select aa from table2 where id = 1; -- 第二次读aa
commit;
串行化不用解释了,依次执行,不会产生冲突。
可重复读是什么意思呢? 事务2执行到一半时,事务1 成功提交:
第二次读count
得到的值和第一次读count
得到的值不一样(因为事务1新增了一条数据),这叫幻读,不隔离新增的数据。第一次读aa
和第二次读aa
得到的值是一样的,对刚更新的值不可见,隔离已经存在的数据。 可以重复读,读到的数据都是一样的。读取已提交是什么意思呢? 事务2执行到一半时,事务1 成功提交:
第二次读count
得到的值和第一次读count
得到的值不一样(因为事务1新增了一条数据),这叫幻读,不隔离新增的数据。第一次读aa
和第二次读aa
得到的值是不一样的,对刚提交的值可见,不隔离已经存在的数据。 不可以重复读,读到的数据是不一样的(如果成功修改)。读取未提交是什么意思呢? 事务2执行到一半时,事务1 还未提交:
- 事务2中 第二次读count
得到的值和第一次读count
得到的值不一样(因为事务1新增了一条数据),这叫幻读,不隔离新增的数据。
- 事务2中 第一次读aa
和第二次读aa
得到的值是不一样的(事务1未提交),对最新版本的值可见,不隔离已经存在的数据。 不可以重复读,读到的数据是不一样的。
- 如果此时事务1因为其他原因回滚了,事务2第二次读到的数据是无意义的,因为修改没有发生(回滚了),这叫脏读 。
在现实环境中,串行化一般不会被使用,因为性能太低。
如果对一致性有要求,比如转账交易,那么要使用可重复读,并发性能相对较差。 原因是,为了实现可重复读,在对更新记录加锁时,除了使用记录锁,还可能会使用间隙锁
锁住区间(看update语句的where条件),这会增加其他事务等待时间。
如果对一致性要求不高,一般使用读取已提交, 由于不考虑重复读,在加锁时一般只加记录锁,不会使用间隙锁,并发性较好,据说使用的最多。
隔离性的问题解决了,但是如果在事务提交后,事务的数据还没有真正落到磁盘上,此时数据库奔溃了,事务对应的数据会不会丢?
事务会保证数据不会丢,当数据库因不可抗拒的原因奔溃后重启,它会保证:
- 成功提交的事务,数据会保存到磁盘
- 未提交的事务,相应的数据会回滚
数据库通过事务日志来达到这个目标。 事务的每一个操作(增/删/改)产生一条日志,内容组成大概如下:
磁盘上每个页(保存数据的,不是保存日志的)都记录着最后一个修改该数据操作的LSN。数据库会通过解析事务日志,将修改真正落到磁盘上(写盘),随后清理事务日志(正常情况下)。
这也是数据库在保证数据安全
和性能
这两个点之前的折中办法:
折中的办法就是:
当数据库从崩溃中恢复时,会有以下几个步骤:
- 解析存在的事务日志,分析哪些事务需要回滚,哪些需要写盘(还没来得及写盘,数据库就崩溃了)。
- Redo,进行写盘。检测对应数据所在数据页的LSN,如果数据页的LSN>=事务操作的LSN,说明已经写过盘,不然进行写盘操作。
- Undo, 按照LSN倒序进行回滚
经过这几个阶段,在数据库恢复后,可以达到奔溃前的状态,也保证了数据的一致性。
转自这里