Spring事务简介
事务
事务的概念:
事务是一个操作序列,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。
事务的四个基本要素(ACID):
- 原子性(Atomicity):事务开始后,所有操作要么全部做完,要么全部不做。事务是一个不可分割的整体。
- 一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏。从一种一致状态,达到另一种一致状态。A给B转账,A扣了钱,B加了钱。总钱数在事务前后是一致的。
- 隔离性(Isolation):不同事务之间的操作,互不干扰。A给B转账100,C给A转账200,这两件事是互不干扰的。
- 持久性(Durability):事务完成之后,就对数据库进行了修改,不能取消。
注:我一开始学习的时候,不明白
持久性
这一特性,比如转账完成,转账转错了,就不能回滚吗?这时候,事务已经COMMIT,结束了,当然不能回滚。但是能重新提交另外一个事务来“抵消”错误的转账。持久性是针对一个事务的概念。要想撤回这个事务对数据库产生的影响,只能用另一个事务来抵消
事务并发的问题:
- 丢失更新:两个事务T1和T2读入同一个数据并修改,T2提交的结果覆盖了T1提交的结果,导致T1的修改被丢失。
- 脏读:事务A读取了事务B更新的数据,如果B回滚,那么A读到的数据就是脏数据。事务A的两次读操作分别在事务B的修改之后和回滚之后。
- 不可重复读:事务A多次读取同一数据,事务B在A多次读取期间,对数据进行了更改并提交,导致A多次读取到的结果不一致。事务B写操作,在事务A的两个读操作之间完成(侧重于修改和删除,通过行锁就能解决)。
- 幻读:事务A将所有人的年龄置空,事务B同时插入一条数据。事务A执行结束,发现还有一条数据年龄不为空(侧重新增,需要锁住表才能解决)。但是Innodb对于幻读有解决方案,对于在默认”可重复读”的隔离级别下,普通查询是“快照读”,是看不到别的事务插入数据的,只有在“当前读”的情况下才会出现幻读。
事务的隔离级别:
用不同的隔离级别会解决不同的事务并发问题:
隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交(read uncommited) | 不允许 | 允许 | 允许 | 允许 |
读已提交(read commited) | 不允许 | 不允许 | 允许 | 允许 |
可重复读(repeatable read) | 不允许 | 不允许 | 不允许 | 允许 |
可串行化(serializable) | 不允许 | 不允许 | 不允许 | 不允许 |
用户通过 设置隔离级别 这个入口,来控制大多数的锁。比较少的用显示调用。通过设置合适的隔离级别,数据库会自动使用合适的锁。
读未提交:通过 排他写锁 来实现。两个事务不可同时写同一条数据。
读已提交:大多数数据库的默认级别就是这个,比如Sql Server,Oracle
可重复读:可以通过“共享读锁”和“排他写锁”实现。这是MYSQL默认的隔离级别。
数据库的锁:
- 加锁方式:两阶段协议锁(2PL:Two-Phase Locking) 具体含义是相对于“一次性锁协议”而言的,所谓“一次性锁协议”指的是事务开始时,一次性申请所有锁,之后不再申请任何锁,如果某个锁不可用,则申请不成功,事务不执行,在事务尾端,一次性释放所有锁。这样不会产生死锁问题。两阶段锁协议指的是,整个事务分为两个阶段,前一个阶段是加锁,后一个阶段是解锁。在加锁阶段,事务只能加锁,也可以操作数据,但是不能解锁。直到事务释放第一个锁,就进入解锁阶段,这时候事务只能解锁,也可以操作数据,但是不能加锁。这么做,让解锁不必发生在事务尾端,提高了事务并发度。但是可能会造成死锁。
-
锁的类型:共享锁(S Lock)、排它锁(X Lock)
- 根据锁的粒度 分为 页级锁 行级锁 和 表级锁;
- 根据锁的模式 分为 共享锁(S锁)、排他锁(X锁)、意向共享锁(IS锁)、意向排它锁(IX锁)
- 表级锁:表锁(分S锁、X锁)、MDL锁、意向锁(分S锁、X锁)
- 行级锁:记录锁(record-lock 分S锁、X锁)、间隙锁(Gap lock)、临键锁(Next-key lock)、插入意向锁
共享锁 会防止 排他锁 来加锁,但是允许其他 共享锁 来加锁。
排他锁 会防止其他 排他锁 和 共享锁 来加锁。
简单记忆:意向锁之间不冲突、排它锁与其他锁都冲突、S锁与 IS锁/S锁 不冲突)
表级锁
-
MDL锁: 全称metadata lock,又叫元数据锁。不需要显示使用,方为一个表时,会被自动加上。对一个表进行增删改查时,会加上MDL读锁;修改一个表时,会加上MDL写锁。
-
表锁: 语法示例
-- 锁定student表
lock tables student read; -- 可以执行,此时其他会话,可以读,会被阻塞;不能写,报错
select * from student where id = 1; -- 可以执行
select * from teacher where id = 1; -- 不能执行,只能读锁定的表
insert into student (id, name) values (10, 'studentName'); -- 不能执行,只能读,不能添删改
unlock tables; -- 可以执行,解锁
lock tables student write; -- 可以执行,此时其他会话,可以读写,会被阻塞
unlock tables; -- 可以执行,解锁
- 意向锁: 表锁与行锁冲突,如果加表锁时,要遍历每行是否加了行锁,效率很低,因此首先加意向锁,就简单了。
行级锁:
- 记录锁(Record Lock):单行记录的锁,锁加在索引上。
- 间隙锁(Gap Lock):范围锁,锁定一个范围,不包含记录本身。RR级别下才会有这个锁。
- 临键锁(Next-Key Lock):锁定一个范围,也锁定记录本身。RR级别下才会有这个锁。
- 插入意向锁(Insert Intention Lock):特殊的间隙锁,简称II Gap,代表有插入意向,只有insert才会有这个锁。
快照读 与 当前读:
一致性非锁定读(快照读):普通的select,不包含如下sql:
select * from student where id = 1 lock in share mode;
select * from student where id = 1 for update;
一致性锁定读(当前读):除了上述sql之外的,还有insert、update、delete。
RC级别下,快照读是 读取 被锁定行数据 最新已提交的快照。
RR级别下,快照读是 读取 事务开始时的快照。
加锁机制
RC级别:
只有记录锁
RR级别:
原则1:加锁单位都是next-key lock,锁定的是前开后闭的区间 原则2:访问到的对象才会加锁; 优化1:索引上的等值查询,给唯一索引加锁的时候,next-key lock会退化成行锁。 优化2:索引上的等值查询,向右遍历 且 最后一个值不满足等值条件的时候,next-key lock会退化成间隙锁。
Spring的事务
所有的数据库访问技术都有事务处理机制,这些技术提供了API用来开启事务、提交事务来完成数据操作,或发生错误时回滚数据。
Spring的事务机制是用统一的机制来处理不同数据访问技术的事务处理。它提供了一个PlatformTransactionManager接口,不同的数据库访问技术提供不同的实现。
数据访问技术 | 实现 |
---|---|
JDBC | DataSourceTransactionManager |
JPA | JpaTransactionManager |
Hibernate | HibernateTransactionManager |
JDO | JdoTransactionManager |
分布式事务 | JtaTransactionManager |
声明式事务
用@Transactional
注解的方式选择需要使用事务的方法就是声明式事务,这是AOP的另外一个应用。当此方法被调用时,Spring开启一个新的事务,无异常运行结束时,提交这个事务。
这个注解来自于org.springframework.transaction.annotation包,而不是javax.transaction包。
@Transactional
的属性说明
属性 | 含义 | 默认值 |
---|---|---|
propagation | Propagation.REQUIRED,方法A被调用时,如果没有事务则新建一个事务。当方法A调用方法B时,方法B使用和A相同的事务。方法B有异常需要回滚时,整个事务回滚。 | Propagation.REQUIRED |
Propagation.REQUIRES_NEW,当方法A调用方法B时,无论是否当前有事务都去新建。这样,方法B有异常需要回滚时,A不回滚。 | ||
Propagation.NESTED,跟REQUIRES_NEW类似,但是只支持JDBC,不支持JPA和Hibernate。 | ||
Propagation.SUPPORTS,方法调用时,有事务就用事务,没有就不用。 | ||
Propagation.NOT_SUPPORTED,强制不在事务中执行,若有事务,则在方法调用到结束的阶段,事务都会被挂起。 | ||
Propagation.NEVER,有事务抛出异常。 | ||
Propagation.MANDATORY,强制在事务中执行,若无事务抛出异常。 | ||
isolation | Isolation.READ_UNCOMMITTED | Isolation.DEFAULT |
Isolation.READ_COMMITTED | ||
Isolation.REPEATABLE_READ | ||
Isolation.SERIALIZABLE | ||
Isolation.DEFAULT,使用当前数据库的隔离级别,Mysql默认是REPEATABLE_READ,Oracle和SQLServer默认是READ_COMMITTED。 | ||
timeout | 过期时间 | 默认-1,不过期(当前数据库的事务过去时间) |
readOnly | 是否是只读事务 | false |
rollBackFor | 指定哪些异常可以引起回滚 | Throwable的子类 |
noRollBackFor | 指定哪些异常,不引起回滚 | Throwable的子类 |
上述问题,其实是在一个事务内完成的,跟事务之间的执行顺序没关系。 由于对数据库的事务,锁,隔离级别已经忘记了,为了加深理解,做如下探索:
最近遇到遇到一个需求: 在一个请求(http接口请求)中完成下面两个过程 过程1:用户的操作产生了一些数据,需要将这些数据,写入mysql数据库不同的表。 过程2:把用户的这个操作产生的数据(比如过程1产生的自增ID),从数据库中查出来,进行二次加工,用到其他地方。
需求1:过程1执行完毕,再执行过程2,如果过程1执行出错(error),则不执行过程2; 这个需求很简单,只要是按照顺序写的同步代码,问题不大。
设计:过程1产生的数据是一些数据,存储到数据库之后,过程2再从数据库取数。
问题1:过程1的事务能控制,保证用户产生的非结构化数据,全部写入数据库或者全部不写入数据库。 如果加入了过程2,在同一个事务内,能否存入数据库后,直接查到?
问题2:springboot和mysql的默认情况,对是如何处理的?
带着以上问题,我搜索并实践,最终得到如下结果: