事务的ACID属性、解决脏读、不可重复读、幻读
事务的ACID属性
-
原子性(Atomictiy)原子性是指事务是一个不可分割的单位,事务中的操作要么都发生,要么都不发生。
简单的来说就是在事务操作中,比如我通过两条SQL 改两条数据,要么这两个操作都完成,要么都不完成就回滚。
-
一致性(Consistency)事务必须从一个一致性状态变换到另一个一致性状态。
比如转账操作,从A转给B 100元钱,那么A 少100,B 收到100,即这个转账操作就是从未转账状态到转账成功状态,类似于事务一致性的体现。
-
隔离性(Isolation)事务的隔离性,是指一个事务的执行不被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
类似于多线程中的线程资源竞争问题。
-
持久性(Durability)一个事务一旦被提交,他对数据库中数据的改变是永久性的,接下来的其他操作对数据库故障不应该对其有任何影响。
事务操作完数据,这个数据就被持久化了,无论数据库断电、还是故障、等都不应该再改变操作完的数据,体现其持久性。
事务并发引起的问题
事务并发一般会引起如下三个问题,分别是 脏读、不可重复读、幻读,这三个里面,脏读是我们必须要解决的。
1.脏读
-
对于两个事务,T1 和 T2, T1 读取了已被T2 更新,但是还没有被提交的字段,之后,若T2 回滚,T1 读取的数据就是临时的且无效的数据。
- 比方说,改一条数据,事务T1 用于操作改数据,T2 用于读取该后的数据,当T1 在改完还未提交事务时,T2 这个时候去读取T1 的数据,居然读出来时改之后的,这显然是有问题的,因为T1 有可能操作失败回滚,或者说T1 还未改完数据,这时T2 去读未改完的数据显然是不合理的。
2.不可重复读
-
对于两个事务,T1,T2, T1 读取了一个字段,然后T2 更新了该字段,之后T1 再去读取同一个字段,值就不一样了。
-
例如,我在买东西的时候,看到库存不足了,这个时候我没有关浏览器,再次刷新了浏览器,结构看到有库存了,这时因为后台数据库增加了库存,而我又没有断开此次事务连接,这样就读取到了最新的数据。(这种情况就是不可重复读问题)
-
可重复读,意思是说我一个数据库事务连接 没有断开,读取的就是我这个事务之前读取的数据,数据更新数据(事务提交),我也不应该读取事务提交后的数据,因为我这都是在一个事务中的。就说不管更新数据的事务有没有提交,只要我当前查询的事务没有关闭,就会读取之前我查询出来的状态,不管另一个更新的事务操作。
-
-
我们为了解决不可重复读问题,需要让他可重复读,然而实际上大多数情况 不可重复读问题是 不需要解决的。
什么又叫可重复读呢?
-
意思就算另一个事务,改了数据(即使他commit了),如果 当前 读取数据的事务操作没有关闭,则读取的数据就还是之前的数据,如图:
-
这种模式下,想要读取提交后的数据,必须重新起一个事务。
3.幻读
-
对于两个事务 T1,T2 T1从一张表中读取了一个字段后,然后T2在表中 插入了一些新的行,之后,如果T1 再次读取同一个表,就会多出几行。
- 这里它强调的是插入操作,例如我一个事务T1读数据读出了100条,另一事务T2此时正在向此表插入数据,这时我第一个事务T1读出来就比100条多了,这种情况就是幻读。
总结
注意: 不可重复读,指的是 更新 操作,而幻读,指的是 插入 操作。
一个事务与其他事务隔离的程度称为隔离级别。数据库规定了多种事务的隔离级别,不同隔离级别应对不同的干扰程度。
隔离级别越高,数据一致性就越好,但并发性就越弱,类似于多线程加锁,隔离性好但性能差。
一般情况下,不可重复读问题是可以接受的,一般我们只需要解决脏读的问题。
四种隔离级别
隔离级别 | 描述 |
READ UNCOMMITED 读未提交数据 | 允许事务读取未被其他事务提交的变更,脏读,不可重复读和幻读问题都会出现 |
READ COMMITED读已提交数据 | 只允许事务读取已被其他事务提交的变更,可以避免脏读,但不可重复读和幻读问题依然存在 |
REPEATABLE READ 可重复读 | 确保事务可以多次从一个字段中读取相同的值,在这个事务持续期间,禁止其他事务对这个字段进行更新,可以避免脏读和不可重复读问题,但幻读问题依然存在。 |
SERIALLZABLE 串行化 | 确保事务可以从一个表中读取相同约行,在这个事务持续期间,禁止其他事务对表执行插入更新删除操作,所有问题都可避免,但性能十分低下 |
-
Oracle 支持2种事务隔离级别,
READ COMMITED
,SERIALIZABLE
. 它默认隔离级别为READ COMMITED
. -
隔离级别越往下,则 一致性越好,并发性越差。
-
一般情况下,脏读问题必须解决,不可重复读和幻读问题是可以接受的。
Java 隔离级别演示
如下代码
一段查询代码,根据设置的隔离级别查询数据,隔离级别如下:
TRANSACTION_REPEATABLE_READ 为可重复读,避免了脏读和可不可重复读问题.
TRANSACTION_READ_UNCOMMITTED 读未提交数据,这种隔离级别存在脏读,会读取未提交的数据
TRANSACTION_READ_COMMITTED 读已提交的数据,如果修改的数据未提交,则读出来还是老的修改之前的数据
TRANSACTION_SERIALIZABLE 串行化,解决了脏读,不可重复读,幻读。 一般不用,因为并发效率低。
查询更新的数据
一般我们都会将隔离级别设置为 TRANSACTION_READ_COMMITTED
, 这样更新的事务如果没有提交,则读取的都是之前的值,这里只会读取已提交的数据。
@Test
public void testTransactionSelect() {
Connection conn = JDBCUtils.GetDBConnection();
try {
//获取取数据库的隔离级别,当前为TRANSACTION_REPEATABLE_READ
//TRANSACTION_REPEATABLE_READ 为可重复读,避免了脏读和可不可重复读问题.
//TRANSACTION_READ_UNCOMMITTED 读未提交数据,这种隔离级别存在脏读,会读取未提交的数据
//TRANSACTION_READ_COMMITTED 读已提交的数据,如果修改的数据未提交,则读出来还是老的修改之前的数据
conn.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
System.out.println(conn.getTransactionIsolation());
//取消自动提交数据
conn.setAutoCommit(false);
String sql = "SELECT `user`,`password`,balance FROM balance WHERE `user` = ?";
DBCommand dbCommand = new DBCommand(conn);
BalanceEntity balance1 = dbCommand.queryMethod(BalanceEntity.class, sql, "jerry");
System.out.println(balance1);
} catch (Exception ex) {
ex.printStackTrace();
}finally {
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
更新数据操作
如下更新操作中,将数据库中的balance改为6000,中途等待15秒后提交,那么在这15秒内,查询数据库中的数据依然是之前的值。
@Test
public void testTransactionUpdate() {
Connection conn = JDBCUtils.GetDBConnection();
DBCommand dbCommand = new DBCommand(conn);
try {
String sql = "UPDATE balance SET `balance` = ? WHERE `user`= ?";
conn.setAutoCommit(false);
int updateResult = dbCommand.updateMethod(conn, sql, 6000, "jerry");
if (updateResult > 0) {
System.out.println("更新数据成功!");
}
Thread.sleep(15000);
conn.commit();
} catch (Exception ex) {
ex.printStackTrace();
}finally {
try {
conn.close();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
如之前的值为 2000 ,隔离级别设置为 TRANSACTION_READ_COMMITTED
,那么在这15秒未提交的时间里,数据库查询结果和代码结果都是2000:
数据库中查询结果
代码中查询结果
15秒后,数据提交,则查询结果如下: