1. 数据库事务的概念:
1) 事务的目的就是为了保证数据库中数据的完整性;
2) 设想一个银行转账的过程,如果分两步,第一步是A的账户-1000,第二步是B的账户+1000,这两个动作必须是连贯的,如果中间断开(出现故障等)比如第一步执行完之后发生异常而终止了操作,那么A就白扣了1000,而B的账户也没有钱增加,这就发生了非常严重的错误;
!!以上这个案例可以看出:
a. 这两步必须是连贯的,一起合成的,应该作为一个整体逻辑执行单元来执行;
b. 如果两步顺利执行完毕那么数据就是完整的,如果中间断开,那么断开时数据就是不完整的(错误的);
!!以上可以总结出,上面两步要么必须全部都做完,要么就全部都不做,否则就会导致数据错误!!
3) 为解决以上问题,只要将上面两步包装成一个事务即可,那么事务具有哪些特点呢?
i. 首先事务肯定包含了若干数据库操作(肯定都是修改数据库的操作),因为只有修改(update)才有可能导致数据的不完整,而事务就是为了解决不完整问题的;
ii. 事务最大的特点就是要么不做,要做就全部做完,那它是如何实现这个要求的呢?这个问题从两方面来说:
a. 第一就是要做就全部都做完,这当然是最好的,如果每一步执行都是顺顺利利地没有出现任何意外,那就自然全部都做完了,这没什么好说的;
b. 那如何做到“要么就不做“?你又不能预测到本次执行是否会出现异常!很多异常都是一些随机因素造成的呀!
c. 很显然,做还是要做的,关键是做了一半出现了异常应该怎么解决?
d. 其实原理很简单,一旦出现了异常,就把刚刚做过的修改撤销了不就行了吗?其实事务就是这样来实现“要么就不做的“;
e. 这个操作就叫做回滚:事务在执行的时候会直接将更新底层的数据,如果一旦发生了暂时无法解决的异常情况,就会立马终止事务,并撤销刚刚执行的所有更新,将数据库还原到事务执行之前的状态,这就是事务的回滚了!
4) 事务要生效必须要提交:
i. 即使把事务完整的执行完毕了必须要提交才能使对数据的修改真正生效;
ii. 如果不提交就结束事务了,即使前面全部都执行完了也会全部回滚掉;
iii. 提交操作即commit,事务执行完毕后一定要记得commit使之真正生效哦!
!!实际上在提交之前对数据的修改都只是在内存(缓存)映像中进行的,之前做完修改后使用select查询会发现结果中数据确实被更新了,但那只是假象,因为被更新的只是内存中的映像,如果你未提交就断开重新连接,再进入后select以下结果发现并未修改;
!!只有提交commit后才会把对内存映像做出的修改永久地写入物理存储设备中!!
!!commit后断开再重连,select一下就会发现数据真正被修改了;
2. 事务的理论级概念:
1) 从上面的例子可以看出事务具有以下4个特性(合成ACID):
i. 原子性(Atomicity):事务是最小的执行单位,不可分割,必须一次作为一个整体执行完;
!这是显然的,上述转账的例子,分割执行了必然导致分割点位置出现数据的不一致性;
!!其实上述银行转账是特例,事务你可以随意定义,不会导致不一致性的两个操作也可以组成一个事务,只不过执行的时候会按照事务的性质进行;
ii. 一致性(Consistency):事务如果中间被割裂可能会导致数据的不一致性,因此事务最终的目的就是为了保证数据的完整性和一致性,而这个性质是由原子性来保障的;
iii. 隔离性(Isolation):并发事务之间不能相互影响(并发事务竞争的数据必然被同不监视!),原因很简单,那就是原子性!并发事务之间不能看到对方的中间状态!
!!可见原子性是事务的根本属性,其它特性都是由原子性保证的;
iv. 持续性(Durability):也称为持久性,是指事务一旦提交,那么对数据的修改就会永久保存到物理存储器中!为提交之前只是在内存映像中进行修改!
2) 事务的内容和提交:
i. 事务必须是由DML语句组成的:这是显然的,事务就是为了防止修改数据时发生数据的不一致!
!!但中间允许出现select语句,但是select语句并不属于事务的一部分,以为select语句并不修改数据,仅仅就是临时查看以下结果而已;
ii. 最多只能出现一条DDL或者DCL语句,并且必须作为最后一句:以为DDL和DCL默认会自动触发提交动作!出现DDL或者DCL就意味着该事务到此终止!
iii. 显式提交和隐式提交:显式提交就是手动显式执行commit命令,隐式提交就是执行DML或DCL语句,在JDBC编程中顺利从方法中正常退出也会隐式自动提交!
3) 回滚:
i. 显式回滚:手动显式执行rollback命令;
ii. 隐式回滚:抛出了没有处理的异常,在JDBC编程中主动强行从一个方法中退出(强退!exit等)也会触发隐式回滚;
!!以上的commit以及rollback都是SQL命令,可以直接在SQL命令行输入并执行!
3. 关闭自动提交功能来开启事务——MySQL中全部(一切)都是事务:
1) 其实默认状态下MySQL将每一条输入的SQL命令都当做一个单独的事务来处理,比如你输入了一条insert into(DML)命令,它会立即执行并将修改直接更新到物理存储器上,这是一位MySQL默认将每一条SQL命令都当做一个单独事务来了,并且执行一条命令就自动提交;
2) 那么这样就没有事务功能了,一位MySQL默认将自动提交功能开启了(即每输入一条命令都会被当做一个单独的事务并立即提交!),因此,为了开启事务功能,就必须将自动提交的功能关闭掉!
3) 开关命令:set autocommit = 0 | 1; // 0表示关闭自动提交(即开启事务功能),1表示开启自动提交(即关闭事务功能);
!!一旦开启了事务功能,就可以顺序执行DML语句了,一旦执行到DCL/DDL或者执行了commit就就会提交由之前连续的DML组成的一个事务,而后面的语句将开启一个新的事务!当然也可以用rollback命令来回滚事务;
4) 开启临时事务:
i. 当你在命令行对数据库进行操作时可能不想set autocommit = 0来关掉自动提交,而只是想临时执行一段事务,这种需求是常见的;
ii. 那么可以输入begin或者start transaction命令(以分号结尾)表示开启了一个临时性的事务;
iii. 接下来就一条条执行事务的DML语句就行了;
iv. 遇到commit或者DDL/DCL就会提交本次临时事务,然后本次临时事务结束,重新回到自动提交的状态,如果要想再执行事务那就必须再使用begin或start transaction开启一个临时事务!
v. 如果发生回滚(不管是显式输入rollback命令还是其它原因异常回滚)都以为着背刺临时事务的结束,重新回到自动提交状态,要想再开始执行事务必须再由begin或strat transaction开启!
5) 开启多个命令行对自动提交模式的影响:由于每个SQL命令行窗口都是一个独立的连接session,因此相互之间互不影响,在一个命令行窗口中设置了自动提交模式并不会影响其他正打开的命令行窗口,这是显然的!
4. 中间点:
1) SQL提供了中间点,允许回滚时不必全部回滚,而是回滚到中间点的位置;
2) 设置中间点的语法是:savepoint 自定义中间点的命名;
3) 中间点肯定是在事务的DML语句中间加入的!
4) 考虑到中间点可以设置很多,因此回滚的时候必须指定回滚到哪个中间点上,语法为:rollback to 中间点的名字;
5. JDBC对事务的支持:
1) JDBC对事务的管理交由Connection,都是由Connection的对象方法实现的;
2) 首先关闭自动提交开启事务功能:void Connection.setAutoCommit(boolean autoCommit); // false表示关闭自动提交开启事务功能
3) 当然也可以查看自动提交功能是否开启:boolean Connection.getAutoCommit(); // true表示开启了自动提交
4) 开启事务功能后就是执行事务了,事务就是一条条DML语句,因此就是一条条stmt.executeUpdate语句了(stmt还是照常后去Statement、PreparedStement);
5) 提交任务:
i. 当你执行到第一条DDL/DCL时自动提交(executeUpdate一条DDL/DCL语句);
ii. void Connection.commit(); // 显式提交
6) 回滚:
i. 如果事务执行过程中抛出异常则会自动隐式回滚;
ii. 显式回滚:void rollback();
7) 中间点:
设置中间点
i. Savepoint Connection.setSavepoint(); // 在事务的某个位置设置一个中间点,该中间点没有命名,使用系统默认的命名
ii. Savepoint setSavepoint(String name); // 给中间点命名
iii. 回滚到指定的中间点:void Connection.rollback(Savepoint savepoint); // 回滚到指定的中间点
!!回滚到中间点的API就这么一个,回滚位置是由Savepoint对象指定的,并不是由中间点名称决定的,因此一般命名的setSavepoint方法不怎么用,但是那个命名还是可以使用到的,那就是必须得到数据库的命令行中使用rollback to命令才能访问那个中间点的命名;
8) 示例:事务在执行过程中遇到没有处理的异常将自动回滚
public class Test {
private String driver;
private String url;
private String user;
private String pass;
public void initParam() throws FileNotFoundException, IOException {
Properties props = new Properties();
props.load(new FileInputStream("mysql.ini"));
driver = props.getProperty("driver");
url = props.getProperty("url");
user = props.getProperty("user");
pass = props.getProperty("pass");
}
public void init(String[] sqls) throws FileNotFoundException, IOException, ClassNotFoundException, SQLException {
initParam();
Class.forName(driver);
try (Connection conn = DriverManager.getConnection(url, user, pass)) {
conn.setAutoCommit(false); // 开启事务功能
try (Statement stmt = conn.createStatement()) {
for (String sql: sqls) {
stmt.executeUpdate(sql);
}
}
conn.commit();
}
}
public static void main(String[] args) throws FileNotFoundException, ClassNotFoundException, IOException, SQLException {
String[] sqls = {
"insert into student_table values(null, 'aaa', 1)",
"insert into student_table values(null, 'bbb', 1)",
"insert into student_table values(null, 'ccc', 1)",
"insert into student table values(null, 'ccc', 7717)" // 以为违反外键约束而抛出异常,测试这种异常是否会造成自动回滚
};
new Test().init(sqls);
}
}
6. 批量更新:
1) 就相当于批处理,即一次性执行大量SQL更新(DML),使用批处理机制显然要比一条一条单独执行所有更新语句要来得更快,显然在有批处理需要时采用这种机制是非常那个必要地;
2) JDBC批处理支持:
i. 必须使用Statement;
ii. 先调用Statement的addBatch方法将要执行的一条条SQL更新加入到批处理队列中:void Statement.addBatch( String sql );
!!这里只能使用Statement而不能使用PreparedStatement,因为在获取PreparedStatement时就已经确定了SQL语句,而这里我们需要动态地往批处理队列中加入SQL语句;
iii. 待所有要批处理的语句都插入队列后调用Statement的executeBatch方法将批处理队列一次性送入数据库执行:int[] Statement.executeBatch();
!!由于每条DML语句都会返回此次更新了多少行,因此所有更新批处理完毕会返回一个数组,代表每个DML语句更行了多少行;
3) addBatch不能添加select语句,要求必须是纯DML语句,毕竟select语句不会返回更新行数,标准SQL规定,addBatch如果添加了select语句会直接报错!
4) 为了让批量更新能正确地处理错误,应该将整个批处理包装成一个事务来处理,以便出现意外可以及时地回滚,毕竟批处理的量都比较大,如果出现了问题会导致大量数据的不一致和不完整,后果是不堪设想的,因此一般批处理都要做成事务来玩儿,示例:
boolean autoCommit = conn.getAutoCommit(); // 备份原有的状态
conn.setAutoCommit(false); // 开启事务功能
Statement stmt = conn.createStatement();
// 添加批处理DML
stmt.addBatch(sql1);
stmt.addBatch(sql2);
stmt.addBatch(sql3);
...
stmt.executeBatch(); // 一次性执行
conn.commit(); // 提交生效
conn.setAutoCommit(autoCommit); // 还原状态
5) 如果更新时更改的行数可能会超过Integer.MAX_VALUE就应该调用Statement的executeLargeBatch方法来执行,返回的是long[]数组,但是并不是所有的数据库都支持该方法(该方法的实现是交由数据库厂商的),而MySQL刚好就不支持,因此还是需要使用传统的executeLargeBatch来执行批处理;