一, 数据库事务 ACID 特性
必须要掌握事务的 4 个特性, 其中事务的隔离性之于 MySQL, 对应 4 级隔离级别.
原子性(Atomicity):
事务中的所有原子操作, 要么都能成功完成, 要么都不完成, 不能停滞在中间环节. 发生错误要回滚至事务开始前状态, 仿佛事务没有发生过.
一致性(Consistency):
事务必须始终保持数据库系统处于一致的状态, 无论并发多少事务.
隔离性(Isolation):
事务的隔离性是多个用户并发访问数据库时, 数据库为每一个用户开启的事务, 不能被其他事务的操作数据所干扰, 多个并发事务之间要存在相互隔离.[MySQL 隔离级别]
持久性(Durability):
持久性是指一个事务一旦被提交, 它对数据库中数据的改变就是永久性的, 接下来即使数据库发生故障也不应该对其有任何影响.
二, MySQL 隔离级别
select @@tx_isolation ; -- 查询 MySQL 隔离级别
set <level> tx_isolaction = 'READ-COMMITTED'; -- 设置事务隔离级别 level 分为 session 默认, global 全局
start transaction ; -- 启动事务
set savepoint <point_name> ; -- 设置回滚点, MySQL 支持回滚点
rollback ; -- 回滚全部
rollback to <savepoint_name> ; -- 回滚至某回滚点
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 操作问题
-- 在 RR 级别下
-- 事务 A 修改操作
update t1 set class_name = '初二三班' where id = 4 ;
-- 这时行锁会锁定 id = 4 的数据行, 不允许其他事务进行修改或删除操作
-- 同时 gap 锁会锁定 id = 4 数据行的相邻区间, 屏蔽其他事务在该区间内的插入操作
-- 如果使用的是没有索引的字段, 比如:
update t1 set class_name ='初二三班' where teacher_id = 2
--(即使没有匹配到任何数据)那么也会给全表加入 gap 锁.
-- 同时, 它不能像行锁一样经过 MySQL Server 过滤自动解除不满足条件的锁
-- 因为没有索引, 则这些字段也就没有排序, 也就没有区间. 除非该事务提交, 否则其它事务无法插入任何数据.
-- 行锁防止别的事务修改或删除, GAP 锁防止别的事务新增
-- 行锁和 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 事务实例
- // 基于 ssm 整合之后的项目, 完成批量操作示例 d
- //1. 创建两个 service A ,B A 负责单条插入 B 负责批量插入 B 循环调用 A 的方法
- //B 为默认隔离级别, 默认 spring 事务传播行为 A 为默认隔离级别, REQUIRES_NEW 的传播行为
- //DAO 层略
- @Transactional(isolation=Isolation.DEFAULT,propagation=Propagation.REQUIRED)
- public int insertStudentList(List<Students> list) {
- // TODO Auto-generated method stub
- int count = 0 ;
- for(Students stu : list) {
- try {
- count += service.insertStudent(stu);
- }catch(DuplicateKeyException e) {
- System.err.println(e);
- System.err.println(stu+"该数据出现了异常 ........");
- }
- }
- System.out.println("新增"+count+"条数据...");
- return count ;
- }
- // 注意需要异常处理, 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, 会产生两个事务
切勿时间占用事务
错误的异常捕捉
来源: https://www.cnblogs.com/lijizhi/p/11395053.html