什么是事务
事务(Transaction):访问并可能更新数据库中各种数据项的一个程序执行单元(unit),它通常由高级数据库操纵语言或编程语言(如SQL,C++或Java)书写的用户程序的执行所引起。当在数据库中更改数据成功时,在事务中更改的数据便会提交,不再改变。否则,事务就取消或者回滚,更改无效。
事务解释:指要做的或所做的事情
事务本质:一系列操作
事务特性:事务是恢复和并发控制的基本单位。
例如 网上购物,其交易过程至少包括以下几个步骤的操作:
- 更改客户所购商品的库存信息;
- 保存客户付款信息;
- 生成订单并且保存到数据库中;
- 更改用户相关信息,例如购物数量等。
在正常情况下,这些操作都将顺利进行,最终交易成功,与交易相关的所有数据库信息也成功地更新。但是,如果在执行的途中遇到突然断电或者其他意外情况,导致这一系列过程中任何一个环节出了差错,例如在更细商品库存信息时发生异常、顾客银行账户余额不足等,都将导致整个交易过程失败。而一旦失败,数据库中所有信息都必须保持交易不影响数据库的状态,即原有的库存信息没有被更新、用户也没有付款、订单也没有生成。否则,数据库的信息将会不一致,或者出现更为严重的不可预测的后果。数据库事务正是用来保证这种情况下交易的平稳性和可预测性的技术。
事务的属性
事务必须满足四个属性,即原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),即ACID四种属性。
(1)原子性
一个事务是一个不可分割的整体,为了保证事务的总体目标,事务必须具有原子性,即当数据修改时,要么全都执行,要么全都不执行。即,不允许事务部分地完成,避免了只执行这些操作的一部分而带来的错误。
(2)一致性
一个事务在执行之前和执行之后,数据库数据必须保持一致性。数据库的一致性状态应该满足模式锁指定的约束条件,那么在完整执行该事务后,数据库仍然处于一致性状态。
例如:银行转账,转账前后两个账户金额之和应保持不变。
(3)隔离性
由并发事务所作的修改必须与任何其它并发事务所作的修改隔离。事务查看数据库时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据。
例如:对任何一对事务T1和T2,对T1而言,T2要么在T1开始之前已经结束,要么在T1完成之后再开始执行。
(4)持久性
也被称为永久性,事务完成以后,DBMS(数据库管理系统)保证它对数据库中数据的修改是永久性的,当系统或介质发生故障时,该修改也永久保持。持久性一般通过数据库备份与恢复来保证。
注意 严格而言,数据库事务属性都是由数据库管理系统来进行保证的,在整个应用程序的运行过程中,应用程序无须去考虑数据库的ACID实现。
一般情况下,通过执行COMMIT(提交)或ROLLBACK(回滚)语句来终止事务。当执行COMMIT语句时,自从事务启动以来对数据库所做的一切更改就成为永久性的,即被写入到磁盘,而当执行ROLLBACK语句时,自从事务启动以来对数据库所做的一切更改都会被撤销,并且数据库中内容返回到事务开始之前所处的状态。无论什么情况,在事务完成时,都能保证回到一致性状态。
并发控制
(1)DBS(数据库系统)一个明显的特点是多个用户共享数据库资源,尤其是多个用户可以同时存取相同数据。
串行控制:如果事务是顺序执行的,即一个事务完成之后,再开始另一事务。
并行控制:如果DBMS可以同时接受多个事务,并且这些事务在时间上可以重叠执行。
(2)并发控制概述
事务是并发控制的基本单位,保证事务ACID的特性是事务处理的重要任务,而并发操作有可能会破坏其ACID特性。
DBMS并发控制机制的责任:对并发操作进行正确调度,保证事务的隔离更一般,确保数据库的一致性。
由于并发操作带来的数据不一致性
如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时可能会发生问题。由于并发操作带来的数据不一致性包括:丢失数据更新、读“脏”数据(脏读)、不可重复读。
(1)更新丢失
两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发并没有被隔离开来。
(2)脏读
一个事务读取到了另一事务未提交的数据操作结果。这是相当危险的,因为很可能所有的操作都被回滚。
(3)不可重复读
不可重复读(Non-repeatable Reads):一个事务对同一行数据重复读取两次,但是却得到了不同的结果。包括以下情况:
虚读:事务T1读取某一数据后,事务T2对其做了修改,当事务T1再次读取该数据时得到与前一次不同的值。
幻读:事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据。这是因为在两次查询过程中有另外一个事务插入数据造成的。
事务隔离级别
为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。
读未提交(Read Uncommitted) | 只处理更新丢失。 | 如果一个事务已经开始写数据,则不允许其他事务同时进行写操作,但允许其他事务读此行数据。 | 可通过“排他写锁”实现。 |
读已提交(Read Committed) | 处理更新丢失、脏读。 | 读取数据的事务允许其他事务继续访问改行数据,但是未提交的写事务将会禁止其他事务访问改行。 | 可通过“瞬间共享读锁”和“排他写锁”实现。 |
可重复读取(Repeatable Read) | 处理更新丢失、脏读和不可重复读取。 | 读取数据的事务将会禁止写事务,但允许读事务,写事务则禁止任何其他事务。 | 可通过“共享读锁”和“排他写锁”实现。 |
序列化(Serializable) | 提供严格的事务隔离。 | 要求失去序列化执行,事务只能一个接一个地执行,不能并发执行。 | 仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。 |
隔离级别越高,越能保证数据的完整性和统一性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
原文链接:https://blog.csdn.net/weixin_39651041/java/article/details/79980202
MySQl
MySql默认的隔离级别为Repeatable Read,因此只会出现幻读的情况。
幻读
事务在插入已经检查过不存在的记录时,惊奇的发现这些数据已经存在了,之前的检测获取到的数据如同鬼影一般。
例子:
在事务1中,查询User表id为1的是用户否存在,如果不存在则插入一条id为1的数据。
select * from User where id = 1;
在事务1查询结束后,事务2往User表中插入了一条id为1的数据。
insert into
User(
id,
name) values (1, 'Joonwhee');
此时,由于事务1查询到id为1的用户不存在,因此插入1条id为1的数据。
insert into ` User`(`id`, `name`) values (1, 'Chillax');
但是由于事务2已经插入了1条id为1的数据,因此此时会报主键冲突,对于事务1 的业务来说是执行失败的,这里事务1 就是发生了幻读,因为事务1读取的数据状态并不能支持他的下一步的业务,见鬼了一样。这里要灵活的理解读取的意思,第一次select是读取,第二次的insert其实也属于隐式的读取,只不过是在mysql的机制中读取的,插入数据也是要先读取一下有没有主键冲突才能决定是否执行插入。
Oracle
Oracle默认的隔离级别为Read Committed,因此可能出现不可重复度和幻读。
不可重复读
同样的条件,你读取过的数据,再次读取出来发现值不一样了。
例子:
在事务1中,JoonWhee读取了自己的工资为1000,但是此时事务1的操作还并没有完成 ,后面还有1次相同的读取操作。
con1 = getConnection();
select salary from employee where employeeName="JoonWhee";
在事务2中,这时财务人员修改了JoonWhee的工资为2000,并提交了事务。
con2 = getConnection(); update employee set salary = 2000 where employeeName = "JoonWhee"; con2.commit();
在事务1中,JoonWhee再次读取自己的工资时,工资变为了2000 。
select salary from employee where employeeName ="JoonWhee";
在一个事务中前后两次读取的结果并不一致,导致了不可重复读。
幻读
同样的条件,第1次和第2次读出来的记录数不一样。
例子:
目前工资为1000的员工有10人。
事务1,读取所有工资为1000的员工,共读取10条记录 。
con1 = getConnection();
Select * from employee where salary =1000;
这时另一个事务向employee表插入了一条员工记录,工资也为1000
con2 = getConnection(); Insert into employee(employeeName,salary) values("Lili",1000); con2.commit();
事务1再次读取所有工资为1000的员工,共读取到了11条记录,这就产生了幻读。
select * from employee where salary =1000;
原文链接:https://blog.csdn.net/v123411739/java/article/details/39298127
扩展阅读: