您的位置:首页 > 其它

关于事务的记录

2018-03-07 19:04 120 查看
Spring的事务:
Spring的事务是数据库对事务的支持,数据库不支持Spring也是无法做到的,Spring能操作事务也是因为控制了和数据库的连接。纯JDBC用事务步骤:
1、获取连接 Connection con = DriverManager.getConnection() 
2、开启事务con.setAutoCommit(true/false); 
3、执行CRUD 
4、提交事务/回滚事务 con.commit() / con.rollback(); 
5、关闭连接 conn.close();
使用Spring也只是可以让我们省去步骤2和4,在我们加上注解后,用代理控制的,即在前后加上了开启事务和提交/回滚的代码。
事务的四个特性ACID:
A:原子性(Atomicity):事务是一个不可分割的整体,事务中要么都执行,要么都不执行。
C:一致性(Consistency):事务的前后有一致性,比如事务前A有100,B有200,事务后A有50,B有250,前后状态要一致。
I:隔离性(Isolation):多个事务执行时,一个事务的执行不影响另外的事务。
D:持久性(Durability):一个事务只要被提交了,那对数据库的修改就是永久的。

可以这样说:保证了ACID就保证了是一个正确的事务操作,那怎么来保证ACID呢?突然感觉这个问题好大,哈哈。根据一些资料,实现ACID的核心技术是并发控制和日志技术。
并发控制:MVCC,2PL,OCC 保证并发操作的正确性。
日志技术:Undo/Redo,WAL协议 保证故障场景下可以恢复。
保证原子性:并发控制+日志
查询和事务执行的之间状态别人看不到,事务中断,可以回退,消除影响。
保证一致性:应用层定义的完整性约束
保证隔离性:并发控制
调度并发的事务对数据库的操作,消除事务之间的干扰造成的异常结果。
保证持久性:日志技术
保证系统crash时,可以通过日志来恢复数据。
何为并发控制:2PL,MVCC,OCC都能实现正确的并发执行,各适合不同的场景。
MVCC:事务不持有锁,但是可以保证在执行过程中这个事务只能看到整个数据库过去的某个时刻的一致性状态,即使在事务执行过程中,别的事务更改了这个事务需要操作的数据。乐观锁。
2PL:两阶段封锁,事务在读、写每个数据对象前需申请持有共享锁/互斥锁,所持有的锁直到事务结束时才释放,事务申请不到锁时,需排队等待。悲观锁。
OCC:多个事务并发执行的时候,互相不阻塞。执行过程中,记录每个事务读写的历史,在事务提交前,检查事务的读写历史是否会造成不可串行化调度,如果是,挑选某个冲突的事务回滚。
2PL是最经典的并发控制方法,MVCC对于短事务和长查询混合负载通常有更好的并发度,OCC方法适合于读写冲突较少的场景,否则大量的事务回滚会造成性能下降。

首先是一致性:
初次看到ACID就感觉,一致性应该是最重要的,因为不管怎样,最终不就是想要得到这种一致性的效果嘛。
那怎么来保证一致性呢?
原子性:

实现原子性,需要通过数据库的日志,如果事务中操作失败(断电、软硬件问题),就可以通过回溯日志,将已经执行成功的操作撤销,从而实现回滚。
recovery的过程:读取日志的REDO,将已经执行成功但未写入磁盘的操作真正的写入磁盘,保证持久性。读取所有的UNDO,撤销所有执行了一部分但未提交的操作。
原子性实现了,可以保证A:100元,B:200元,A转B50元,B是250元,但是在这期间有另外的事务最终完成,是B转C100元,覆盖了A转B事务的结果,B变为了100元,咋办?为此引入了隔离性。
隔离性:即每一个事务看到的数据总是一致的,就好像其他并发事务不存在一样,即多个事务并发的结果,和他们串行执行的结果一样,怎么控制?就是上面的三种方式了MVCC,2PL,OCC,比如最经典的2PL,两阶段锁。

先来过一遍隔离性的理论知识,为了解决隔离性的问题,sql标准定义了四个隔离级别:
1、读未提交:(Read Uncommitted) 可导致脏读。
2、读已提交:(Read Committed)避免脏读,可导致不可重复读和幻读,大多数数据库默认的隔离级别。
3、可重复读:(Repeatable-Read) 避免脏读和不可重复读,可导致幻读,mysql数据库所默认的级别。
4、序列化:(serializable)避免脏读、不可重复读和幻读,串行化,效率低。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
大多数的数据库默认隔离级别为 Read Commited,比如 SqlServer、Oracle
少数数据库默认隔离级别为:Repeatable Read 比如: MySQL InnoDB

用户连接到数据库时,可以修改隔离模式,为了便于我们代码中修改,所以Spring也定义了隔离级别,便于我们操作数据库的隔离级别。
Spring的隔离级别:
ISOLATION_DEFAULT:这是PlatfromTransactionManager 默认的隔离级别,即使用数据库默认的事务隔离级别。另外四个与 JDBC 的隔离级别相对应。 
ISOLATION_READ_UNCOMMITTED:对应数据库的读未提交。
ISOLATION_READ_COMMITTED:对应数据库的读已提交。
ISOLATION_REPEATABLE_READ:对应数据库的可重复读
ISOLATION_SERIALIZABLE:对应数据库的序列化

脏读:一事务对数据进行了增删改,但未提交,另一事务可以读取到未提交的数据。如果第一个事务这时候回滚了,那么第二个事务就读到了脏数据。
不可重复读:一个事务中发生了两次读操作,第一次读操作和第二次操作之间,另外一个事务对数据进行了修改,这时候两次读取的数据是不一致的。
幻读:第一个事务对一定范围的数据进行批量修改,第二个事务在这个范围增加一条数据,这时候第一个事务就会丢失对新增数据的修改。



事务嵌套:如Service A 的method A()调用Service B的method B(),就有如下四种方案:
PROPAGATION_REQUIRED:(spring 默认) 如果ServiceB.methodB() 的事务级别定义为 PROPAGATION_REQUIRED,那么执行 ServiceA.methodA() 的时候spring已经起了事务,这时调用 ServiceB.methodB(),ServiceB.methodB() 看到自己已经运行在 ServiceA.methodA() 的事务内部,就不再起新的事务。 假如 ServiceB.methodB() 运行的时候发现自己没有在事务中,他就会为自己分配一个事务。 这样,在 ServiceA.methodA() 或者在 ServiceB.methodB() 内的任何地方出现异常,事务都会被回滚。 
PROPAGATION_REQUIRES_NEW:比如我们设计ServiceA.methodA()的事务级别为
4000
PROPAGATION_REQUIRED,ServiceB.methodB() 的事务级别为 PROPAGATION_REQUIRES_NEW。那么当执行到 ServiceB.methodB() 的时候,ServiceA.methodA() 所在的事务就会挂起,ServiceB.methodB() 会起一个新的事务,等待 ServiceB.methodB() 的事务完成以后,它才继续执行。他与 PROPAGATION_REQUIRED 的事务区别在于事务的回滚程度了。因为 ServiceB.methodB() 是新起一个事务,那么就是存在两个不同的事务。如果 ServiceB.methodB() 已经提交,那么 ServiceA.methodA() 失败回滚,ServiceB.methodB() 是不会回滚的。如果 ServiceB.methodB() 失败回滚,如果他抛出的异常被 ServiceA.methodA() 捕获,ServiceA.methodA() 事务仍然可能提交(主要看B抛出的异常是不是A会回滚的异常)。 
PROPAGATION_SUPPORTS:假设ServiceB.methodB() 的事务级别为 PROPAGATION_SUPPORTS,那么当执行到ServiceB.methodB()时,如果发现ServiceA.methodA()已经开启了一个事务,则加入当前的事务,如果发现ServiceA.methodA()没有开启事务,则自己也不开启事务。这种时候,内部方法的事务性完全依赖于最外层的事务。 
PROPAGATION_NESTED:现在的情况就变得比较复杂了, ServiceB.methodB()的事务属性被配置为 PROPAGATION_NESTED, 此时两者之间又将如何协作呢? ServiceB.methodB如果rollback, 那么内部事务(即 ServiceB.methodB) 将回滚到它执行前的 SavePoint 而外部事务(即 ServiceA#methodA) 可以有以下两种处理方式:

a、捕获异常,执行异常分支逻辑 void methodA() { try { ServiceB.methodB(); } catch (SomeException) { // 执行其他业务, 如 ServiceC.methodC(); } } 这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果, 如果 ServiceB.methodB 失败, 那么执行 ServiceC.methodC(), 而 ServiceB.methodB 已经回滚到它执行之前的 SavePoint, 所以不会产生脏数据(相当于此方法从未执行过), 这种特性可以用在某些特殊的业务中, 而 PROPAGATION_REQUIRED 和 PROPAGATION_REQUIRES_NEW 都没有办法做到这一点。 
b、外部事务回滚/提交 代码不做任何修改, 那么如果内部事务(ServiceB.methodB) rollback, 那么首先 ServiceB.methodB 回滚到它执行之前的 SavePoint(在任何情况下都会如此), 外部事务(即 ServiceA.methodA) 将根据具体的配置决定自己是 commit 还是 rollback 
另外三种事务传播属性基本用不到。
那么这么多隔离级别,我们是怎么来控制的?怎么来保证隔离级别正常的运行?怎么构建这些隔离级别体系?
就产生了数据库锁,用数据库锁来构建隔离级别。
所以本质上,是读写锁不同的应用导致了不同的隔离级别。

说到两阶段锁,先来说下 一阶段锁,
因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,即一阶段锁,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁),所以两阶段锁用的不好可导致死锁?。
在同一个事务内,对所涉及的所有数据项进行先加锁,然后才对所有的数据项解锁,加锁和解锁是两个阶段,且加锁阶段不允许解锁,解锁阶段不允许加锁,即加锁和解锁不能交叉执行。

加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。



这种方式虽然无法避免死锁,但是两阶段锁协议可以保证事务的并发调度室串行化的(此串行化并非隔离级别中的串行化)。
另外还有一个锁管理(Lock Manager),管理锁的申请和释放,通过哈希表管理锁资源,也管理着锁和数据的关联关系,包括:哪些事务对特定数据加了锁。哪些事务在等待对特定数据加锁。

这里的锁就是悲观锁(读锁和写锁)。
悲观锁,即悲观的认为我这个事务在操作这个数据的时候,别的事务肯定也会同时操作这个数据。所以当前事务将所有涉及操作的对象加锁,操作完成后释放给其它对象使用。为了尽可能提高性能,发明了各种粒度(数据库级/表级/行级……)/各种性质(共享锁/排他锁/共享意向锁/排他意向锁/共享排他意向锁……)的锁。为了解决死锁问题,又发明了两阶段锁协议/死锁检测等一系列的技术。简单来说就是读读共享,读写互斥,写读互斥,写写互斥。
隔离级别和锁的关系,这个不同的数据库不同引擎实现方式都不同,而且各具体数据库并不一定完全实现了上述4个隔离级别,如,Oracle只提供Read committed和Serializable两个标准隔离级别,另外还提供自己定义的Read only隔离级别,SQL Server除支持4个隔离级别外,还支持一个叫做“快照”的隔离级别,但严格来说它是一个用MVCC实现的Serializable隔离级别。MySQL 支持全部4个隔离级别,但在具体实现时,有一些特点,比如在一些隔离级别下是采用MVCC一致性读,但某些情况下又不是。

乐观锁相比较悲观锁就比较简单了,她是很乐观的,读取数据的时候不加锁,那不同的事务就可以同时看到同一对象(一般是数据行)的不同历史版本。如果有两个事务同时修改了同一数据行,那么在较晚的事务提交时进行冲突检测。实现也有两种,一种是通过日志UNDO的方式来获取数据行的历史版本,一种是简单地在内存中保存同一数据行的多个历史版本,通过时间戳来区分。通常用的就是版本号来控制,并发执行时一个更新了数据时同时更新版本+1,另一个更新的时候发现版本和读数据的时候不匹配,就更新不了。update xxx set num = num-1,version = version+1 where version = nowVersion and ...。

持久性:通过日志来实现,redo,undo操作。
个人的理解大概就这样,记录一下,数据库在事务的整个过程中,redo,undo怎么操作转换,怎么刷盘的,em,,,还不知道

,有待看书研究。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: