事务的特性
- 原子性(Atomicity):事务是一个原子操作,由一系列动作组成。事务的原子性确保动作要么全部完成,要么完全不起作用。
- 一致性(Consistency):一旦事务完成(不管成功还是失败),系统必须确保它所建模的业务处于一致的状态,而不会是部分完成部分失败。在现实中的数据不应该被破坏。
- 隔离性(Isolation):可能有许多事务会同时处理相同的数据,因此每个事务都应该与其他事务隔离开来,防止数据损坏。
- 持久性(Durability):一旦事务完成,无论发生什么系统错误,它的结果都不应该受到影响,这样就能从任何系统崩溃中恢复过来。通常情况下,事务的结果被写到持久化存储器中。
Spring支持编程式事务管理以及声明式事务管理两种方式。
1. 编程式事务管理
编程式事务管理是侵入性事务管理,使用TransactionTemplate或者直接使用PlatformTransactionManager,对于编程式事务管理,Spring推荐使用TransactionTemplate。
2. 声明式事务管理
声明式事务管理建立在AOP之上,其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,执行完目标方法之后根据执行的情况提交或者回滚。
编程式事务每次实现都要单独实现,但业务量大功能复杂时,使用编程式事务无疑是痛苦的,而声明式事务不同,声明式事务属于无侵入式,不会影响业务逻辑的实现,只需要在配置文件中做相关的事务规则声明或者通过注解的方式,便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是Spring倡导的非侵入式的编程方式。唯一不足的地方就是声明式事务管理的粒度是方法级别,而编程式事务管理是可以到代码块的,但是可以通过提取方法的方式完成声明式事务管理的配置。
事务的传播机制
在 Spring 中,当事务方法调用非事务方法时,非事务方法里的数据库操作通常不受事务方法的事务影响。
事务的传播性一般用在事务嵌套的场景,比如一个事务方法里面调用了另外一个事务方法,那么两个方法是各自作为独立的方法提交还是内层的事务合并到外层的事务一起提交,这就是需要事务传播机制的配置来确定怎么样执行。
常用的事务传播机制如下:
- PROPAGATION_REQUIRED --propagation_required Spring默认的传播机制,能满足绝大部分业务需求,如果外层有事务,则当前事务加入到外层事务,一块提交,一块回滚。如果外层没有事务,新建一个事务执行
- PROPAGATION_REQUES_NEW --propagation_request_new 该事务传播机制是每次都会新开启一个事务,同时把外层事务挂起,当当前事务执行完毕,恢复上层事务的执行。如果外层没有事务,执行当前新开启的事务即可
- PROPAGATION_SUPPORT --propagation_support 如果外层有事务,则加入外层事务,如果外层没有事务,则直接使用非事务方式执行。完全依赖外层的事务
- PROPAGATION_NOT_SUPPORT --propagation_not_support 该传播机制不支持事务,如果外层存在事务则挂起,执行完当前代码,则恢复外层事务,无论是否异常都不会回滚当前的代码
- PROPAGATION_NEVER --propagation_never 该传播机制不支持外层事务,即如果外层有事务就抛出异常
- PROPAGATION_MANDATORY --propagation_mandatory 与NEVER相反,如果外层没有事务,则抛出异常
- PROPAGATION_NESTED --propagation_nested 该传播机制的特点是可以保存状态保存点,当前事务回滚到某一个点,从而避免所有的嵌套事务都回滚,即各自回滚各自的,如果子事务没有把异常吃掉,基本还是会引起全部回滚的。
传播规则回答了这样一个问题:一个新的事务应该被启动还是被挂起,或者是一个方法是否应该在事务性上下文中运行。
回滚规则
在默认设置下,事务只在出现运行时异常(runtime exception)时回滚,而在出现受检查异常(checked exception)时不回滚(这一行为和EJB中的回滚行为是一致的)。
不过,可以声明在出现特定受检查异常时像运行时异常一样回滚。同样,也可以声明一个事务在出现特定的异常时不回滚,即使特定的异常是运行时异常。
Spring声明式事务配置参考
事物配置中有哪些属性可以配置?以下只是简单的使用参考
- 事务的传播性: @Transactional(propagation=Propagation.REQUIRED)
- 事务的隔离级别: @Transactional(isolation = Isolation.READ_UNCOMMITTED)
读取未提交数据(会出现脏读, 不可重复读) 基本不使用
- 只读: @Transactional(readOnly=true) 该属性用于设置当前事务是否为只读事务,设置为true表示只读,false则表示可读写,默认值为false。
- 事务的超时性: @Transactional(timeout=30)
- 回滚: 指定单一异常类:@Transactional(rollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class})
事务的隔离级别 事务的隔离级别定义一个事务可能受其他并发务活动活动影响的程度,可以把事务的隔离级别想象为这个事务对于事物处理数据的自私程度。 在一个典型的应用程序中,多个事务同时运行,经常会为了完成他们的工作而操作同一个数据。并发虽然是必需的,但是会导致以下问题: 脏读(Dirty read) 脏读发生在一个事务读取了被另一个事务改写但尚未提交的数据时。如果这些改变在稍后被回滚了,那么第一个事务读取的数据就会是无效的。 不可重复读(Nonrepeatable read) 不可重复读发生在一个事务执行相同的查询两次或两次以上,但每次查询结果都不相同时。这通常是由于另一个并发事务在两次查询之间更新了数据。 幻读(Phantom reads) 幻读和不可重复读相似。当一个事务(T1)读取几行记录后,另一个并发事务(T2)插入了一些记录时,幻读就发生了。在后来的查询中,第一个事务(T1)就会发现一些原来没有的额外记录。 幻读重点在新增或删除。
第一种隔离级别:Read uncommitted(读未提交)
如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据,该隔离级别可以通过“排他写锁”,但是不排斥读线程实现。这样就避免了更新丢失,却可能出现脏读,也就是说事务B读取到了事务A未提交的数据
解决了更新丢失,但还是可能会出现脏读
第二种隔离级别:Read committed(读提交) 如果是一个读事务(线程),则允许其他事务读写,如果是写事务将会禁止其他事务访问该行数据,该隔离级别避免了脏读,但是可能出现不可重复读。事务A事先读取了数据,事务B紧接着更新了数据,并提交了事务,而事务A再次读取该数据时,数据已经发生了改变。
解决了更新丢失和脏读问题
第三种隔离级别:Repeatable read(可重复读取) 可重复读取是指在一个事务内,多次读同一个数据,在这个事务还没结束时,其他事务不能访问该数据(包括了读写),这样就可以在同一个事务内两次读到的数据是一样的,因此称为是可重复读隔离级别,读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务(包括了读写),这样避免了不可重复读和脏读,但是有时可能会出现幻读。(读取数据的事务)可以通过“共享读镜”和“排他写锁”实现。不会禁止新增行事务,导致两次总表查询不一致
解决了更新丢失、脏读、不可重复读、但是还会出现幻读
第四种隔离级别:Serializable(可序化) 提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行,如果仅仅通过“行级锁”是无法实现序列化的,必须通过其他机制保证新插入的数据不会被执行查询操作的事务访问到。序列化是最高的事务隔离级别,同时代价也是最高的,性能很低,一般很少使用,在该级别下,事务顺序执行,不仅可以避免脏读、不可重复读,还避免了幻读
解决了更新丢失、脏读、不可重复读、幻读(虚读)
使用 Spring Boot + Sleuth + Zipkin 实现链路跟踪
InnoDB 的 MVCC 实现机制
MVCC 的实现方式是通过为每个事务创建一个可见性版本( Version ),而不是直接对数据进行加锁。每个版本都有一个时间戳,用于表示该版本的创建时间。当一个事务开始时,它会获得一个时间戳,并且只能看到在该时间戳之前已经提交的版本。当一个事务对数据进行修改时,会创建一个新的版本,并将该版本的时间戳设为当前时间戳。
主要依赖于:三大法宝( 隐藏字段、Read View、undo log )。
通过数据可见性算法( DB_TRX_ID 事务 ID) 和 Read View (快照) 来判断数据的可见性,如果不可见,通过数据行的另一个隐藏字段 DB_ROLL_PRT(回滚指针)找到 undo log 中的历史版本。
读操作分类与实现
快照读(Snapshot Read)
- 操作类型:普通SELECT语句
- 实现原理:基于第一次SELECT时创建的Read View
- 隔离级别支持: READ COMMITTED:每次SELECT创建新Read ViewREPEATABLE READ:使用第一次SELECT的Read View
当前读(Current Read)
- 操作类型:SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE、INSERT、UPDATE、DELETE
- 实现原理:读取最新已提交数据并加锁
- 锁机制: 记录锁(Record Locks)间隙锁(Gap Locks)临键锁(Next-Key Locks)
快照读读到的是第一次查询之前所插入的数据,而当前读,每次读取的是最新数据,如果多次查询有其他事务插入,就会产生幻读的效果,因此当前读必须使用临键锁( Next-key lock ),防止幻读。
什么是当前读和快照读
当前读
像select lock in share mode(共享锁), select for update ; update, insert ,delete(排他锁)这些操作都是一种当前读。
当前读就是它读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁
快照读
快照读的前提是隔离级别不是串行级别,串行级别下的快照读会退化成当前读;
之所以出现快照读的情况,是基于提高并发性能的考虑,快照读的实现是基于多版本并发控制,即MVCC,可以认为MVCC是行锁的一个变种,但它在很多情况下,避免了加锁操作,降低了开销;既然是基于多版本,即快照读可能读到的并不一定是数据的最新版本,而有可能是之前的历史版本
不加锁的简单的 SELECT 都属于快照读,例如: SELECT * FROM t WHERE id=1
与 快照读 相对应的则是 当前读,当前读就是读取最新数据,而不是历史版本的数据。加锁的 SELECT 就属于当前读,例如: SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM t WHERE id=1 FOR UPDATE;
数据库隔离级别
要说MVCC,就不能脱离数据库的隔离级别。
大家都知道数据库事务具备ACID特性,即Atomicity(原子性) Consistency(一致性), Isolation(隔离性), Durability(持久性)
原子性:要执行的事务是一个独立的操作单元,要么全部执行,要么全部不执行
一致性:事务的一致性是指事务的执行不能破坏数据库的一致性,一致性也称为完整性。一个事务在执行后,数据库必须从一个一致性状态转变为另一个一致性状态。
隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(durability):事务一旦提交,则其所有的修改将会保存到数据库。即使此时系统崩溃,修改的数据也不会丢失。
读未提交(READ UNCOMMITED)->读已提交(READ COMMITTED)->可重复读(REPEATABLE READ)->序列化(SERIALIZABLE)。隔离级别依次增强,但是导致的问题是并发能力的减弱。
MVCC适用的隔离级别
MVCC只在REPEATABLE READ(可重复读)和READ COMMITTED(读已提交)两个隔离级别下工作。
REPEATABLE READ读取之前系统版本号的记录,保证同一个事务中多次读取结果一致。
REPEATABLE READ隔离级别下,MVCC具体操作:
SELECT操作,InnoDB会根据以下两个条件检查每行记录:
a. InnoDB只查找创建版本号早于或等于当前系统版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
b. 行的删除版本号要么未定义,要么大于当前的系统版本号(在当前事务开始之后删除的)。这可以确保事务读取到的行,在事务开始之前未被删除。
READ COMMITTED读取最新的版本号记录,就是所有事务最新提交的结果。
其他两个隔离级别和MVCC不兼容。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE会对所有读取的行都加锁。
MVCC适用的隔离级别
MVCC只在REPEATABLE READ和READ COMMITTED两个隔离级别下工作。
REPEATABLE READ读取之前系统版本号的记录,保证同一个事务中多次读取结果一致。
REPEATABLE READ隔离级别下,MVCC具体操作:
SELECT操作,InnoDB会根据以下两个条件检查每行记录:
a. InnoDB只查找创建版本号早于或等于当前系统版本号的数据行,这样可以确保事务读取的行,要么是在事务开始前已经存在的,要么是事务自身插入或者修改过的。
b. 行的删除版本号要么未定义,要么大于当前的系统版本号(在当前事务开始之后删除的)。这可以确保事务读取到的行,在事务开始之前未被删除。
READ COMMITTED读取最新的版本号记录,就是所有事务最新提交的结果。
其他两个隔离级别和MVCC不兼容。READ UNCOMMITTED总是读取最新的数据行,而不是符合当前事务版本的数据行。SERIALIZABLE会对所有读取的行都加锁。
MVCC的实现原理
每一行记录有三个隐藏键,分别为DATA_TRX_ID、DATA_ROLL_PTR、DB_ROW_ID,其中:
DATA_TRX_ID
记录最近更新这条行记录的事务 ID,大小为 6 个字节
DATA_ROLL_PTR
表示指向该行回滚段(rollback segment)的指针,大小为 7 个字节,InnoDB 便是通过这个指针找到之前版本的数据。该行记录上所有旧版本,在 undo 中都通过链表的形式组织。
DB_ROW_ID
行标识(隐藏单调自增 ID),大小为 6 字节,如果表没有主键,InnoDB 会自动生成一个隐藏主键,即此列。
undo log
MySQL InnoDB是使用undo log实现多版本并发控制(MVCC)。事务未提交之前,undo log保存未提交数据之前的数据版本,当读取某一行被其他事务操作时,可以从undo log中分析出该行之前记录的数据。 undo log中的数据作为数据旧版本快照,提供事务之间的并发处理。
总结
1、MVCC的实现,是通过保存数据在某个时间点的快照来实现的。
2、因此一个事务只需在启动时声明:以我启动时刻为准;
如果一个数据版本是在我启动前生成的,就认;
启动后才生成的,我不认,必须要找到它的上一个版本;
若“上个版本”也不可见,那就继续往前找;
如果是这个事务自己更新的数据,自己还是认的。
分布式事务
1、⼆阶段提交(2PC)强⼀致性。
2、柔性事务(最终一致性)
TCC事务
全称:Try-Confifirm-Cancel(可以理解为sql中的Lock、Commit、Rollback)
这个其实是用到了补偿的概念,分为了三个阶段:
1)Try阶段:这个阶段说的是对各个服务的资源做检测以及对资源进行锁定或者预留
2)Confirm阶段:这个阶段说的是在各个服务中执行实际的操作
3)Cancel阶段:如果任何一个服务的业务方法执行出错,那么这里就需要进行补偿,就是执行已经执行成功的业务逻辑的回滚操作
本地消息表
本地消息表这个⽅案最初是ebay提出的分布式事务完整⽅案
这个大概意思是这样的
1)A系统在自己本地一个事务里操作同时,插入一条数据到消息表
2)接着A系统将这个消息发送到MQ中去
3)B系统接收到消息之后,在一个事务里,往自己本地消息表里插入一条数据,同时执行其他的业务操作,如果这个消息已经被处理过了,那么此时这个事务会回滚,这样保证不会重复处理消息
4)B系统执行成功之后,就会更新自己本地消息表的状态以及A系统消息表的状态
5)如果B系统处理失败了,那么就不会更新消息表状态,那么此时A系统会定时扫描自己的消息表,如果有没处理的消息,会再次发送到MQ中去,让B再次处理
6)这个方案保证了最终一致性,哪怕B事务失败了,但是A会不断重发消息,直到B那边成功为止