一、概述
1. ACID
事务:所谓事务,它是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
ACID:是指在可靠的数据库管理系统(DBMS)中,事务(transaction)应该具备四个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability)。
一个支持事务(Transaction)的数据库,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易方的要求。
事务的(ACID)特性是由关系数据库管理系统(RDBMS,数据库系统)来实现的。数据库管理系统采用日志来保证事务的原子性、一致性和持久性。日志记录了事务对数据库所做的更新,如果某个事务在执行过程中发生错误,就可以根据日志,撤销事务对数据库已做的更新,使数据库退回到执行事务前的初始状态。
数据库管理系统采用锁机制来实现事务的隔离性。当多个事务同时更新数据库中相同的数据时,只允许持有锁的事务能更新该数据,其他事务必须等待,直到前一个事务释放了锁,其他事务才有机会更新该数据。
1.1 原子性
原子性是指事务是一个不可再分割的工作单位,事务中的操作要么全部成功,要么全部失败。这是最基本的特性,保证了因为一些其他因素导致数据库异常,或者宕机。
场景:A账户给B账户转账100元钱
分析:在事务中的A账户扣款和B账户加款两条语句,要么都执行,要么就都不执行。否则,如果只执行了扣款语句,
就提交了,此时如果突然断电,就会导致A账号已经发生了扣款,B账号却没收到加款。
解决:在数据库管理系统(DBMS)中,默认情况下一条SQL就是一个单独事务,事务是自动提交的。只有显式的使用start transaction开启一个事务,才能将一个代码块放在事务中执行。保障事务的原子性是数据库管理系统的责任,为此许多数据源采用日志机制。例如,SQL Server使用一个预写事务日志,在将数据提交到实际数据页面前,先写在事务日志上。
1.2 一致性
一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
场景:对银行转帐事务,不管事务成功还是失败,应该保证事务结束后ACCOUNT表中aaa和bbb的存款总额为2000元。
解决:保障事务的一致性,可以从以下两个层面入手
(1) 数据库机制层面
数据库层面的一致性是,在一个事务执行之前和之后,数据会符合你设置的约束(唯一约束,外键约束,Check约束等)和触发器设置。这一点是由SQL SERVER进行保证的。比如转账,则可以使用CHECK约束两个账户之和等于2000来达到一致性目的
(2) 业务层面
对于业务层面来说,一致性是保持业务的一致性。这个业务一致性需要由开发人员进行保证。当然,很多业务方面的一致性,也可以通过转移到数据库机制层面进行保证。
1.3 隔离性
多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
这指的是在并发环境中,当不同的事务同时操纵相同的数据时,每个事务都有各自的完整数据空间。由并发事务所做的
修改必须与任何其他并发事务所做的修改隔离。事务查看数据更新时,数据所处的状态要么是另一事务修改它之前的状态,
要么是另一事务修改它之后的状态,事务不会查看到中间状态的数据。
在Windows中,如果多个进程对同一个文件进行修改是不允许的,Windows通过这种方式来保证不同进程的隔离性。
企业开发中,事务最复杂问题都是由事务隔离性引起的。当多个事务并发时,SQL Server利用加锁和阻塞来保证事务之间不同等级的隔离性。一般情况下,完全的隔离性是不现实的,完全的隔离性要求数据库同一时间只执行一条事务,这样会严重影响性能。想要理解SQL Server中对于隔离性的保障,首先要了解并发事务之间是如何干扰的。
事务之间的相互影响分为几种,分别为:脏读,不可重复读,幻读,丢失更新。
1.4 持久性
持久性,意味着在事务完成以后,该事务所对数据库所作的更改便持久的保存在数据库之中,并不会被回滚。
即使出现了任何事故比如断电等,事务一旦提交,则持久化保存在数据库中。
SQL SERVER通过write-ahead transaction log来保证持久性。write-ahead transaction log的意思是,事务中对数据库的改变在写入到数据库之前,首先写入到事务日志中。而事务日志是按照顺序排号的(LSN)。当数据库崩溃或者服务器断点时,重启动SQL SERVER,SQLSERVER首先会检查日志顺序号,将本应对数据库做更改而未做的部分持久化到数据库,从而保证了持久性。
2. 数据库事务的隔离级别
数据库事务的隔离级别有4个,由低到高依次为Read uncommitted 、Read committed 、Repeatable read 、Serializable ,这四个级别可以逐个解决脏读 、不可重复读 、幻读这几类问题。
(1)Read UnCommitted(读未提交)
最低的隔离级别,一个事务可以读取另一个事务并未提交的更新结果(很少使用)。
(2) Read Committed(读提交)
大部分数据库采用的默认隔离级别,一个事务的更新操作结果只有在该事务提交之后,另一个事务才可以的读取到同一笔数据更新后的结果。
即:一个事务只能看见已经提交事务所做的改变。
(3) Repeatable Read(重复读)
mysql的默认级别,整个事务过程中,对同一笔数据的读取结果是相同的,不管其他事务是否在对共享数据进行更新,也不管更新提交与否。
(4) Serializable(序列化)
最高隔离级别,所有事务操作依次顺序执行。注意这会导致并发度下降,性能最差。通常会用其他并发级别加上相应的并发锁机制来取代它。
不同事务级别带来的并发问题:
(1)脏读(读提交的数据)
脏读意味着一个事务A读取了另一个事务B还未提交的数据。假如B回滚,则事务A读取的是无效的数据。这跟不可重复读类似,但是第二个事务不需要执行提交。
(2)不可重复读
不可重复读意味着在数据库访问中,一个事务范围内两个相同的查询却返回了不同数据。这是由于查询时系统中其他事务修改的提交而引起的。
在基于锁的并行控制方法中,如果在执行select时不添加读锁,就会发生不可重复读问题。
(3)幻读
幻读发生在当两个完全相同的查询执行时,第二次查询所返回的结果集跟第一个查询不相同。幻读,是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
发生的情况:没有范围锁。
以上三种并发问题的区别:
1)脏读主要是读取到了没有提交事物的数据,如果数据回滚,则该读取到的数据有问题;
2)不可重复读主要是第一次读取数据后,该数据发生了修改并提交,导致再次读取时,该数据不一致;
3)幻读主要是第一次读取数据后,发生了新增或者删除(数据条数的变化),导致再次读取时,该数据(条数)不一致.
3. 事务的七种传播行为
事务传播行为类型 | 说明 |
---|---|
propagation_required |
Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行。 |
propagation_supports |
如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行,完全依赖外层的事务。 |
propagation_mandatory |
与NEVER相反,如果外层没有事务,则抛出异常。 mandatory:强制的 /ˈmændətɔːri/ |
propagation_requires_new |
每次都会新开启一个事务,同时把外层事务挂起,当前事务执行完毕后,再恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可。 |
propagation_not_supported |
不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码。 |
propagation_never |
不支持外层事务,即如果外层有事务就抛出异常。 |
propagation_nested |
可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。 nested 嵌套的 /ˈnestɪd/ |
4. 事务锁
悲观锁:一段执行逻辑加上悲观锁,不同线程同时执行时,只能有一个线程执行,其他的线程在入口处等待,直到锁被释放。
乐观锁:一段执行逻辑加上乐观锁,不同线程同时执行时,可以同时进入执行,在最后更新数据的时候要检查这些数据是否
被其他线程修改了(版本和执行初是否相同),没有修改则进行更新,否则放弃本次操作。
悲观锁的实现:
begin;/begin work;/start transaction; (三者选一就可以) //1.查询出商品信息 select status from t_goods where id=1 for update; //2.根据商品信息生成订单 insert into t_orders (id,goods_id) values (null,1); //3.修改商品status为2 update t_goods set status=2; //4.提交事务 commit;/commit work;
乐观锁的实现:
//1.查询出商品信息 select (status,status,version) from t_goods where id=#{id} //2.根据商品信息生成订单
... //3.修改商品status为2 update t_goods set status=2,version=version+1 where id=#{id} and version=#{version};
二、使用步骤
1. 事务隔离级别与传播行为
使用示例参考spring事务和锁.
2. 锁的使用
参考:
spring事务和锁 (如何使用)