数据库的事务
前言
什么是事务,它的解释很简单,简单到你觉得这个东西没有多重要。它的意思就是把好几句SQL语句当成一句话来执行。
但是这样简单的一句话,如果不结合代码和实践,很容易理解错误。我自己就是一直将其理解的不是很透彻(没错,这么简单我都理解错了,究其原因就是因为觉得太简单了,就没有用很多的代码去实践,这也导致我在下一步学习隔离级别的时候有很多地方理解错误。所以呀,学习知识还是要踏踏实实的学习,该搬砖的时候好好搬砖,不该搬砖的时候也可以搬搬砖,说不定就搬出不一样的感觉了,还是要慢慢来,比较快。)
事务的 ACID
事务具有四个特征:原子性( Atomicity )、一致性( Consistency )、隔离性( Isolation )和持续性( Durability )。这四个特性简称为 ACID 特性。
- 原子性
事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做。 - 一致性
事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是不一致的状态。 - 隔离性
一个事务的执行不能其它事务干扰。即要达到这么一种效果:对于任意两个并发的事务T1和T2,在事务T1看来,T2要么在T1开始之前就已经结束,要么在T1结束之后才开始,这样每个事务都感觉不到有其他事务在并发地执行。 - 持续性
也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。
PS:看完上面数据库的这四个特性,也许你觉得很简单(仅仅是这样嘛),也许你觉得很难(完全不懂在讲什么)。但是这些都不重要,重要的是接下来代码的实践。(笔者是觉得很简单的那一种,结果其实只是理解了表面的东西(深层次也很简单,但是要实践),还是要踏踏实实敲代码。)
上代码前需知
- MySQL的默认隔离级别是Read committed(呀,你可能看不懂,但是问题不大,不要有压力,下一章节的隔离级别我会带你慢慢的了解,现在不要着急,知道有这么回事就行了,后面再回头看看这些)。
- JDBC 使用
connection.setAutoCommit(false)
来关闭自动提交,使用connection.commit()
来手动提交,使用connection.rollback()
来回滚数据。(如果你看不懂提交和回滚,那不要担心,后面上了代码你自然就明白了,但是如果你不会JDBC,那对不起,你需要去学习JDBC了) - 事务的提交是以Connection为单位的(不是Statement),关于Connection的释放,每次使用时申请连接,事务结束后,释放连接,最好不要在两个线程的不同事务中使用同一个连接。(除非加锁,一个用完另一个再用,数据库连接池应该就是用了加锁的方法。)
上代码
直接上代码(使用commit()提交)
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Performer performer1 = new Performer("阳光照", "木木",
"男", 23, "2+1工作室", "陕西", "1311221112");
Performer performer2 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "1311222112");
Performer performer3 = new Performer("阳光照", "1阳阳",
"男", 23, "2+1工作室", "陕西", "131122112");
Performer performer4 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "111221112");
Performer performer5 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "11221112");
PerformersDAO.getConnection().setAutoCommit(false);
PerformersDAO.addPerformerByPs(performer1);
PerformersDAO.addPerformerByPs(performer2);
PerformersDAO.addPerformerByPs(performer3);
PerformersDAO.addPerformerByPs(performer4);
PerformersDAO.addPerformerByPs(performer5);
System.out.println("已增加,但是还未提交,沉睡20s后提交");
Thread.sleep(20000);
PerformersDAO.getConnection().commit();
System.out.println("已提交");
}
以上这段代码,分为三个阶段,第一个阶段——SQL语句执行前;第二个阶段——SQL语句执行后,但未提交(20s的Sleep中);第三个阶段——使用commit()方法提交。下面分别是三个阶段的数据库的截图,有图有真相,我就不用过多解释了。
第一个阶段——SQL语句执行前
第二个阶段——SQL语句执行后(20s的Sleep中)但未提交
咦,没有啥变化,因为还没有提交呀!所以你是不是大概已经知道提交是啥意思了吧,嘿嘿嘿。
第三个阶段——使用commit()方法提交
哈,懂了吧。
继续上代码(使用rollback()回滚)
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Performer performer1 = new Performer("阳光照", "木木",
"男", 23, "2+1工作室", "陕西", "1311221112");
Performer performer2 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "1311222112");
Performer performer3 = new Performer("阳光照", "1阳阳",
"男", 23, "2+1工作室", "陕西", "131122112");
Performer performer4 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "111221112");
Performer performer5 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "11221112");
PerformersDAO.getConnection().setAutoCommit(false);
PerformersDAO.addPerformerByPs(performer1);
PerformersDAO.addPerformerByPs(performer2);
PerformersDAO.addPerformerByPs(performer3);
PerformersDAO.addPerformerByPs(performer4);
PerformersDAO.addPerformerByPs(performer5);
System.out.println("已增加,但是还未提交,沉睡20s后回滚");
Thread.sleep(20000);
PerformersDAO.getConnection().rollback();
System.out.println("已回滚");
}
以上这段代码,分为三个阶段,第一个阶段——SQL语句执行前;第二个阶段——SQL语句执行后,但未提交(20s的Sleep中);第三个阶段——使用rollback()方法回滚。下面分别是三个阶段的数据库的截图,有图有真相,我就不用过多解
第一个阶段——SQL语句执行前
第二个阶段——SQL语句执行后,但未提交(20s的Sleep中)
没错,和上面一样没有变化
第三个阶段——使用rollback()方法回滚
咦,怎么还没有变化。没错,这就是rollback(),因为改变数据的操作都被回滚了。与commit()方法再进行比较,我想你就醍醐灌顶了。
隔离级别
前言
上一章节我们讲解了数据库的事务是什么,这一章节我们就来讲一讲数据库的隔离级别。要彻底理解隔离级别首先要明了几个很重要的原则:
- 隔离级别并不是越高就越好,世界上不存在完美,就像生活中也并不是最优秀的人就最幸福,所以做一个普通人心态良好才是最舒服的了。好了,扯得有点远,但是其实也并不远,因为编程其实也就是编写生活。咳咳,那为什么说隔离级别并不是越高就越好呢,因为隔离级别越高就越会影响线程的并发,至于为什么呢,请看下回分解。
- 数据库不同的隔离级别分别能解决哪些问题。这个非常重要,因为隔离级别的设置就是为了解决问题,所以如果你没有搞清楚这些问题,那么你的隔离级别就是白学。具体会有哪些问题,下一章节我会讲解,然后用编程来复现问题,让这些问题在不同的隔离级别直接被看到,现在你只需要知道当它出现的时候,这是重点。希望你也不要浮躁,自己也可以实验一下,来用代码复现这些问题。编程到最后,重要的可能就是这些最基本的东西了,而不是花里胡哨的操作。最近,我刚做完毕业设计,我是通信工程的学生,毕设题目是卫星定位相关,刚开始拿到这个题目,我也是一脸懵逼,但是随着后面得不断深入了解,发现卫星定位重要的是测距,而测距有主要分为两个步骤,这两个步骤最后究其根源就是自相关性和傅里叶变换。而其实自相关性和傅里叶变换再究其根源就是积分。如果一步一个脚印的学习,相信这个课题也很简单了。但是由于我之前并没有好好学习自相关性和傅里叶变换,所以这个论文课题我做的很艰难,但是不要向我学习啦,我是个反面教材。所以呀,核心还是那些最最基本的东西。与其花里胡哨的过眼云烟般的学习,不如沉下心来,慢慢地把这些最最基本的东西弄得明明白白,后面产生的问题归其根源也就是源于这些最基本的问题了。
- 隔离级别的设置只对当前链接有效,它是针对连接,而不是针对数据库。
- 设置数据库的隔离级别一定要是在开启事务之前!如果是使用JDBC对数据库的事务设置隔离级别的话,也应该是在调用Connection对象setAutoCommit(false)方法之前。调用Connection对象的setTransactionIsolation(level)即可设置当前链接的隔离级别,至于参数level,可以使用Connection对象的字段:
数据库的隔离级别(整篇代码错误,原因是没有搞懂事务的提交时用Connection的)
数据库有四种隔离级别,分别为 Read uncommitted,Read committed,Repeatable read,Serizable。
√:会出现 ×:不会出现
脏读,不可重复读,幻读,具体是什么,我会在代码出现的时候再进行解释。
Read uncommitted级别以及脏读
Read uncommitted
读未提交,也就是说事务所作的修改在未提交前,其他并发事务是可以读到的。
脏读
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
复现问题代码如下,这段代码的思路如下,先设置隔离级别为Read uncommitted,一个事务进行插入演员,等待2s后再提交事务,另一个事务在还没回滚事务之前进行查询。事务3在事务1回滚结束后再进行表查询。
事务1 | 事务2 | 事务3 |
---|---|---|
插入演员 | ||
Sleep(2000) | 查询所有演员 | |
回滚事务 | ||
查询所有演员 |
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Performer performer1 = new Performer("阳光照", "木木",
"男", 23, "2+1工作室", "陕西", "1311221112");
Performer performer2 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "1311222112");
Performer performer3 = new Performer("阳光照", "1阳阳",
"男", 23, "2+1工作室", "陕西", "131122112");
Performer performer4 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "111221112");
Performer performer5 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "11221112");
//设置隔离级别为READ_UNCOMMITTED
PerformersDAO.getConnection().setTransactionIsolation(Connection.TRANSACTION_READ_UNCOMMITTED);
PerformersDAO.getConnection().setAutoCommit(false);
//建立插入演员的线程
Thread threadAdd = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
PerformersDAO.addPerformerByPs(performer1);
PerformersDAO.addPerformerByPs(performer2);
PerformersDAO.addPerformerByPs(performer3);
PerformersDAO.addPerformerByPs(performer4);
PerformersDAO.addPerformerByPs(performer5);
System.out.println("以增加,但是还未提交,沉睡20s后回滚");
try {
Thread.sleep(2000);
PerformersDAO.getConnection().rollback();
} catch (InterruptedException | ClassNotFoundException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("已回滚");
System.out.println(PerformersDAO.queryAllPerformersByPS());
}
});
//建立查询演员的线程
Thread threadQuery = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
System.out.println(PerformersDAO.queryAllPerformersByPS());
}
});
threadAdd.start();
Thread.sleep(1000);
threadQuery.start();
Thread.sleep(1500);
threadQuery.start();
}
从最终结果可以看出,在事务未提交之前,就可以查询到了事务的改变,结果最后事务回滚,这个就叫做脏读。
试想公司发工资了,领导把5000元打到tom的卡上,但是还未提交事务,这时tom查看自己的银行卡,发现自己多了工资5000元,心里想着为什么这次工资少了,但是这时老板发现给tom算错工资了,是10000元,于是事务回滚,将工资修改为10000元,tom再次查银行卡发现自己的工资是10000元,心里总算解闷了。
Read committed级别以及不可重复读
Read committed:
读已提交,一个事务只能看到其他并发的已提交事务所作的修改。
不可重复读:
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
复现问题代码如下,这段代码的思路是,先设置隔离级别为Read committed,然后一个线程进行查询演员(未提交事务),然后等待2s,再次进行查询,在等待期间另一个线程改变演员信息(提交事务),然后上一个线程再查询,查询完后提交。
事务1 | 事务2 |
---|---|
查询指定演员 | |
Sleep(2000) | 修改指定演员信息并提交事务 |
再次查询指定演员并提交事务 |
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
// Performer performer1 = new Performer("阳光照", "木木",
// "男", 23, "2+1工作室", "陕西", "1311221112");
// Performer performer2 = new Performer("阳光照", "阳阳",
// "男", 23, "2+1工作室", "陕西", "1311222112");
// Performer performer3 = new Performer("阳光照", "1阳阳",
// "男", 23, "2+1工作室", "陕西", "131122112");
// Performer performer4 = new Performer("阳光照", "阳阳",
// "男", 23, "2+1工作室", "陕西", "111221112");
// Performer performer5 = new Performer("阳光照", "阳阳",
// "男", 23, "2+1工作室", "陕西", "11221112");
//设置隔离级别为READ_UNCOMMITTED
PerformersDAO.getConnection().setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
PerformersDAO.getConnection().setAutoCommit(false);
//建立插入演员的线程
Thread threadQuery = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
Performer performer = PerformersDAO.findPerformersByTelePhone("18191758092");
System.out.println("第一次查询,但是未提交事务,沉睡3s后再查询然后提交事务");
System.out.println(performer);
Thread.sleep(3000);
performer = PerformersDAO.findPerformersByTelePhone("18191758092");
System.out.println("第二次查询,查询完提交");
System.out.println(performer);
PerformersDAO.getConnection().commit();
} catch (InterruptedException | ClassNotFoundException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("已提交");
//System.out.println(PerformersDAO.queryAllPerformersByPS());
}
});
//建立查询演员的线程
Thread threadChange = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
PerformersDAO.changePerformerNikenameByPs("18191758092",
"西电刘德华");
System.out.println("修改");
PerformersDAO.getConnection().commit();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threadQuery.start();
Thread.sleep(500);
threadChange.start();
}
代码结果及分析:
从最终结果来看,在一个事务中,查询的结果却有两种。这就是不可重复读。
试想tom去超市购物,结账时系统读到卡里有10000元,而此时tom的老婆正在网上转账,把tom卡里的10000元转到了另一账户,并在tom前提交了事务,此时系统检查到tom的工资卡里已经没有钱了,tom非常纳闷,明明卡里有钱…
Repeatable read级别以及幻读
Repeatable read
REPEATABLE READ解决了脏读和不可重复读的问题,该级别保证了每行的记录的结果是一致的,但是却无法解决另一个问题。幻读。
幻读
幻读是事务非独立执行时发生的一种现象,例如事务T1批量对一个表中某一列列值为1的数据修改为2的变更,但是在这时,事务T2对这张表插入了一条列值为1的数据,并完成提交。此时,如果事务T1查看刚刚完成操作的数据,发现还有一条列值为1的数据没有进行修改,而这条数据其实是T2刚刚提交插入的,这就是幻读。
复现问题代码如下,大概思路是一个线程的事务进行查询所有演员的操作,然后隔2s后继续查询所有演员,再提交事务。在等待2s的过程中,另一个线程插入多个演员并提交事务。
事务1 | 事务2 |
---|---|
查询所有演员 | |
Sleep(2000) | 加入演员并提交事务 |
再次查询所有演员并提交事务 |
public static void main(String[] args) throws InterruptedException, SQLException, ClassNotFoundException {
Performer performer1 = new Performer("阳光照", "木木",
"男", 23, "2+1工作室", "陕西", "1311221112");
Performer performer2 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "1311222112");
Performer performer3 = new Performer("阳光照", "1阳阳",
"男", 23, "2+1工作室", "陕西", "131122112");
Performer performer4 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "111221112");
Performer performer5 = new Performer("阳光照", "阳阳",
"男", 23, "2+1工作室", "陕西", "11221112");
//设置隔离级别为READ_UNCOMMITTED
PerformersDAO.getConnection().setTransactionIsolation(Connection.TRANSACTION_REPEATABLE_READ);
PerformersDAO.getConnection().setAutoCommit(false);
//建立插入演员的线程
Thread threadQuery = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
ArrayList<Performer> performers = PerformersDAO.queryAllPerformersByPS();
System.out.print(performers);
System.out.println("第一次查询结果如上,但是未提交事务,沉睡2s后再查询然后提交事务");
Thread.sleep(2000);
performers = PerformersDAO.queryAllPerformersByPS();
System.out.print(performers);
System.out.println("第二次查询结果如上,查询完提交");
PerformersDAO.getConnection().commit();
} catch (InterruptedException | ClassNotFoundException | SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println("已提交");
//System.out.println(PerformersDAO.queryAllPerformersByPS());
}
});
//建立查询演员的线程
Thread threadChange = new Thread(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
try {
PerformersDAO.addPerformer(performer1);
PerformersDAO.addPerformer(performer2);
PerformersDAO.addPerformer(performer3);
PerformersDAO.addPerformer(performer4);
PerformersDAO.addPerformer(performer5);
System.out.println("增加演员");
PerformersDAO.getConnection().commit();
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SQLException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
});
threadQuery.start();
Thread.sleep(500);
threadChange.start();
}
代码执行结果如下:
从以上结果中可以看出,在一个事务中的两次查询,查询结果的行数不相同,这就是幻读的情况。
Serizable级别
该级别是最高级别的隔离级。它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。在这个级别,可能导致大量的超时Timeout和锁竞争Lock Contention现象,实际应用中很少使用到这个级别,但如果用户的应用为了数据的稳定性,需要强制减少并发的话,也可以选择这种隔离级。
隔离级别如何实现
想要学习数据库的隔离级别实现的原理需要对数据库中的锁进行了解。点击下面的链接进行学习。