MySQL隔离性及Spring事务
一、数据库事务ACID特性
必须要掌握事务的4个特性,其中事务的隔离性之于MySQL,对应4级隔离级别。
-
原子性(Atomicity):
-
一致性(Consistency):
事务必须始终保持数据库系统处于一致的状态,无论并发多少事务。
-
隔离性(Isolation):
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要存在相互隔离。[MySQL隔离级别]
-
持久性(Durability):
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
二、MySQL隔离级别
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
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 ; --提交事务
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操作。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- 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 的数据
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- 情景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新插入的数,幻读。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
-- 在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条数据,出现幻读现象
脏读 | 不可重复读 | 幻读 | |
---|---|---|---|
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级别在[写数据时的]幻读问题。
在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根据是否出现异常判断
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<!-- 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事务的使用错误
-
static 方法和非public方法 @Transactional注解失效
-
同一个类中(主要针对service层),A方法调用B方法 @Transactional失效
-
相同controller中调用两次相同事务Service,会产生两个事务
-
切勿时间占用事务
-