MySQL隔离性及Spring事务

一、数据库事务ACID特性

  必须要掌握事务的4个特性,其中事务的隔离性之于MySQL,对应4级隔离级别。

  • 原子性(Atomicity):

   事务中的所有原子操作,要么都能成功完成,要么都不完成,不能停滞在中间环节。发生错误要回滚至事务开始前状态,仿佛事务没有发生过。

  • 一致性(Consistency):

   事务必须始终保持数据库系统处于一致的状态,无论并发多少事务。

  • 隔离性(Isolation):

   事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要存在相互隔离。[MySQL隔离级别]

  • 持久性(Durability):

    持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。

二、MySQL隔离级别

1 select @@tx_isolation--查询MySQL隔离级别
2 set <level> tx_isolaction  = 'READ-COMMITTED'--设置事务隔离级别level分为session默认,global全局
3 start transaction-- 启动事务
4 set savepoint <point_name>--设置回滚点,mysql支持回滚点
5 rollback--回滚全部
6 rollback to <savepoint_name>--回滚至某回滚点
7 commit--提交事务
事务常用SQL命令

MySQL支持的隔离级别

  • 读未提交(READ-UNCOMMITTED)

      这是事务最低的隔离级别,它充许令外一个事务可以看到这个事务未提交的数据。

  • 读/写提交(READ-COMMITTED)

      保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。

    快照读(Snapshot Read)在事务A尚未提交时,其他事务仍然可以到表中数据(不含未提交)即为快照读

    与之对应的还有当前读(Current Read),若事务A不提交,当前读便会阻塞waiting... (select * from t1 for update )

    在RC级别下快照读在更新,即若有其他事务提交,则更新数据快照,这也是不可重复读产生的原因。而RR级别下同一事务只有一版数据快照来实现可重复读

  • √ 可重复读(REPEATABLE-READ)(MySQL默认此级别)

      MySql的默认隔离级别,当第一次读取到数据时就对数据加s锁(共享锁),就不允许其他事务进行“修改操作(Update 、delete会加x锁,排它锁 s与x互斥)”了,而“不可重复读”恰恰是因为两次读取之间进行了数据的修改。

    多版本并发控制(MVCC,Multiversion Currency Control)见下文

  • 序列化(SERIALIZABLE)

      这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行,读取数据加s锁,写加x锁,读写互斥。可以有效避免不可重复读取、幻读、脏读问题,但会极大的降低数据库的并发能力。

常见错误

  • 脏读:允许一个事务A去读取另外一个事务B未提交的数据。(未提交即未确认,所以A事务读到脏数据的可能性非常高)

  • 不可重复读:同一条数据被多个事务影响,造成数据动态变化,不能重复读取。(事务A前后多次查询的同一条数据结果不一致,即该条数据不可重复读。原因是事务B在A多次读取间隙修改过该数据)

  • 幻读:在一个事务中,第二次select多出了row就算幻读(MySQL官方定义),不可重复读针对的是数据的修改、删除,而幻读针对破坏数据整体性insert操作。

-- RR级别下:
-- 情景1
--事务A添加一条新数据,
select * from t1 ;    
+----+--------------+------------+
| id | class_name   | teacher_id |
+----+--------------+------------+
|  3 | 初二一班     |          2 |
|  4 | 初二二班     |          2 |
|  5 | 初三三班     |          2 |
+----+--------------+------------+
insert into t1 (class_name,teacher_id) values ('初三一班',3); 
--1行被影响
select * from t1 ;    
+----+--------------+------------+
| id | class_name   | teacher_id |
+----+--------------+------------+
|  3 | 初二一班     |          2 |
|  4 | 初二二班     |          2 |
|  5 | 初三三班     |          2 |
|  6 | 初三一班     |          3 |
+----+--------------+------------+
commit ;
--事务B查询所有 (RR级别不发生不可重复读)
select * from t1 ;
+----+--------------+------------+
| id | class_name   | teacher_id |
+----+--------------+------------+
|  3 | 初二一班     |          2 |
|  4 | 初二二班     |          2 |
|  5 | 初三三班     |          2 |
+----+--------------+------------+
--事务B插入操作
insert into t1 values(6,'初一一班',4);
--插入失败,但select 操作又不会发现id= 6 的数据
RR级别幻读情景1
-- 情景2
-- 事务A 查询表t1
select * from t1 ;     -- 假设为3条数据
-- 事务B查询表t1(加s锁) 
select * from t1 ;     -- 假设为3条数据
-- 事务A向t1插入1数据
insert into t1 values(6,'初一一班',4);
select * from t1 ;     -- 4条数据,注意是事务A查询
-- 当事务B修改'全部数据'时会发现
update t1 set teacher_id = 888 ;    -- 4条数据被影响
select * from t1 ;     --3条数据
-- 查询和修改的记录条数不一致,查询是查不到事务A新插入的数据的,但修改却可以成功修改事务A新插入的数,幻读。
RR级别幻读情景2
-- 在RC级别的下幻读操作很容易实现
-- 事务A修改操作
update t1 set class_name = '初三四班' where teacher_id= 2-- 2行数据被影响
--这时事务B插入操作
insert into t1 (class_name,teacher_id) values ('初三三班',2); -- 1行数据被影响
commit ;
--当事务A查询
select * from t1 where teacher_id = 2 ;
+----+--------------+------------+
| id | class_name   | teacher_id |
+----+--------------+------------+
|  3 | 初二一班     |          2 |
|  4 | 初二二班     |          2 |
|  5 | 初三三班     |          2 |
+----+--------------+------------+
--合计3条数据,出现幻读现象
RC级别幻读

 

 脏读不可重复读幻读
READ-UNCOMMITTED
READ-COMMITTED ×
REPEATABLE-READ × ×
SERIALIZABLE × × ×

MVCC相关的锁

多版本并发控制(MVCC,Multiversion Currency Control)

  不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC来避免这两种问题。

  • 悲观锁

      正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

    在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据(序列化级别)

  • 乐观锁

      相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

      而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

    要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。

  • NEXT—KEY锁

      NEXT—KEY锁是行锁gap(间隙锁)的合并,行锁可以避免不同事务对相同数据的修改冲突,但无法避免insert操作问题  

     1 --在RR级别下
     2 --事务A修改操作
     3 update t1 set class_name = '初二三班' where id = 4 ; 
     4 --这时行锁会锁定id = 4的数据行,不允许其他事务进行修改或删除操作
     5 --同时gap锁会锁定id = 4数据行的相邻区间,屏蔽其他事务在该区间内的插入操作
     6 --如果使用的是没有索引的字段,比如:
     7 update t1 set class_name ='初二三班' where teacher_id = 2
     8 --(即使没有匹配到任何数据)那么也会给全表加入gap锁。
     9 --同时,它不能像行锁一样经过MySQL Server过滤自动解除不满足条件的锁
    10 --因为没有索引,则这些字段也就没有排序,也就没有区间。除非该事务提交,否则其它事务无法插入任何数据。
    11 --行锁防止别的事务修改或删除,GAP锁防止别的事务新增
    12 --行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在[写数据时的]幻读问题。

MVCC在MySQL的InnoDB中的实现

在InnoDB中,会在每行数据后添加两个额外的隐藏的值来实现MVCC,这两个值一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。 在实际操作中,存储的并不是时间,而是事务的版本号,每开启一个新事务,事务的版本号就会递增。 在可重读RR事务隔离级别下:

  • SELECT时,读取创建版本号<=当前事务版本号,删除版本号为空或>当前事务版本号。(快照读

  • INSERT时,保存当前事务版本号为行的创建版本号

  • DELETE时,保存当前事务版本号为行的删除版本号

  • UPDATE时,插入一条新纪录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号到原来删除的行  

      通过MVCC,虽然每行记录都需要额外的存储空间,更多的行检查工作以及一些额外的维护工作,但可以减少锁的使用,大多数读操作都不用加锁,读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。我们不管从数据库方面的教课书中学到,还是从网络上看到,大都是上文中事务的四种隔离级别这一模块列出的意思,RR级别是可重复读的,但无法解决幻读,而只有在Serializable级别才能解决幻读。于是我就加了一个事务C来展示效果。在事务C中添加了一条teacher_id=1的数据commit,RR级别中应该会有幻读现象,事务A在查询teacher_id=1的数据时会读到事务C新加的数据。但是测试后发现,在MySQL中是不存在这种情况的,在事务C提交后,事务A还是不会读到这条数据。可见在MySQL的RR级别中,是解决了部分幻读的读问题的。

三、Spring事务

  有关Spring事务仅做简单实例说明,相关底层及原理不介绍,可能会觉得虎头蛇尾,日后会单独出一节关于Spring事务的帖子。

传播行为是指方法之间的调用事务策略的问题。

传播行为含义备注
REQUIRED 不存在创建新事务,存在沿用之前的事务 Spring默认传播行为
SUPPORTS 不存在不创建,存在沿用之前的事务 -
MANDATORY 方法必须在事务中运行 没有事务,抛出异常
REQUIRES_NEW 无论是否存在事务,都会在新的事务中运行 事务管理器自动创建新的
NOT_SUPPORTED 不支持事务,存在事务会挂起,知道方法结束恢复 适用于不需要事务的SQL
NEVER 不支持事务,不能在事务环境下运行 存在事务,抛出异常
NESTED 嵌套事务,支持内部事务回滚,不影响主事务 savepoint保存点

四、Spring事务实例

 1 //基于ssm整合之后的项目,完成批量操作示例d
 2 //1.创建两个service A 、B   A负责单条插入  B负责批量插入 B循环调用A的方法
 3 //B为默认隔离级别,默认spring事务传播行为   A为默认隔离级别,REQUIRES_NEW的传播行为
 4 //DAO层略
 5 @Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED)
 6     public int insertStudentList(List<Students> list) {
 7         // TODO Auto-generated method stub
 8         int count = 0 ;
 9         for(Students stu : list) {
10             try {
11                 count += service.insertStudent(stu);
12             }catch(DuplicateKeyException e) {
13                 System.err.println(e);
14                 System.err.println(stu+"该数据出现了异常 。。。。。。。。");
15             }
16         }
17         System.out.println("新增"+count+"条数据...");
18         return count ;
19 }
20 //注意需要异常处理,SpringTX根据是否出现异常判断
<!-- spring-service.xml  配置事务管理器 -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource" />
    </bean><!-- 配置基于注解的声明式事务 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
spring.xml

五、常见Spring事务的使用错误

  • static 方法和非public方法 @Transactional注解失效

  • 同一个类中(主要针对service层),A方法调用B方法 @Transactional失效

  • 相同controller中调用两次相同事务Service,会产生两个事务

  • 切勿时间占用事务

  • 错误的异常捕捉

posted @ 2019-08-22 16:22  2JZ  阅读(421)  评论(1编辑  收藏  举报