事务

介绍

  1. 事务由单独单元的一个或多个SQL语句组成,在这个单元中,每个mysql语句是相互依赖的。而整个单独单元作为一个不可分割的整体,如果单元中某条sql语句一旦执行失败或者发生错误,整个单元将会回滚。所有受影响的数据将返回到事务开始以前的状态。如果单元的所有sql语句都执行成功,事务就会顺利执行并提交
  2. 事务开始、结束

    1. 以第一个DML语句的执行作为开始;
    2. 结束:COMMIT或ROLLBACK语句;DDL或DCL语句(自动提交);用户会话正常结束;系统异常终止;
      1. begin transation--开启事务
      2. start transaction---关闭自动提交事务语句
      3. DML语句---一些sql操作
      4. commit --提交事务
      5. rollback---回滚事务,只能回滚到上一次的提交点

      事务的特性

  3. 原子性

    1. 事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生;事务中任何一个数据库操作失败,已经执行的任何操作都必须被撤销,让数据返回到初始状态。只有所有的操作执行成功,整个事务才提交;
  4. 一致性
    1. 事务必须使数据库从一个一致性状态转换到另一个一致性状态;
    2. 一旦事务的操作完成,事务被提交,数据和资源就处于一种满足业务规则的一致性状态,即数据不会被破坏;
    3. 如:a+b=100,一个事务改变了a那么b也需要改变,保证事务结束以后a+b=100依然成立,这就是一致性;
  5. 隔离性
    1. 一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他的事务是隔离的,并发执行的各个事务之间不能互相干扰;
    2. 在并发数据操作时,不同事务拥有各自的数据空间,他们的操作不会对对方产生干扰,准确说,并非要求做到完全不干扰;
    3. 数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度。隔离级别越高,数据一致性越好,但并发越弱。比如对于A对B进行转账,A没把这个交易完成的时候,B是不知道A要给他转钱;
  6. 持久性
    1. 是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。数据库管理系统一般采用重执行日志来保证原子性、一致性和持久性;
    2. 原子性、隔离性、持久性,都是为了一致性服务的,数据库必须要实现AID三大特性,才有可能实现一致性。
  7. mysql是如何保证原子性的呢?是利用InnoDB的undo log也就是回滚日志,当事务回滚时能够撤销所有已经成功执行的sql语句,它需要记录你要回滚的相应日志消息。比如:
    1. 当你delete一条数据的时候,需要记录这条数据的信息,回滚的时候,insert这条旧数据;
    2. 当你update一条数据的时候,需要记录之前的旧值,回滚的时候根据旧值执行update操作;当你insert一条数据的时候,就需要这条记录的主键,回滚的时候根据主键执行delete操作;
    3. undo log通过这些记录,当接受事务回滚或者事务失败的时候,可以将数据回滚到修改之前的样子;
  8. Mysql是利用InnoDB 的redo log来保证事务的持久性。mysql先把磁盘上的数据加载到内存中,在内存中对数据进行修改,再刷回磁盘上,如果这时突然宕机,内存中的数据就会丢失。所以事务提交前需要直接把数据写入磁盘中;但是写入磁盘中操作的是随机IO,速度较慢。于是采用redo log解决这些问题。当做数据修改的时候,不仅在内存中操作,还会在redo log中记录这次操作。当事务提交的时候,会将redo log日志进行刷盘(redo log一部分在内存中,一部分在磁盘上)。当数据库宕机重启的时候,会将redo log中的内容恢复到数据库中,再根据undo log和binlog内容决定回滚数据还是提交数据。
  9. 重执行日志记录了数据库变化的每一动作,数据库在一个事务中执行一部分操作后发生错误退出,数据库即可根据重执行日志撤销已经执行的操作。对于已经提交的事务即使数据库崩溃,在重启数据库时也能根据日志对尚未持久化的数据进行相应的重执行操作。

事务的传播性

  1. 即当前的事务方法被另外一个事务方法调用时如何使用事务(界定两个事务之间相互调用的影响粒度),默认取值required,即使用调用方法的事务; | 事务传播行为类型 | 说明 | | —- | —- | | required | 如果当前存在事务,那么这个方法就加入这个事务中运行;否则,就启动一个新的事务,并在自己的事务内运行 | | requires_new | 当前的方法必须启动新事务,并在它自己的事务内运行;如果当前存在事务,就将它挂起 | | mandatory | 当前的方法必须运行在事务内部,如果没有正在运行的事务,将抛出异常 | | supports | 如果有事务在运行,当前的方法就在这个事务内运行;否则它可以不运行在事务中,作普通方法执行。 | | not_supported | 当前的方法不应该运行在事务中(普通方法执行),如果有运行的事务,将它挂起 | | never | 当前的方法不应该运行在事务中,如果有运行的事务,就抛出异常 | | nested | 如果有事务在运行,当前的方法就应该在这个事务的嵌套事务内运行。否则,就启动一个新的事务,并在它自己的事务内运行(父子事务):父事务回滚,子事务就需要回滚;子事务回滚,父事务按照自己的规则去判断是否回滚。 |
  1. 最常用的REQUIRED、REQUIRES_NEW。PROPAGATION_NESTED是Spring所提供的一个特殊变量。它要求事务管理器或者使用JDBC3.0Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionMannager)
  2. ROPAGATION_REQUIRES_NEW启动一个新的,不依赖于环境“内部”事务,这个事务将被完全commited或rolled back而不依赖于外部事务,它拥有自己的隔离范围,自己的锁,等等,当内部事务开始执行时,外部事务被挂起,内部事务结束时,外部事务将继续执行。
  3. PROPAGTION_REQUIRES_NEW和PROPAGATION_NESTED的最大区别在于:PROPAGATION_REQUIRES_NEW完全是一个新的事务,而PROAGATION_NESTED则是外部事务的子事务,如果外部事务commit,嵌套事务也会被cmmit,这个规则同样适用于rollback。

事务的隔离级别

  1. 一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务隔离级别,不同隔离级别对应不同的干扰程序,隔离级别越高,数据一致性就越好,但并发性越弱;
  2. 数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。对于同时运行的多个事务,当这些事务访问数据库中相同的数据时,如果没有采取必要的隔离机制,就会导致各种并发问题。
  3. 五种隔离级别:DEFAULT、READ_UNCOMMITED、READ_COMMIIED、REPEATABLE_READ和SERIALIZABLE;
    1. Oracle支持的2种事务隔离级别:READ_COMMITTED,SERIALIABLE。Oracle默认的事务隔离级别为:READ_COMMITTED。
    2. MySQL支持4种事务隔离级别:READ_UNCOMMITED、READ_COMMITTED、REPEATABLE_READ和SERIALIZABLE。MYSQL默认的事务隔离级别:REPEATABLE_READ;
    3. DEFAULT
      1. 这是默认的隔离级别,使用数据库默认的事务隔离级别。其他四个和JDBC隔离级别相对应。大部分数据库的默认级别都是READ_COMMITTED.
    4. READ_UNCOMMITTED(读取未提交)
      1. 这是事务最低的隔离级别,运行当前事务读取未被其他事务提交的变更。这种隔离级别会产生脏读、不可重复读和幻读;
      2. 产生脏读场景:A事务读取一个字段,但是这个字段被另一个事务更新了但是还未提交,这时候读取的数据是脏数据(可能不是正确的)。再次读取该字段时如果另外一个事务回滚那么读到的数据与第一次读数据库中的数据都不同;
      3. 产生不可重复读场景:A事务读取一个字段,但是这个字段被另外一个事务更新并提交,再次读取该字段值不一样则出现了不可重复读现象(在一个事务中多次读取同一个数据,不能保证读取的字段值相同)
      4. 产生幻读场景:A事务读取一个字段集合,但是这个表被另外一个事务插入数据并提交,再次读取该表可能会多几行则出现了幻读想象;
      5. 事务A开启事务,查询当前表的目标数据不存在;B事务开启事务插入一个新数据(id为主键);事务A插入和事务B一样的数据,会出现锁等待超时现象。事务B提交后,事务A再次尝试插入,出现Duplicate entry…事务B很诧异,明明查询出来不存在将要插入的数据。
    5. READ_COOMITTED(读取已提交)
      1. 保证一个事务修改的数据提交之后才能被另一个事务读取,另外一个事务不能读取该事务未提交的数据。
      2. 这个级别可以避免脏读,但是存在不可重读和幻读的问题;
    6. REPEATABLE_READ(可重读)
      1. 确保事务可以多次从某行记录的一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新。
      2. 这种事务可以防止脏读、不可重复读,但是幻读问题不能消除。
    7. SERIALIZABLE(可串行化)
      1. 在并发情况下和串行化的读取结果是一致,没有什么不同。这是花费最高代价但是最可靠的事务隔离级别,事务被处理为顺序执行。除了防止脏读,不可重复读还避免了幻读。
    • 什么是脏读?不可重复读?幻读?
    1. 脏读:对于两个事务T1、T2,T1读取了已经被T2更新但没有被提交的字段,之后T2要是回滚的话,T1读取的数据就是临时的且无效的,视为脏数据;
    2. 不可重复读:对于两个事务T1,T2,T1读取了一个字段,然后T2更新了该字段的值,之后T1再去读取同一个字段,值是不相同的。重点是update;
    3. 幻读:事务T1读取一条指定where条件的语句,返回结果集。这时候事务T2插入一些新的记录,刚好满足T1的where条件。然后T1使用相同的条件再次查询,结果集中可以看到T2插入的记录,这些多出来的新纪录就是幻读。重点是insert和delete;
    • 设置MySQL的事务隔离级别
  • 每启动一个mysql程序,就会获得一个单独的数据库连接。每个数据库连接都有一个全局变量@@tx_isolation,表示当前的事务隔离级别。

    • 查看当前的隔离级别:SELECT @@tx_isolation;
    • 设置当前MySql连接的隔离级别:set transaction isolation level read committed;
    • 设置数据库系统的全局的隔离级别:set global transaction isolation read committed;
      1. set global transaction isolation level read uncommitted;--读未提交级别
      2. set global transaction isolation level read committed;---读已提交级别
      3. set global transaction isolation level repeatable read;---默认级别,可重复读
      4. set global transaction isolation level serializable;----串行化
      5. -- 查看事务隔离级别
      6. @@global.tx_isolation
  • 事务优化

    • 超时事务属性:(timeout=millons)事务在回滚之前可以保持多久。这样 可以防止长期运行的事务占用资源;
    • 只读事务属性:(readOnly=true/false):表示这个事务只读取数据但不更新数据,这样可以帮助数据库引擎优化事务;

锁机制

快照读:简单的select操作,属于快照读,不加锁;

  • 在事务中执行普通的select查询之后,会创建快照,后面再执行select语句时,查询的其实时之前生成的快照。这就是mysql中RR(可重复读)隔离级别;
  • 在事务中的读操作通过对当前的数据库中记录一个版本,以后的读操作只会读取记录的版本,因此相当于对数据库的数据建立一个快照数据,因此叫做快照读,不需要对数据库中的数据进行加锁(乐观锁)。

当前读:特殊的读操作,插入/更新/删除操作,都数据当前读,需要加锁。

  • update时使用当前读(读取最新数据),再查询的时候就会查询最新的数据。

mysql的锁机制

  • 记录锁(只有一条记录)
  • 页锁(一页数据)
  • 行级锁(有可能是多条记录):共享锁和排他锁

    • 共享锁:称为读锁,S锁。共享锁就是多个事务对于同一个数据可以共享一把锁,都能访问到数据,但是不能对其进行修改;
      • select * from t_user where id=1 lock in share mode;
    • 排他锁:称为写锁(独占锁),X锁,不能与其他锁并存;如一个事务获得一个数据行的排他锁,其他事务就不能再获得该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务可以对谁进行读取和修改;
      • select * from t_user where id=1 for update;
    • MySQL InnoDB引擎默认的修改数据语句update、delete、insert都会自动给涉及的数据加上排他锁,select语句默认不会加任何锁类型;
    • 为了允许行锁和表锁共存,实现多粒度锁的机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁;
      • 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁;
      • 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁之前必须先取得该表的IX锁;
        • 意向锁是InnoDB自动加的,不需要用户干预。
    • InnoDB行锁的兼容性: | 兼容性 | x | ix | s | is | | —- | —- | —- | —- | —- | | x | 冲突 | 冲突 | 冲突 | 冲突 | | ix | 冲突 | 兼容 | 冲突 | 兼容 | | s | 冲突 | 冲突 | 兼容 | 兼容 | | is | 兼容 | 兼容 | 兼容 | 兼容 |

    • InnoDB行锁是通过给索引上的索引项加锁来实现的,这一点和Oracle不一样;InnoDB这种行锁实现特点意味着:只有通过索引条件检索数据,InnoDB才使用行级锁,否则,InnoDB将使用表锁。

  • 间隙锁(Gap Lock锁)
    • 为了解决幻读问题,InnoDB引入了间隙锁(Gap Lock)。指的是在索引记录之间的间隙上进行加锁,可以是两个索引记录之间,也可以是索引记录之前或者是最后一个索引之后的空间。【开区间】
    • 当我们使用范围条件而不是相等条件去检索数据,并且请求共享或排他锁时,InnoDB会给符合条件的已有数据记录的索引项加上锁,对于键值在条件范围内但是并不存在的记录叫做“间隙”;遵循左开右闭原则。【间隙锁(Gap锁)+行锁(X锁)合称为Next-Key Lock】;
    • 间隙锁之间是不存在冲突的,但是如果要往间隙中插入一条数据,就会产生冲突。
    • 间隙锁是在可重复读隔离级别下才会生效。
    • 间隙锁的引入,可能会导致同样的语句锁住更大的范围,影响并发度。
  • 解决幻读:
    • 在快照读情况下,mysql通过mvcc来避免幻读;
    • 在当前读情况下,mysql通过next-key来避免幻读;
    • MVCC:多版本并发控制。mysql把每个操作都定义成一个事务,每开启一个事务,系统的事务版本号自动递增。每行记录都有隐藏列:创建版本号和删除版本号;
      • select:事务每次只能读到创建版本号小于或者是等于此次系统版本号的记录,同时行的删除版本号不存在或者大于当前事务的版本号;
      • update:插入一条新的记录,并把当前系统版本号作为记录的版本号,同时保存当前系统版本号到原有的行作为删除版本号
      • delete:把当前系统版本号作为行记录的删除版本号;
      • insert:把当前系统版本号作为行记录的版本号;

事务隔离级别和数据库锁之间的关系

  • spring事务和数据库事务、数据库锁的关系
    • spring事务本质上使用数据库事务,而数据库事务的本质是使用数据库的锁,所以spring事务本质上使用数据库锁,开启spring事务意味着使用数据库锁;
  • 事务隔离级别和锁的关系
    • 事务的隔离级别是通过锁的机制去实现的,事务的隔离级别是数据库开发商根据业务逻辑的实际需要定义的一组锁的使用策略,当我们将数据库的隔离级别定义为某一级别后不能满足需求,那么我们可以自定义sql的锁来覆盖事务隔离级别默认的锁机制。
    • Spring事务实际使用AOP拦截注解方法,然后使用动态代理处理事务方法,捕获过程中的异常,spring事务其实是把异常交给spring处理。
    • spring事务只有捕获到异常才会终止或回滚,如果你在程序中try、catch后自己 处理异常而没有throw,那么事务将不会终止或回滚,失去事务本来的作用。
    • spring事务会捕获所有的异常,但只会回滚数据库相关的操作,并且在声明了rollbackForClassName=“Exception”之类的配置才会回滚。
    • spring事务会回滚同一事务中的所有数据库操作,本质上回滚同一数据库连接上的数据库操作;
  • 对象锁和spring事务的对比
    • 对象锁可以保证数据一致性和业务逻辑正确性,但是不能保证并发性;
    • spring事务不能严格保证数据一致性和业务逻辑正确性,但具有较好的并发性,因为只锁数据库行数据。

spring事务的实现方法

  • @Transactional注解实现
  • 编程式

spring事务传播机制

  • 方法A是一个事务方法,方法A执行的过程中调用了B方法,那么方法B有无事务或者方法B对事务的要求不同都会对A方法的事务执行造成影响。同时方法A的事务对方法B的事务执行也会有影响,这种影响具体由它们的事务传播属性所决定的。
  • REQUIRED,spring默认的事务传播类型。

spring事务什么时候会失效

  • spring事务的原理是AOP,进行切面增强,失效的根本原因是这个AOP不起作用:
    • 发生自调用,类里面使用this调用本类的方法(this通常省略),此时这个this对象不是代理类,而是其对象本身;
    • 方法不是public的:@Transactional只能用于public的方法上,否则事务不会生效。如果要用在非public方法上,可以开启Aspectj代理模式;
    • 数据库不支持事务的时候(用的是MyISAM),spring事务会失效;
    • 没有被spring管理;
    • 异常被吃掉,事务不会回滚(或者抛出的异常没有被定义,默认为RuntimeException)