关于数据库事务的那些事儿

本文约5000字,建议阅读时间10分钟

关于数据库的事务,相信每个码农都有接触,也相信都遇到过与之相关的坑。
本文旨在归纳总结下事务的概念、原理及使用。
本文针对的主要是MySQL的事务机制以及Spring 的事务管理。


什么是事务

简单挑明下事务的概念,从说事务都会举的一个实际问题引出事务的概念:
用户A想要给用户B转账100元,那么需要做的事情是:

  1. 查询A账户的信息
  2. 如大于100,从A账户中取出100
  3. 查询B账户的信息
  4. 如果B账户一切正常,往B账户中存入100

上面的流程不要太过注重细节,阐述流程用,其中明显会有几个问题,比如,A、B账户处于异常状态,不能进行交易;A的余额不足100等等
当出现问题的时候,这整个转账的动作都是失败的,我们需要保证A账户的钱不能少,B账户的钱也不能多,都还是原来的金额。
自此便是事务的概念:
数据库事务(简称:事务)是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
一个数据库事务通常包含了一个序列的对数据库的读/写操作。它的存在包含有以下两个目的:

  1. 为数据库操作序列提供了一个从失败中恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法。
  2. 当多个应用程序在并发访问数据库时,可以在这些应用程序之间提供一个隔离方法,以防止彼此的操作互相干扰。

当事务被提交给了DBMS,则DBMS需要确保该事务中的所有操作都成功完成且其结果被永久保存在数据库中,如果事务中有的操作没有成功完成,则事务中的所有操作都需要被回滚,回到事务执行前的状态;同时,该事务对数据库或者其他事务的执行无影响,所有的事务都好像在独立的运行。

事务的四大特性(ACID)

原子性(Atomicity):事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
一致性(Consistency):事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
隔离性(Isolation):多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
持久性(Durability):已被提交的事务对数据库的修改应该永久保存在数据库中。

隔离性-隔离级别

并发的状态下,事务会出现如下一些问题:

  • 脏读
    当一个事务允许读取另外一个事务修改但未提交的数据时,就可能发生脏读。
    当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是脏数据,依据脏数据所做的操作可能是不正确的。

  • 不可重复读
    一个事务内,多次读同一个数据。在这个事务还没有结束时,另一个事务也访问该同一数据。那么,在第一个事务的两次读数据之间。由于第二个事务的修改,那么第一个事务读到的数据可能不一样,这样就发生了在一个事务内两次读到的数据是不一样的,因此称为不可重复读,即原始读取不可重复。

  • 幻影读
    在事务执行过程中,当两个完全相同的查询语句执行得到不同的结果集。这种现象称为“幻影读(phantom read)”
    当事务没有获取范围锁的情况下执行SELECT ... WHERE操作可能会发生“幻影读”。
    “幻影读”是不可重复读的一种特殊场景:当事务1两次执行SELECT ... WHERE检索一定范围内数据的操作中间,事务2在这个表中创建了(如INSERT)了一行新数据,这条新数据正好满足事务1的“WHERE”子句。
    需要指出的是事务1执行了两遍同样的查询语句,第二次查询可能会得到不同的结果集。

隔离级别

为了解决上述问题,引入了不同的隔离级别来解决:

  • 未提交读
    未提交读(READ UNCOMMITTED)是最低的隔离级别。允许“脏读”(dirty reads),事务可以看到其他事务“尚未提交”的修改。
  • 提交读
    在提交读(READ COMMITTED)级别中,基于锁机制并发控制的DBMS需要对选定对象的写锁一直保持到事务结束,但是读锁在SELECT操作完成后马上释放(因此“不可重复读”现象可能会发生,见下面描述)。和前一种隔离级别一样,也不要求“范围锁”。
  • 可重复读
    在可重复读(REPEATABLE READS)隔离级别中,基于锁机制并发控制的DBMS需要对选定对象的读锁(read locks)和写锁(write locks)一直保持到事务结束,但不要求“范围锁”,因此可能会发生“幻影读”。
  • 可串行化
    最高的隔离级别。在基于锁机制并发控制的DBMS实现可串行化,要求在选定对象上的读锁和写锁保持直到事务结束后才能释放。在SELECT 的查询中使用一个“WHERE”子句来描述一个范围时应该获得一个“范围锁”(range-locks)。这种机制可以避免“幻影读”(phantom reads)现象。当采用不基于锁的并发控制时不用获取锁。但当系统探测到几个并发事务有“写冲突”的时候,只有其中一个是允许提交的。

随着隔离级别的提高,能够避免的问题也越多,但同时带来的开销也迅速增加:

隔离级别 脏读 不可重复读 幻影读
未提交读 可能发生 可能发生 可能发生
提交读 - 可能发生 可能发生
可重复读 - - 可能发生
可序列化 - - -

MySQL事务的原理

MySQL的事务分类

  • 扁平事务
    扁平事务(Flat Transactions)是事务类型中最简单但使用最频繁的事务。在扁平事务中,所有的操作都处于同一层次,由BEGIN/START TRANSACTION开始事务,由COMMIT/ROLLBACK结束,且都是原子的,要么都执行,要么都回滚。因此扁平事务是应用程序成为原子操作的基本组成模块。

  • 带有保存节点的扁平事务
    带有保存节点的扁平事务(Flat Transactions with Savepoints)允许事务在执行过程中回滚到较早的一个状态,而不是回滚所有的操作。保存点(Savepoint)用来通知系统应该记住事务当前的状态,以便当之后发生错误时,事务能回到保存点当时的状态。
    对于扁平事务来说,在事务开始时隐式地设置了一个保存点,回滚时只能回滚到事务开始时的状态

  • 链事务
    链事务(Chained Transaction)是指一个事务由多个子事务链式组成。前一个子事务的提交操作和下一个子事务的开始操作合并成一个原子操作,这意味着下一个事务将看到上一个事务的结果,就好像在一个事务中进行的一样。这样,在提交子事务时就可以释放不需要的数据对象,而不必等到整个事务完成后才释放。
    链事务与带保存节点的扁平事务不同的是,链事务中的回滚仅限于当前事务,相当于只能恢复到最近的一个保存节点,而带保存节点的扁平事务能回滚到任意正确的保存点。但是,带有保存节点的扁平事务中的保存点是易失的,当发生系统崩溃是,所有的保存点都将消失,这意味着当进行恢复时,事务需要从开始处重新执行。

  • 嵌套事务
    嵌套事务(Nested Transaction)是一个层次结构框架。由一个顶层事务(top-level transaction)控制着各个层次的事务。顶层事务之下嵌套的事务成为子事务(subtransaction),其控制着每一个局部的操作,子事务本身也可以是嵌套事务。
    MySQL本身是不支持嵌套事务的,每次start transaction 都会隐式提交。

  • 分布式事务
    分布式事务(Distributed Transactions)通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中不同节点的数据库资源,分布式环境下运行的扁平事务.
    牵涉到分布式,就默默埋下一个大坑好了,写起来也是能有一篇文章。

MySQL事务控制

BEGIN或START TRANSACTION;显式地开启一个事务;
COMMIT;也可以使用COMMIT WORK,不过二者是等价的。COMMIT会提交事务,并使已对数据库进行的所有修改称为永久性的;
ROLLBACK;有可以使用ROLLBACK WORK,不过二者是等价的。回滚会结束用户的事务,并撤销正在进行的所有未提交的修改;
SAVEPOINT identifier;SAVEPOINT允许在事务中创建一个保存点,一个事务中可以有多个SAVEPOINT;
RELEASE SAVEPOINT identifier;删除一个事务的保存点,当没有指定的保存点时,执行该语句会抛出一个异常;
ROLLBACK TO identifier;把事务回滚到标记点;
SET TRANSACTION;用来设置事务的隔离级别。InnoDB存储引擎提供事务的隔离级别有READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ和SERIALIZABLE。
MYSQL 事务处理主要有两种方法:

  1. 用 BEGIN, ROLLBACK, COMMIT来实现
    BEGIN 开始一个事务
    ROLLBACK 事务回滚
    COMMIT 事务确认

  2. 直接用 SET 来改变 MySQL 的自动提交模式:
    SET AUTOCOMMIT=0 禁止自动提交
    SET AUTOCOMMIT=1 开启自动提交

Tips

在 MySQL 命令行的默认设置下,事务都是自动提交的,即执行 SQL 语句后就会马上执行 COMMIT 操作。因此要显式地开启一个事务务须使用命令 BEGIN 或 START TRANSACTION,或者执行命令 SET AUTOCOMMIT=0,用来禁止使用当前会话的自动提交。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务。
事务用来管理 insert, update, delete 语句
在MySQL数据库中,默认的隔离级别为Repeatable read (可重复读);


Spring的事务管理

OK, 到这叨叨了一大波原理,Talk is cheap,Show me the code。下面用代码来说下Spring对事务是如何进行管理的。
Spring通过AOP,抽象统一的接口来进行声明式事务管理,好处是它将事务管理代码从业务方法中分离出来,并且让开发按人员可以不用关注具体事务管理方式的实现;
可采用xml配置或者注解来实现。

以下是基于Spring 4.X 的通用事务管理配置文件:
此处采用通用bean配置,其他配置方式,可以参照
Spring事务配置的五种方式和spring里面事务的传播属性和事务隔离级别

<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.2.xsd">
<!-- 
常见的TranszctionManager类
org.springframework.orm.jpa.JpaTransactionManager   使用JPA持久化使用该事务管理器
org.springframework.orm.hibernate3.HibernateTransactionManager  Hibernate3.o事务管理器
org.springframework.jdbc.datasource.DataSourceTransactionManager SpringJdbc或ibatis等基于DataSource数据源持久化技术时使用管理器
org.springframework.orm.jdo.JdoTransactionManager   使用JDO持久化使用该事务管理器
org.springframework.transaction.jta.JtaTranszctionManager   具有多个数据源的全局事务使用事务管理器

目前普遍使用DataSourceTransactionManager居多,配合MyBatis
-->
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource" />
  </bean>
  <bean id="txTransactionInterceptor"class="org.springframework.transaction.interceptor.TransactionInterceptor">
    <property name="transactionManager" ref="txManager" />
    <property name="transactionAttributes">
<!--
用正则匹配扫描方法,以save,update,delete开头的方法,使用事务管理

Spring在TransactionDefinition接口中规定了7中类型的事务传播行为,规定了事务方法和事务方法发生嵌套调用时事务如何传播的。
propagation_required:如果当前没有事务,就新建一个事务,如果已经存在,就加入到存在的事务中,这是最常见的选择
propagation_supports:支持当前的事务,如果当前没有事务,就按非事务模式执行
propagation_mandatory:使用当前事务,如果当前没有事务,抛出异常
propagation_requires_new:新建事务,如果当前存在事务,把当前事务挂起
propagation_not_supported:以非事务方式执行,如果当前存在事务,就把当前事务挂起
propagation_never:以非事务方式执行,如果当前存在事务,则抛出异常
propagation_nested:如果当前存在事务,则在嵌套事务内执行,如果没有事务,则执行与propagation_required类似的操作

回滚策略:
在碰到哪些异常的时候回滚,
支持Exception,CheckedException,以及自定义异常
- 表示抛出该异常时需要回滚
+表示即使抛出该异常事务同样要提交
-->
      <props>
        <prop key="save*">PROPAGATION_REQUIRED,-Exception</prop>
        <prop key="update*">PROPAGATION_REQUIRED,-Exception</prop>
        <prop key="delete*">PROPAGATION_REQUIRED,-Exception</prop>
      </props>
    </property>
  </bean>

<!--AOP切片,在配置的包中进行扫描匹配的方法进行事务管理-->
  <bean id="txTransactionPointcutAdvisor" class="org.springframework.aop.support.RegexpMethodPointcutAdvisor">
    <property name="advice">
      <ref bean="txTransactionInterceptor"/>
    </property>
    <property name="patterns">
      <list>
        <value>com.test.service.impl.*.*</value>
      </list>
    </property>
  </bean>

  <bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoProxyCreator">
    <property name="proxyTargetClass" value="true"></property>
    <property name="beanNames">
      <value>*ServiceImpl</value>
    </property>
    <property name="interceptorNames">
       <list>
        <value>txTransactionPointcutAdvisor</value>
      </list>
    </property>
  </bean>
</beans>

以下是基于Spring 4.X 的注解式实现的例子:

//添加事务注解
//1.使用 propagation 指定事务的传播行为, 即当前的事务方法被另外一个事务方法调用时如何使用事务, 取值见上XML配置注解
//2.使用 isolation 指定事务的隔离级别, 最常用的取值为 READ_COMMITTED,参见隔离级别,支持DEFAULT,READ_UNCOMMITTED,READ_COMMITTED,REPEATABLE_READ,SERIALIZABLE
//3.默认情况下 Spring 的声明式事务对所有的运行时异常进行回滚,也可以通过对应的属性进行设置,通常情况下去默认值即可,取值见上XML配置注解
//no-rollback-for 对应xml中的+Exception
//4.使用 readOnly 指定事务是否为只读. 表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务. 若真的事一个只读取数据库值的方法, 应设置 readOnly=true,或者直接不使用事务
//5.使用 timeout 指定强制回滚之前事务可以占用的时间
@Transactional(propagation=Propagation.REQUIRES_NEW,
            isolation=Isolation.READ_COMMITTED,
            rollbackFor = { Exception.class },
            //noRollbackFor = {Exception.class},
            readOnly=false,
            timeout=3)
public void crud(){
  try {  
    doCURD();
    //这里为了说明,抛异常时,如果没有使用try catch ,则spring会回滚,doCURD中对数据库的操作都不会执行
    throw new Exception();
   } catch (Exception e) {  
      e.printStackTrace(); 
      //如果用了try catch,在catch里面再抛出一个指定异常,这样出了异常才会回滚
      //但是加上这句之后抛了异常就能回滚(有这句代码就不需要再手动抛出运行时异常了)
      TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();  
     } 
}

遇到的坑

  1. 嵌套事务的传递
    当Service A 调用Service B时,就会产生逻辑上的嵌套事务,Spring 的处理如下:
    PROPAGATION_REQUIRED 如果当前线程中已经存在事务, 方法调用会加入此事务, 如果当前没有事务,就新建一个事务

PROPAGATION_REQUIRES_NEW 启动一个新的, 不依赖于环境的 "内部" 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

另一方面, PROPAGATION_NESTED 开始一个 "嵌套的" 事务, 它是已经存在事务的一个真正的子事务. 嵌套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此 savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。

由此PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 的最大区别在于, PROPAGATION_REQUIRES_NEW 完全是一个新的事务, 而 PROPAGATION_NESTED 则是外部事务的子事务, 如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 roll back。

当数据库处理结果与预期不符,可以检查配置是否合理。

  1. 异常的回滚
    在使用try catch块时需要注意回滚策略,详见上java代码

  2. MySQL的引擎
    当MySQL的引擎配置为MyISAM(默认)时,是不支持事务的,Spring配置得再6,也不会起作用,目前普遍使用InnoDB,可以参考
    MyISAM与InnoDB两者之间区别与选择,详细总结,性能对比

  3. 使用Spring MVC时,加载顺序不当导致事务无效
    spring的容器(applicationContext)和springMVC的(applicationContext)是不同的。
    spring容器加载得时候,优先加载ServletContextListener(对应spring.xml)产生的父容器,而springMVC(对应springMVC.xml)产生的是子容器。子容器Controller进行扫描装配时装配的@Service注解的实例是没有经过事务加强处理,
    即没有事务处理能力的Service。而父容器进行初始化的Service是保证事务的增强处理能力的。如果不在子容器中将Service除去掉,此时得到的将是原样的无事务处理能力的Service。
    所以,我们应把扫描Service的工作放在spring.xml中。让Service和事务注解存在于同一个容器中,这样配置的事务注解就能起作用了。


总结

总结一下:

  • 本文阐述了事务的基本概念:是由一个有限的数据库操作序列构成在系统执行过程中的一个逻辑单位
  • 事务的四大特性(ACID):原子性(A),一致性(C),隔离性(I),持久性(Durability)
  • 事务的隔离级别:未提交读(READ UNCOMMITTED),在提交读(READ COMMITTED),在可重复读(REPEATABLE READS),可串行化(Serializable)
  • 隔离级别解决对应的问题:脏读,不可重复读,幻影读
  • MySQL的事务类型:扁平事务,带有保存节点的扁平事务,链事务,嵌套事务,分布式事务
  • MySQL的事务控制:BEGIN, ROLLBACK, COMMIT以及SET AUTOCOMMIT两种方式
  • Spring对事务的管理,以及常见的一些问题

谢谢阅读,希望能对你有所帮助。


思考题

留个思考题给大家,思考一下数据库的事务隔离机制的实现和多线程中的并发控制有何异同,我们有缘再聊聊这个问题


引用:

维基百科:数据库事务

维基百科:事务隔离

菜鸟教程:MySQL事务

以及其他一些博文的零碎引用,深表感谢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,444评论 4 365
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,867评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 110,157评论 0 248
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,312评论 0 214
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,673评论 3 289
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,802评论 1 223
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,010评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,743评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,470评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,696评论 2 250
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,187评论 1 262
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,538评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,188评论 3 240
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,127评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,902评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,889评论 2 283
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,741评论 2 274

推荐阅读更多精彩内容