写在前面
在谈到数据库的事务隔离级别之前,需要先谈谈数据库的事务以及它的ACID 特性。事务隔离级别指的是一个事务必须与由其他事务进行的资源或数据更改相隔离的程度。隔离级别从允许的并发副作用(例如,脏读或虚拟读取)的角度进行描述。
事务的ACID 特性可以理解为对事务的强制性要求,也就是说理想状态下的事务应该是具有这些性质的。但现实情况是达到这些性质会非常影响性能。相比于强一致性来说,糟糕的性能更加难以接受。所以就在一定程度上进行了屈服,以此保证数据操作的执行速度。
这种程度反映到具体的概念上来讲,就是事务的隔离级别了。在性能(执行速度)与安全(称之为安全也不太合适,更为确切的应该是多事务执行时,数据能够互相影响的程度)之间,进行不断取舍而构成的可能性选项。
数据库事务是数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
数据库事务拥有以下四个特性,习惯上称之为ACID 特性
原子性:事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
场景模拟:更新数据库中的name字段和age字段
static String UPDATE_NAME = "UPDATE student SET `name` = 'zhangsan'";
static String UPDATE_AGE = "UPDATE student SET age = 15";
static String SELECT_ALL = "SELECT * FROM student";
public static void main(String[] args) throws Exception{
final Connection connection = DbUtils.getConnection();
// 无事务情况下:
/*try {
noTransactionEnvir(connection);
} catch (Exception e) {
System.out.println("异常抛出:"+e.getMessage());
}
selectAll(connection,"无事务--最终执行结果-@-: ");*/
// 有事务情况下:
try {
transactionEnvir(connection);
} catch (Exception e) {
System.out.println("异常抛出:"+e.getMessage());
}
selectAll(connection,"有事务--最终执行结果-@-: ");
// 资源清理
connection.close();
}
public static void noTransactionEnvir(Connection connection) throws Exception{
selectAll(connection,"无事务--初始化-@-: ");
try (PreparedStatement preparedStatement = connection.prepareStatement(UPDATE_NAME)){
int i = preparedStatement.executeUpdate();
System.out.println("更新name字段成功!影响行数: "+i);
selectAll(connection,"无事务--更新name-@-: ");
}catch (Exception e){
e.printStackTrace();
}
throwEx();
try (PreparedStatement preparedStatement = connection.prepareStatement(UPDATE_AGE)){
int i = preparedStatement.executeUpdate();
System.out.println("更新age字段成功!影响行数: "+i);
selectAll(connection,"无事务--更新name-@-: ");
}catch (Exception e){
e.printStackTrace();
}
}
public static void transactionEnvir(Connection connection) throws Exception{
selectAll(connection,"有事务--初始化-@-: ");
connection.setAutoCommit(false);
Statement statement = null;
try (PreparedStatement preparedStatement = connection.prepareStatement(UPDATE_NAME)){
int i = preparedStatement.executeUpdate();
System.out.println("更新name字段!影响行数: "+i);
selectAll(connection,"有事务--更新name-@-: ");
throwEx();
statement = connection.createStatement();
int i1 = statement.executeUpdate(UPDATE_AGE);
System.out.println("更新age字段!影响行数: "+i1);
selectAll(connection,"有事务--更新name-@-: ");
System.out.println("提交事务");
// 当执行到该命令后,数据会被真正的提交
connection.commit();
}catch (Exception e){
System.out.println("异常抛出:"+e.getMessage());
// 在数据被真正提交之前,进行回滚操作
connection.rollback();
}
}
private static void selectAll(Connection connection , String prefix){
try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_ALL)){
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
System.out.println(prefix+"name:"+resultSet.getString(2)+",age:"+resultSet.getInt(3));
}
}catch (Exception e){
e.printStackTrace();
}
}
private static void throwEx() throws Exception{
// 模拟异常发生
throw new Exception("我收到了程序员大大给的异常指令");
}
当出现异常,数据会进行回滚,执行的更新操作不会同步到数据库中。
当调用了 commit 方法,数据会被真正的提交。此后再调用 rollback 也无法回滚。
当出现异常,调用 rollback 方法,使用当前的connection 查询,不能够查询到更新后的数据,并且数据库也没有同步该数据。
当出现异常,无法调用 commit 方法,也无法调用 rollback 方法,使用当前的connection 查询 ,能够查询到更新后的数据,但数据库并没有同步该数据。
总结:commit 和 rollback 都能够终结事务,只要当一方执行,当前的事务便已经终结,另一方执行将无法产生任何影响。事务相当于封装了一系列的数据库操作序列,当执行到某个序列出现异常时,可以回滚之前已经执行成功的操作。确保数据要么全部被执行,要么都不执行,不可能停滞在中间某个环节,也不会被其它事务所干扰。
一致性:事务应确保数据库的状态从一个一致状态转变为另一个一致状态。一致状态的含义是数据库中的数据应满足完整性约束。
一致性是指数据处于一种语义上的有意义且正确的状态。一致性是对数据可见性的约束,保证在一个事务中的多次操作的数据中间状态对其他事务不可见的。因为这些中间状态,是一个过渡状态,与事务的开始状态和事务的结束状态是不一致的。
原子性和一致性的的侧重点不同:原子性关注状态,要么全部成功,要么全部失败,不存在部分成功的状态。而一致性关注数据的可见性,中间状态的数据对外部不可见,只有最初状态和最终状态的数据对外可见。
在未提交读的隔离级别下,会造成脏读,这就是因为一个事务读到了另一个事务操作内部的数据。ACID中是的一致性描述的是一个最理想的事务应该怎样的,是一个强一致性状态,如果要做到这点,需要使用排它锁把事务排成一队,即Serializable(串行)的隔离级别,这样性能就大大降低了。现实是骨感的,所以使用隔离性的不同隔离级别来破坏一致性,来获取更好的性能
隔离性:多个事务并发执行时,一个事务的执行不应影响其他事务的执行。
场景模拟:线程一更新age 字段,使用线程协调工具,保证线程二在线程一更新完age 字段后,再执行查询和更新age为age+1字段操作。
持久性:已被提交的事务对数据库的修改应该被永久保存在数据库中。
事务提交成功,数据保存成功,一次完整的操作执行完毕,数据对其它的数据库客户端程序可见。
如果在没有事务隔离性的情况下,并发访问问题是我们需要直接面对的问题。
那么,并发访问能够带来哪些问题呢?
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
4类隔离级别如下:
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(read-uncommitted) | 是 | 是 | 是 |
读已提交(read-committed) | 否 | 是 | 是 |
可重复读(repeatable-read) | 否 | 否 | 是 |
串行化(serializable) | 否 | 否 | 否 |
mysql 默认的事务隔离级别为repeatable-read。
Java 代码模拟脏读、不可重复读和幻读现象
static Integer id = 202;
static String SELECT_ALL = "SELECT * FROM student";
static String UPDATE_AGE = "UPDATE student SET age = age+1";
private static final ExecutorService poolExecutor = new ThreadPoolExecutor(2, 5, 1,
TimeUnit.HOURS, new LinkedBlockingQueue<Runnable>(), r->new Thread(r,"tcIsoLevel"));
public static void main(String[] args) {
Connection a = DbUtils.getConnection();
Connection b = DbUtils.getConnection();
//修改数据库的隔离级别 运行:help ISOLATION 按指示操作
// a 读取到了事务b未提交的数据
//dirtyRead(a,b);
// a 在同一事务中读取的数据不一致
//unrepeatableRead(a,b);
phantomRead(a,b);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
poolExecutor.shutdown();
}
/**
* 脏读模拟
* @author duofei
* @date 2019/7/3 16:10
* @param
* @return
* @throws
*/
private static void dirtyRead(Connection a,Connection b){
// A事务读取到B事务更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。
CountDownLatch aContinue = new CountDownLatch(1);
CountDownLatch bContinue = new CountDownLatch(1);
CountDownLatch aContinue1 = new CountDownLatch(1);
poolExecutor.execute(()->{
try {
a.setAutoCommit(false);
aContinue.await();
selectAll(a,"a在b修改数据后回滚之前查询所有数据:");
bContinue.countDown();
a.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
a.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
try {
aContinue1.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
selectAll(a,"a在b回滚之后查询所有数据:");
});
poolExecutor.execute(()->{
PreparedStatement preparedStatement = null;
try {
b.setAutoCommit(false);
preparedStatement = b.prepareStatement(UPDATE_AGE);
preparedStatement.executeUpdate();
aContinue.countDown();
bContinue.await();
// 模拟异常抛出,导致回滚
throwEx();
b.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(preparedStatement != null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
try {
b.rollback();
aContinue1.countDown();
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
/**
* 不可重复读模拟
* @author duofei
* @date 2019/7/3 16:11
* @param
* @return
* @throws
*/
private static void unrepeatableRead(Connection a,Connection b){
// A事务多次读取同一数据,事务B在事务A读取的过程中。对事务作了更新并提交,导致事务A 多次读取同一数据时,结果不一致
CountDownLatch aContinue = new CountDownLatch(1);
CountDownLatch bContinue = new CountDownLatch(1);
CountDownLatch aContinue1 = new CountDownLatch(1);
CountDownLatch aContinue2 = new CountDownLatch(1);
poolExecutor.execute(()->{
try {
a.setAutoCommit(false);
aContinue.await();
selectAll(a,"a在b修改数据前查询所有数据:");
aContinue1.await();
selectAll(a,"a在b修改数据之后,提交事务之前查询所有数据:");
bContinue.countDown();
aContinue2.await();
selectAll(a,"a在b提交事务之后查询所有数据:");
a.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
a.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
});
poolExecutor.execute(()->{
PreparedStatement preparedStatement = null;
try {
b.setAutoCommit(false);
aContinue.countDown();
preparedStatement = b.prepareStatement(UPDATE_AGE);
preparedStatement.executeUpdate();
aContinue1.countDown();
preparedStatement = b.prepareStatement(UPDATE_AGE);
preparedStatement.executeUpdate();
bContinue.await();
b.commit();
aContinue2.countDown();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(preparedStatement != null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
try {
b.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
});
}
/**
* 幻读模拟
* @author duofei
* @date 2019/7/3 16:12
* @param
* @return
* @throws
*/
private static void phantomRead(Connection a,Connection b){
// 当事务A读取数据,事务B在此时插入了一条数据,事务a再次读取数据,发现还有一条之前没有读到过的数据,像产生幻觉一样
CountDownLatch aContinue = new CountDownLatch(1);
CountDownLatch aContinue1 = new CountDownLatch(1);
CountDownLatch bContinue = new CountDownLatch(1);
// 数据准备
try{
PreparedStatement prstat = a.prepareStatement(insertOne());
prstat.executeUpdate();
PreparedStatement preparedStatement = a.prepareStatement(insertOne());
preparedStatement.executeUpdate();
prstat.close();
preparedStatement.close();
}catch (Exception e){
e.printStackTrace();
}
selectAll(a,"初始化数据查询:");
poolExecutor.execute(()->{
try {
a.setAutoCommit(false);
aContinue.await();
selectAll(a,"a在b修改数据前查询所有数据:");
bContinue.countDown();
aContinue1.await();
selectAll(a,"a在事务b提交完毕查询数据:");
a.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
a.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
});
poolExecutor.execute(()->{
PreparedStatement preparedStatement = null;
try {
b.setAutoCommit(false);
aContinue.countDown();
bContinue.await();
preparedStatement = b.prepareStatement(insertOne());
preparedStatement.executeUpdate();
b.commit();
} catch (Exception e) {
e.printStackTrace();
} finally {
if(preparedStatement != null){
try {
preparedStatement.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
try {
b.rollback();
} catch (SQLException e) {
e.printStackTrace();
}
}
aContinue1.countDown();
selectAll(b,"事务b提交完事务查询数据:");
});
}
private static void selectAll(Connection connection , String prefix){
try (PreparedStatement preparedStatement = connection.prepareStatement(SELECT_ALL)){
ResultSet resultSet = preparedStatement.executeQuery();
while(resultSet.next()){
System.out.println(prefix+"name:"+resultSet.getString(2)+",age:"+resultSet.getInt(3));
}
}catch (Exception e){
e.printStackTrace();
}
}
private static void throwEx() throws Exception{
// 模拟异常发生
throw new Exception("我收到了程序员大大给的异常指令");
}
private static String insertOne(){
return "INSERT INTO student VALUES('"+(++id)+"','wang"+id+"',"+id+",'class"+id+"')";
}
注:mysql 在 REPEATABLE-READ 级别下已经能够有限的避免幻读现象。
总结:脏读是由于另一事务的回滚,造成了当前事务读取到了错误的数据;不可重复读是由于当前事务在读取了数据后,另一事务修改了数据,导致当前事务再次读取数据,结果数据内容不一致;幻读则是由于当前事务在读取过程中,另一事务插入了数据,导致当前事务读取到的数据数量不对。
脏读强调了读取到了未提交事务的数据;不可重复读则强调读取到了事务修改的数据;幻读则强调读取到了事务插入的数据。
反过来想,如果事务完全串行,是不是就不存在上面的问题了呢?如果事务完全并行,那是不是完全无法信任读取的数据。所以,劳动人民的智慧是无穷的,基于隔离级别来解决这个问题,我们只要负责选择我们能够接受的现象就好了,有没有很赞?哈哈哈…
参考博文
怎么理解一致性
一文搞定MySQL的事务和隔离级别