推荐阅读 https://blog.csdn.net/xifeijian/article/details/20313977

以 MySQL 数据库 InnoDB 为例,可拓展了解

  • 各隔离级别的事务在执行 DML 时,分别显式地不加锁、使用悲观锁、使用乐观锁时有什么表现,对应锁的类型是什么
  • MVCC 原理
  • SQL 索引与查询优化

数据库事务

由事务开始与事务结束之间执行的全部数据库操作组成,是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

ACID 特性

  • 原子性(Atomicity)
    事务执行要么全部成功,要么全部失败
  • 一致性(Consistency)
    一致性确保了任何事物都会使数据库从一种合法的状态变为另一种合法的状态。通过定义的各种规则,包括约束(constraints)、级联(cascades)、触发器(triggers)以及它们的组合来保证写入数据库的所有数据都必须是合法的。一致性并不能保证事物(程序)的正确性,换句话说事物的一致性并不一定如程序员所期望的那样(这应该是由应用层代码来负责的),它只能保证数据库中的所有数据都不会违反定义好的规则,不管程序有没有发生错误甚至是发生了任何错误都不会违反定义好的规则。
  • 隔离性(Isolation)

    可见性判断 https://www.jianshu.com/p/03d1bf80f7e8

一个事务所做的修改在最终提交以前,对其他事务通常来说是不可见的。在 SQL 标准中定义了四种隔离级别,每一种级别都规定了一个事务中所作的修改,哪些在事务内和事务间可见,哪些不可见。较低级别的隔离通常可以执行更高的并发,系统的开销也更低。

  • 持久性(Durability)
    持久性保证了一个事物一旦被提交以后,其状态就保持不变,甚至是发生了主机断电、奔溃、错误等。例如,在关系数据库中,一旦一组 sql 语句被执行后,其结果就被永久保存(甚至事物刚被提交数据库系统就发生了奔溃)。为了主机抵御断电的风险,事物(或者是事物的结果)必须被记录在永久性存储中

    隔离级别

    R(EAD) U(NCOMMITTED) 未提交读

    事务中的修改,即使没提交,对其他事务也都可见。事务可以读取未提交的数据,称为脏读(Dirty Read)。这个级别会导致很多问题,从性能上来说,READ UNCOMMITTED 不会比其他的级别好太多,但却缺乏其他级别的很多好处。在实际应用中一般很少使用。

R(EAD) C(OMMITED) 已提交读/不可重复读

大多数数据库系统的默认隔离级别是 READ COMMITTED. 该级别满足前面提到的隔离性的简单定义:一个事务开始时,只可见已提交的事务所作的修改。也就是说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。之所以又称为“不可重复读”,是因为两次执行同样的查询,可能(即在其他事务修改的提交前后)会得到不一样的结果。

R(EAPEATABLE) R(EAD) 可重复读

MySQL 数据库系统的默认隔离级别。REAPEATABLE READ 解决了脏读的问题,保证了在同一个事务中多次读取同样记录的结果是一致的。但是理论上,该级别还是无法解决另外一个问题——“幻读”(Phantom Read).

幻读(Phantom Read),指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围内的记录时,会产生幻行。对于 MySQL数据库系统而言,InnoDB 和 XtraDB 存储引擎通过 MVCC(多版本并发控制)解决了幻读的问题。MVCC 的核心原理是版本快照,需要注意的是;MVCC 只对 RC 和 RR 两个级别起作用,而 RR 级别之所以解决了幻读则是通过 MVCC 的GAP间隙锁。

另外,由于 RC 和 RR 级别的特点,因此在业务中用到该隔离级别时,如果数据处理不当,则可能引入另外一个问题——丢失更新(Lost Update)。
在数据库层面,通常我们是通过悲观锁(依赖的是数据库的锁实现)或乐观锁来解决该问题。

丢失更新 (Lost Update),指当两个或多个事务选择同一行数据,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了由其他事务所做的更新。 例如,两个编辑人员制作了同一文档的电子副本。每个编辑人员独立地更改其副本,然后保存更改后的副本,这样就覆盖了原始文档。最后保存其更改副本的编辑人员覆盖另一个编辑人员所做的更改。如果在一个编辑人员完成并提交事务之前,另一个编辑人员不能访问同一文件,则可避免此问题。 “ABA”是典型的丢失更新案例

SERIALIZABLE 串行化

最高的隔离级别。它通过强制事务串行(排队)执行,避免了前面说的幻读问题。简单来说,串行化会在读取的每一行数据上都加锁,一旦出现有并发操作,可能导致大量的超时和锁竞争(饿死、死锁)问题。所以在实际业务中,几乎不会用该级别。


死锁

死锁是指两个或者多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环的现象。

  • 当多个事务试图以不同的顺序锁定资源时,就可能会产生死锁;
  • 多个事务同时锁定同一个资源时,也会产生死锁

数据库系统往往都实现了各种死锁检测和死锁超时机制。对于 MySQL 数据库的 InnoDB 引擎而言,目前处理死锁的方法是——将持有最少行级排他锁的事务进行回滚。 锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁,有些则不会。死锁产生的原因,有些是因为真正的数据冲突,有些完全是由于存储引擎的实现方式导致。 死锁发生以后,只有部分或者完全回滚其中一个事务,才能打破死锁。大多数情况下,只需要重新执行因死锁回滚的事务即可。

所以,发生死锁有四个必要条件
更多可参见 https://blog.csdn.net/wljliujuan/article/details/79614019

  1. 互斥
  2. 不可剥夺
  3. 请求与保持
  4. 循环等待

延伸-服务层的事务传递(传播)

spring中的事务

目的:在同一个 JVM 中,存在事务嵌套或依赖时,为了解决事务对数据处理的“一致性”问题。

也就是说如果事务2的执行依赖于事务1的执行结果,那么对事务2的处理起决定性作用的为事务1

Spring中事务的定义
Propagation :
  key属性确定代理应该给哪个方法增加事务行为。这样的属性最重要的部份是传播行为。有以下选项可供使用:

  • PROPAGATION_REQUIRED—支持当前事务,如果当前没有事务,就新建一个事务。
  • PROPAGATION_SUPPORTS—支持当前事务,如果当前没有事务,就以非事务方式执行。
  • PROPAGATION_MANDATORY—支持当前事务,如果当前没有事务,就抛出异常。
  • PROPAGATION_REQUIRES_NEW—新建事务,如果当前存在事务,把当前事务挂起。
  • PROPAGATION_NOT_SUPPORTED—以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
  • PROPAGATION_NEVER—以非事务方式执行,如果当前存在事务,则抛出异常。