说说数据库事务

多条 SQL 语句,要么全部执行成功,要么全部执行失败。

1 特性

数据库事务必须同时满足 4 个特性 ( ACID )。

特性 说明
原子性 Atomic 表示组成一个事务的多次数据库操作是一个不可分割的原子单元,只有所有的操作都执行成功,才提交整个事务 。 事务中的任何一次数据库操作失败,已经执行操作都必须回滚,让数据库返回到操作前的状态 。
一致性 Consistency 事务操作后,数据库所处的状态和它的业务规则是一致的 。比如 A 账户转账到 B 账户,不管操作是否异常, A 账户与 B 账户的总额是不变的。
隔离性 Isolation 在并发操作数据时,不同的事务拥有各自的数据空间,它们的操作既可能地不对对方产生干扰。数据库规定了多种事务隔离级别,不同的隔离级别对应不同的干扰程度 。 隔离级别越高,数据一致性越好,但并发性越差。
持久性 Durability 一旦事务提交成功,事务中所有的数据都必须被持久化到数据库中 。 即使在提交事务后数据库发生崩溃,那么当数据库重启时,也必须保证能够根据日志恢复数据 。

在这些事务特性中,数据的 “ 一致性 ” 是最终目标, 其他特性都是为了达到这个目标而采取的措施或要求。

数据库管理系统采用数据库锁来保证事物的隔离性,当多个事务试图对相同的数据执行操作时,只有持有锁的事务才能真正操作数据。

Oracle 采用了数据版本机制,在回滚阶段为数据的每一种变化都保留了一个版本,修改数据不会影响读取数据 。

2 并发问题

数据库中的相同数据,可能同时被多个事务所访问。所以,如果没有采取必要的隔离措施,就会导致各种并发问题,从而破坏数据的完整性 。

并发问题可以归结为 5 类,包括 3 类数据读问题(脏读 、 不可重复度 、 幻读)和 2 类数据更新问题(第一类丢失更新和第二类丢失更新)。

2.1 脏读(dirty read)

A 事务读取了 B 事务尚未提交的更改数据,并在此数据的基础上进行操作 。 如果此时 B 事务回滚,那么 A 事务之前读到的数据就是脏数据。

时间序列 事务 A 事务 B
1 开始事务 开始事务
2 - 查询账户余额(100 元)
3 - 取出 50 元
4 查询账户余额(50 元)【脏读】 -
5 - 回滚事务(账户余额:100 元)
6 存入 100 元 -
7 提交事务(账户余额:150 元) -

这里因为发生脏读,导致账户损失了 50 元(事务 A 存款 100 元,事务 B 无影响,再加上原来的账户余额 100 元,最后的账户余额应该是 200 元才是)。

扫描二维码关注公众号,回复: 4042390 查看本文章

2.2 不可重复读(unrepeatable read)

不可重复读指的是事务在不同的时间点,读取到的数据不同。

时间序列 事务 A 事务 B
1 开始事务 开始事务
2 - 查询账户余额(100 元)
3 查询账户余额(100 元) -
4 - 取款 10 元
5 - 提交事务(账户余额:90 元)
6 查询账户余额(90 元) -

在时间序列 6,与在时间序列 3 时查询到的余额不同,发生不可重复读现象。

2.3 幻读(phantom read)

幻象读一般发生在计算统计数据的事务中 。 A 事务读取了 B 事务提交的新增数据,这时 A 事务将出现幻象读的问题 。

假设在同一个事务中,两次统计名某银行支行所有账户的总金额,在两次统计过程中,刚好新增了一个存款账户 。那么,这两次统计的总金额肯定会不一致 。

时间序列 事务 A 事务 B
1 开始事务 开始事务
2 统计(总金额:10 w) -
3 - 新增存款账户(金额:1 w)
4 - 提交事务(总金额:11 w)
5 统计(总金额:11 w)幻读 -

2.4 不可重复读与幻读比较

比较 不可重复读 幻读
读取对象 读到其它事务已经提交的修改或删除数据。 读到其它事务已经提交的新增数据。
采取措施 对所要操作的数据添加级锁,避免这些数据发生变化。 对所要操作的数据所在表添加级锁,即将整张表锁定(在 Oracle 中,是以多版本数据的方式实现的)。

2.5 第一类丢失更新

A 事务回滚时,把 B 事务中已经提交的更新数据给覆盖咯 。

时间序列 事务 A 事务 B
1 开始事务 开始事务
2 查询账户余额(100 元) -
3 - 查询账户余额(100 元)
4 - 取款 10 元
5 - 提交事务(账户余额:90 元)
6 存入 10 元 -
7 提交事务(账户余额:110 元) -

这个问题影响很大。这个例子中,账户余额应该还是 100 元(取款 10 元,存入 10 元,实际对账户无影响),但因为存在第一类丢失更新,导致银行损失 10 元。如果事务 A 先提交,那么账户将损失 10 元。

2.6 第二类丢失更新

A 事务提交后覆盖了 B 事务已经提交的数据,导致 B 事务所做操作丢失。

时间序列 事务 A 事务 B
1 开始事务 开始事务
2 - 查询账户余额:100 元
3 查询账户余额:100 元 -
4 - 取款 10 元
5 - 提交事务(账户余额:90 元)
6 存款 10 元 -
7 提交事务(账户余额:110 元) -

上述示例,直接导致银行损失 10 元。如果 A 事务先提交,那么将导致账户损失 10 元。

3 锁机制

分类方式 类别
锁定对象 表锁定(整张表)、行锁定(特定行)
并发事务锁定关系 共享锁定(运行其它的共享锁定,但防止独占锁定)、独占锁定(防止任何锁定)

oracle 数据库中常见的锁定:

锁定 说明 防止 允许
行共享锁定 可通过 select for update 语句隐式获得该锁定,或者通过 LOCK TABLE IN ROW SHARE MODE 语句显式获取 。 表独占锁定 行共享锁定、行独占锁定、表共享行独占锁定
行独占锁定 可通过 insert、update、delete 语句隐式获取,或者通过 LOCK TABLE IN ROW EXCLUSIVE MODE 语句显式获取 。 行或表共享锁定、行或表独占锁定 -
表共享锁定 可通过 LOCK TABLE IN SHARE MODE 语句显式获取。该锁定可以让会话具有对表事务级的一致性访问,因为其他会话在用户提交或者回滚该事务并释放对该表的锁定之前,不能更改这张表 。 表共享行独占锁定、表独占锁定 行共享锁定、表共享锁定
表共享行独占锁定 可通过 LOCK TABLE IN SHARE ROW EXCLUSIVE MODE 语句显式获取。 表共享行独占锁定、行独占锁定、表独占锁定 其它行的共享锁定
表独占锁定 可通过 LOCK TABLE IN EXCLUSIVE MODE 显式获取。 所有锁定 -

上式表中的防止与允许列都是针对其它会话而言的。

4 事务的隔离级别

因为直接使用锁比较麻烦,所以数据库为我们设置了事务的隔离级别,这些级别实现了自动锁机制 。 设置好事务的隔离级别后,数据库就会分析事务中的 SQL 语句,然后自动为事务所操作的数据加上适合的锁 。 而且,数据库还会维护这些锁,当一个资源上的锁数目太多时,就会自动升级,从而提高系统的运行性能。这些过程对我们来说是完全透明的。

ANSI/ISO SQL 92 定义了 4 个等级的隔离级别:

隔离级别 脏读 不可重复读 幻读 第一类丢失更新 第二类丢失更新
READ UNCOMMITTED 允许 允许 允许 不允许 允许
READ COMMITTED 不允许 允许 允许 不允许 允许
REPEATABLE_READ 不允许 不允许 允许 不允许 不允许
SERIALIZABLE 不允许 不允许 不允许 不允许 不允许

隔离级别与并发性是对立的,READ UNCOMMITTED 并发性最高,而 SERIALIZABLE 的并发性最低。

因为 Oracle 通过多版本机制,彻底解决了脏读问题,所以它的 READ COMMITTED 已经达到 SQL 92 定义的 REPEATABLE_READ 标准。

SQL 92 推荐使用的隔离级别是:REPEATABLE_READ。

5 JDBC 事务

我们可以通过 Connection 的 getMetaData() 方法获取 DatabaseMetaData 对象,然后通过该对象的 supportsTransactions()supportsTransactionIsolationLevel(int level) 方法查看底层数据库的事务支持情况 。

Connection 在默认情况下是自动提交的,也就是说,每一条执行的 SQL 都对应一个事务。为了能够将多条 SQL 放在一个事务中执行,我们可以通过 Connection 的 setAutoCommit(false) 来关闭 Connection 的自动提交机制,还可以通过 Connection 的 setTransactionIsolation() 来设置事务的隔离级别, Connection 中定义了 SQL 92 标准中的 4 个事务隔离级别常量 。


Connection connection = null;

try {
	String url = "xxx";

	//获取数据库连接
	connection = DriverManager.getConnection(url);

	//关闭自动提交机制
	connection.setAutoCommit(false);

	//设置事务隔离级别
	connection.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);

	Statement statement = connection.createStatement();
	String sql = "xxx";
	statement.execute(sql);

	//提交事务
	connection.commit();


} catch (Exception e) {
	e.printStackTrace();
	try {
		//回滚事务
		connection.rollback();
	} catch (SQLException e1) {
		e1.printStackTrace();
	}
}
复制代码

JDBC2.0 中事务只有提交与回滚操作 。在 JDBC3.0 中(Java1.4+)引入了保存点( SavePoint 接口)。保存点可以把事务分割为多个阶段,这样我们就可以根据业务要求,来指定需要回滚到的特定保存点啦O(∩_∩)O~

我们可以通过 DatabaseMetaData 的 supportsSavepoints() 方法验证所连接的数据库是否支持保存点特性 。


Statement statement = connection.createStatement();
String sql1 = "xxx";
statement.execute(sql1);

//设置保存点
Savepoint savepoint=connection.setSavepoint();

String sql2 = "xxx";
statement.execute(sql2);

//回退到保存点
connection.rollback(savepoint);
复制代码

如果事务提交了上段代码, 那么 sql1 语句将有效,而 sql2 语句因为在保存点之后,所以被回滚咯。

猜你喜欢

转载自juejin.im/post/5be64f27f265da617006a334