事务
一个事务是由一个逻辑单元进行的一系列操作。一个事务的逻辑单元的操作必须满足4个属性,(ACID)原子性,一致性,独立性,持久性。
原子性(Atomictity)
一个事务必须是原子单元操作,对数据的修改必须全部执行或者全部不执行
一致性(Consistency)
事务执行结束的时候,所有的数据必须处于一致的状态。在关系型数据库中,所有的规则必须应用于事务的修改来保证数据完整性。在事务结束的时候,所有内部的数据结构,比如B树或者双边列表必须是正确的。
独立性(Isolation)
多并发下的事务修改必须保持独立性。一个事务获得的数据必须在其他事物修改之前或者完成修改以后,不应该获取任何处于中间状态的数据。这可以被理解为"串行化",因为如果我们使用相同的初始数据并且重新执行相同的一系列事务,我们最后获得的结果应该是一致的。
持久性(Durability)
当事务完成以后,在系统中的改变应该是永久的。甚至在系统宕机的情况下事务所完成的修改应该也是有效的。
说明和执行事务 Specifying and Enforcing Transactions
SQL程序员负责在开始和结束事务的时候保证数据的逻辑一致性。程序员必须根据业务要求定义一个数据修改的序列,来确保数据处于一致的状态。程序员在单个事务中执行数据修改语句,这样数据库才能保证事务执行的物理完整性。
数据库系统负责提供每个事务完整性的机制。数据库引擎提供:
- 锁机制保证事务的独立性
- 日志机制保证事务持久性。即使服务器硬件,操作系统或者数据库引擎本身宕机,数据库引擎可以在重启时使用事务日志自动将未完成事务回滚至系统故障点。
- 执行事务时确保一致性和原子性事务管理功能。事务开始后必须成功完成,或者数据库引擎撤销事务开始后所有对数据修改的操作。
数据读问题
在一个事务读取另外一个事务可能修改的数据的时候可能会出现某些"读现象"
比如我们有以下user表
id | name | age |
---|---|---|
1 | Joe | 20 |
2 | Jill | 25 |
脏读
当一个事务允许读另外一个事务修改但是未提交的数据时,可能发生脏读。
脏读和不可重复读(non-repeatable reads)类似。事务2没有提交造成事务1的语句两次执行得到不同的结果集。
然后我们有事务1和2 以下面顺序执行。事务2修改了一行,但是没有提交,事务1读了这个没有提交的数据。现在如果事务2回滚了刚才的修改或者做了另外的修改的话,事务1中查到的数据就是不正确的了。
事务1 | 事务2 |
---|---|
SELECT age FROM users WHERE id = 1;(20) | |
UPDATE users SET age = 21 WHERE id = 1;(没commit) | |
SELECT age FROM users WHERE id = 1; (21) | |
ROLLBACK; |
不可重复读
在一次事务中,当一行数据获取两遍得到不同的结果表示发生了“不可重复读”。
基于锁的并发控制中,"不可重复读"现象发生在执行SELECT操作时没有获得读锁或者SELECT操作执行完后马上释放了读锁;当没有要求一个提交冲突的事务回滚也会发生不可重复读。
事务1 | 事务2 |
---|---|
SELECT age FROM users WHERE id = 1;20) | |
UPDATE users SET age = 21 WHERE id = 1; COMMIT; |
|
SELECT age FROM users WHERE id = 1; (21) |
在可序列化和可重复读的隔离级别,数据库在第二次SELECT请求的时候返回事务2更新前的值(20)。但是在提交读和未提交读级别时,返回的是21,这个现象就是不可重复读。
幻读
在事务执行过程中,当两个完全相同的查询语句执行得到不同的结果集。这种现象称为“幻读(phantom read)”。
事务没有获取范围锁的情况下执行SELECT...WHERE可能会发生幻读。幻读是不可重复读的特殊场景:当事务1两次执行SELECT...WHERE检索一定范围内数据的操作时,事务2 在表中更改了一行数据(比如INSERT),这条数据正好满足事务1的WHERE子句。
事务1 | 事务2 |
---|---|
SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
|
INSERT INTO users VALUES ( 3, 'Bob', 27 ); COMMIT; |
|
SELECT * FROM users WHERE age BETWEEN 10 AND 30; |
事务1执行了两次相同的查询,如果设置了最高隔离级别,两次会得到相同的结果集。在较低的隔离级别上,第二次查询可能会得到不同的结果集。在可序列化隔离级别,事务1在age10到30的记录上加锁,事务2只能阻塞直到事务1提交。在可重复读级别,这个范围不会被锁定,允许记录插入。
隔离级别,读现象和锁
隔离级别 vs 锁现象
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | 可能发生 | 可能发生 | 可能发生 |
提交读 | 可能发生 | 可能发生 | |
可重复读 | 可能发生 | ||
可序列化 |
隔离级别 vs 锁持续时间
基于锁的并发控制中,隔离级别决定锁的持有时间。"C"表示锁持续到事务提交,"S"表示持续到当前语句执行完毕。如果锁在语句执行完毕就释放则另外一个事务就可以在这个事务提交前修改锁定的数据,导致以上的读现象出现。
隔离级别 | 写操作 | 读操作 | 范围操作(Select...Where) |
---|---|---|---|
未提交读 | S | S | S |
提交读 | C | S | S |
可重复读 | C | C | S |
可序列化 | C | C | C |
JDBC设置以及 AutoCommit
在JDBC中,我们可以在Connection通过setTransactionIsolation设置事务隔离级别, 这个只对当前connection有效。
Connection conn = null;
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
conn = DriverManager.getConnection("jdbc:xxxxxx");
Statement stmt = conn.createStatement();
/* conn.setTransactionIsolation(Connection.TRANSACTION_NONE);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
conn.setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);*/
conn.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE);
conn.setAutoCommit(false);
stmt.executeQuery("select * from user where id = 1");
stmt.execute("update user set age = 12 where id = 1");
stmt.executeQuery("select * from user where id = 1");
stmt.execute("update user set age = 15 where id = 2");
conn.commit();
} catch (SQLException | ClassNotFoundException e) {
try {
if(conn != null)
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
}
}
我们如果要设置事务隔离级别,首先需要把autoCommit设置为false,这样我们才可以把多条SQL Statement归并到一个事务中。当autoCommit设置为true的时候,每一条sql语句都会被认为是一条事务并且在完成时自动COMMIT。