1. 数据库事务的概念:
1) 事务的目的就是为了保证数据库中数据的完整性。
2) 设想一个银行转账的过程,假设分两步,第一步是A的账户-1000,第二步是B的账户+1000。这两个动作必须是连贯的,假设中间断开(出现问题等)比方第一步运行完之后发生异常而终止了操作。那么A就白扣了1000。而B的账户也没有钱添加,这就发生了非常严重的错误;
!!
以上这个案例能够看出:
a. 这两步必须是连贯的,一起合成的。应该作为一个总体逻辑运行单元来运行。
b. 假设两步顺利运行完成那么数据就是完整的。假设中间断开,那么断开时数据就是不完整的(错误的);
!
!以上能够总结出,上面两步要么必须所有都做完。要么就所有都不做,否则就会导致数据错误。!
3) 为解决以上问题,仅仅要将上面两步包装成一个事务即可,那么事务具有哪些特点呢?
i. 首先事务肯定包括了若干数据库操作(肯定都是改动数据库的操作),由于仅仅有改动(update)才有可能导致数据的不完整,而事务就是为了解决不完整问题的;
ii. 事务最大的特点就是要么不做,要做就所有做完,那它是怎样实现这个要求的呢?这个问题从双方面来说:
a. 第一就是要做就所有都做完,这当然是最好的。假设每一步运行都是顺顺利利地没有出现不论什么意外。那就自然所有都做完了,这没什么好说的。
b. 那怎样做到“要么就不做“?你又不能预測到本次运行是否会出现异常!非常多异常都是一些随机因素造成的呀!
c. 非常显然,做还是要做的,关键是做了一半出现了异常应该怎么解决?
d. 事实上原理非常easy,一旦出现了异常,就把刚刚做过的改动撤销了不即可了吗?事实上事务就是这样来实现“要么就不做的“。
e. 这个操作就叫做回滚:事务在运行的时候会直接将更新底层的数据。假设一旦发生了暂时无法解决的异常情况,就会立刻终止事务。并撤销刚刚运行的所有更新,将数据库还原到事务运行之前的状态,这就是事务的回滚了!
4) 事务要生效必须要提交:
i. 即使把事务完整的运行完成了必须要提交才干使对数据的改动真正生效。
ii. 假设不提交就结束事务了,即使前面所有都运行完了也会所有回滚掉。
iii. 提交操作即commit,事务运行完成后一定要记得commit使之真正生效哦!
!!实际上在提交之前对数据的改动都仅仅是在内存(缓存)映像中进行的,之前做完改动后使用select查询会发现结果中数据确实被更新了,但那仅仅是假象,由于被更新的仅仅是内存中的映像,假设你未提交就断开又一次连接,再进入后select下面结果发现并未改动。
!!仅仅有提交commit后才会把对内存映像做出的改动永久地写入物理存储设备中!!
!
。commit后断开再重连,select一下就会发现数据真正被改动了;
2. 事务的理论级概念:
1) 从上面的样例能够看出事务具有下面4个特性(合成ACID):
i. 原子性(Atomicity):事务是最小的运行单位,不可切割,必须一次作为一个总体运行完;
!这是显然的,上述转账的样例。切割运行了必定导致切割点位置出现数据的不一致性。
!!事实上上述银行转账是特例。事务你能够任意定义,不会导致不一致性的两个操作也能够组成一个事务,仅仅只是运行的时候会依照事务的性质进行。
ii. 一致性(Consistency):事务假设中间被割裂可能会导致数据的不一致性,因此事务终于的目的就是为了保证数据的完整性和一致性。而这个性质是由原子性来保障的;
iii. 隔离性(Isolation):并发事务之间不能相互影响(并发事务竞争的数据必定被同不监视!)。原因非常easy。那就是原子性!
并发事务之间不能看到对方的中间状态!
!
!
可见原子性是事务的根本属性,其他特性都是由原子性保证的;
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来运行批处理;