多条 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 元才是)。
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 语句因为在保存点之后,所以被回滚咯。