事务的 ACID 特性
Atomicity(原子性)
一个事务是一个不可分割的工作单位,事务中包括的诸操作要么都做,要么都不做。
Consistency(一致性)
事务必须是使数据库从一个一致性状态变到另一个一致性状态。一致性与原子性是密切相关的。
Isolation(隔离性)
一个事务的执行不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
Duration(持久性)
持久性也称永久性(permanence),指一个事务一旦提交,它对数据库中数据的改变就应该是永久性的。接下来的其他操作或故障不应该对其有任何影响。
由事务并发引起的问题
Dirty Read(脏读)
脏读又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。
例如:事务T1修改了一行数据,但是还没有提交,
这时候事务T2读取了被事务T1修改后的数据,
之后事务T1因为某种原因Rollback了,
那么事务T2读取的数据就是脏的。
解决办法:把数据库的事务隔离级别调整到 READ_COMMITTED
过程如下图:
Unrepeatable Read(不可重复读)
不可重复读是指在同一个事务内,两个相同的查询返回了不同的结果。
例如:事务T1读取某一数据,事务T2读取并修改了该数据,
T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。
解决办法:把数据库的事务隔离级别调整到REPEATABLE_READ
过程如下图:
Phantom Read(幻读)
幻读是指当事务不是独立执行时发生的一种现象。
例如:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,
但是系统管理员B就在这个时候插入了一条具体分数的记录,
当系统管理员A改结束后发现还有一条记录没有改过来,
就好像发生了幻觉一样。这就叫幻读。
解决办法:把数据库的事务隔离级别调整到SERIALIZABLE_READ
过程如下图:
不可重复读和幻读的区别
不可重复读
不可重复读的重点是修改
同样的条件, 你读取过的数据, 再次读取出来发现值不一样了
例子:在事务1中,Mary 读取了自己的工资为1000,操作并没有完成
--在postresql中的例子,可能和其他的数据库略有区别
begin transaction;
select salary from employee
where id = MaryID;
在事务2中,这时财务人员修改了Mary的工资为2000,并提交了事务.
begin transaction;
update employee
set salary = 2000
where id = MaryID;
commit;
在事务1中,Mary 再次读取自己的工资时,工资变为了2000
select salary
from employee
where id = MaryID;
在一个事务中前后两次读取的结果并不致,导致了不可重复读。
幻读
幻读的重点在于新增或者删除
同样的条件, 第1次和第2次读出来的记录数不一样
例子:目前工资为1000的员工有10人。
事务1,读取所有工资为1000的员工。
begin transaction;
select * from employee
where salary = 1000;
-- 共读取10条记录
这时另一个事务向employee表插入了一条员工记录,工资也为1000
begin transaction;
insert into employee(,salary,)
values(,1000,);
commit;
事务1再次读取所有工资为1000的员工
--事务1
select * from employee
where salary = 1000;
--共读取到了11条记录
这就产生了幻像读。
总结
归纳一下,以上提到了事务并发所引起的跟读取数据有关的问题,各用一句话来描述一下:
- 脏读:事务 A 读取了事务 B 未提交的数据,并在这个基础上又做了其他操作。
- 不可重复读:事务 A 读取了事务 B 已提交的更改数据。
- 幻读:事务 A 读取了事务 B 已提交的新增数据。
第一条是坚决抵制的,后两条在大多数情况下可不作考虑。
这就是为什么必须要有事务隔离级别这个东西了,它就像一面墙一样,隔离不同的事务。看下面这个表格,您就清楚了不同的事务隔离级别能处理怎样的事务并发问题:
根据您的实际需求,再参考这张表,最后确定事务隔离级别,应该不再是一件难事了。
JDBC 也提供了这四类事务隔离级别,但默认事务隔离级别对不同数据库产品而言,却是不一样的。我们熟知的 MySQL 数据库的默认事务隔离级别就是 READ_COMMITTED
,Oracle、SQL Server、DB2等都有有自己的默认值。我认为 READ_COMMITTED
已经可以解决绝大多数问题了,其他的就具体情况具体分析吧。
若对其他数据库的默认事务隔离级别不太清楚,可以使用以下代码来获取:
DatabaseMetaData meta = DBUtil.getDataSource().getConnection().getMetaData();
int defaultIsolation = meta.getDefaultTransactionIsolation();
提示:在 java.sql.Connection 类中可查看所有的隔离级别。
我们知道 JDBC 只是连接 Java 程序与数据库的桥梁而已,那么数据库又是怎样隔离事务的呢?其实它就是“锁”这个东西。当插入数据时,就锁定表,这叫“锁表”;当更新数据时,就锁定行,这叫“锁行”。当然这个已经超出了我们今天讨论的范围。
JDBC 解决方案
事务隔离级别
- READ_UNCOMMITTED
- READ_COMMITTED
- REPEATABLE_READ
- SERIALIZABLE
Spring 解决方案
首先要明确的是,事务是从哪里来?传播到哪里去?答案是,从方法 A 传播到方法 B。Spring 解决的只是方法之间的事务传播,那情况就多了,比如:
- 方法 A 有事务,方法 B 也有事务。
- 方法 A 有事务,方法 B 没有事务。
- 方法 A 没有事务,方法 B 有事务。
- 方法 A 没有事务,方法 B 也没有事务。
这样就是 4 种了,还有 3 种特殊情况。还是用我的 Style 给大家做一个分析吧:
假设事务从方法 A 传播到方法 B,您需要面对方法 B,问自己一个问题:
方法 A 有事务吗?以下就是解答了。
事务传播特性
PROPAGATION_REQUIRED
(default)
如果没有,就新建一个事务;如果有,就加入当前事务。这就是 PROPAGATION_REQUIRED
,它也是 Spring 提供的默认事务传播行为,适合绝大多数情况。
RROPAGATION_REQUIRES_NEW
如果没有,就新建一个事务;如果有,就将当前事务挂起。这就是 RROPAGATION_REQUIRES_NEW
,意思就是创建了一个新事务,它和原来的事务没有任何关系了。
PROPAGATION_NESTED
如果没有,就新建一个事务;如果有,就在当前事务中嵌套其他事务。这就是 PROPAGATION_NESTED
,也就是传说中的“嵌套事务”了,所嵌套的子事务与主事务之间是有关联的(当主事务提交或回滚,子事务也会提交或回滚)。
PROPAGATION_SUPPORTS
如果没有,就以非事务方式执行;如果有,就使用当前事务。这就是 PROPAGATION_SUPPORTS
,这种方式非常随意,没有就没有,有就有,有点无所谓的态度,反正我是支持你的。
PROPAGATION_NOT_SUPPORTED
如果没有,就以非事务方式执行;如果有,就将当前事务挂起。这就是 PROPAGATION_NOT_SUPPORTED
,这种方式非常强硬,没有就没有,有我也不支持你,把你挂起来,不鸟你。
PROPAGATION_NEVER
如果没有,就以非事务方式执行;如果有,就抛出异常。这就是 PROPAGATION_NEVER
,这种方式更猛,没有就没有,有了反而报错,确实够牛的,它说:我从不支持事务!
PROPAGATION_MANDATORY
如果没有,就抛出异常;如果有,就使用当前事务。这就是 PROPAGATION_MANDATORY,这种方式可以说是牛逼中的牛逼了,没有事务直接就报错,确实够狠的,它说:我必须要有事务!
需要注意的是 PROPAGATION_NESTED
,不要被它的名字所欺骗,Nested(嵌套),所以凡是在类似方法 A 调用方法 B 的时候,在方法 B 上使用了这种事务传播行为,如果您真的那样做了,那您就错了。因为您错误地以为 PROPAGATION_NESTED
就是为方法嵌套调用而准备的,其实默认的 PROPAGATION_REQUIRED
就可以帮助您,做您想要做的事情了。
事务超时
事务超时(Transaction Timeout):为了解决事务时间太长,消耗太多的资源,所以故意给事务设置一个最大时常,如果超过了,就回滚事务。
只读事务
只读事务(Readonly Transaction):为了忽略那些不需要事务的方法,比如读取数据,这样可以有效地提高一些性能。