1 mysqlTransaction
1.1 Introduction
事务是恢复和并发控制的基本单元。所谓事务一个sql语句操作序列,这些操作要么都执行,要么都不执行,他是一个不可分割的工作单元。
事务需要满足ACID四个特性。
1. A(atomicity) 原子性。一个事务的执行被视为一个不可分割的最小单元。事务里面的操作,要么全部成功执行,要么全部失败回滚,不可以只执行其中的一部分。
2. C(consistency) 一致性。一个事务的执行不应该破坏数据库的完整性约束。如果上述例子中第2个操作执行后系统崩溃,保证A和B的金钱总计是不会变的。
3. I(isolation) 隔离性。通常来说,事务之间的行为不应该互相影响。然而实际情况中,事务相互影响的程度受到隔离级别的影响。文章后面会详述。
4. D(durability) 持久性。事务提交之后,需要将提交的事务持久化到磁盘。即使系统崩溃,提交的数据也不应该丢失。
1.2 mysqlTransaction
在默认情况下,MySQL每执行一条sql语句,都是一个单独的事务。如果需要在一个事务中包含多条sql语句,那么需要开启事务和结束事务。
开启事务: start transaction | |
执行sql语句群 | |
出现异常 事务回滚(撤销)事务结束 rollback | 无异常 事务提交(生效) 事务结束commit |
start transaction:--- 开启事务。以后的sql都在一个事务中。更改的内容不会自动提交。
Rollback:--- 回滚事务,都失败的情况。事务结束,全部失败,数据恢复到事务未开启之前的状态。
Commit:--- 提交事务,都成功的情况。事务结束,全部成功。
1.1.1 放弃事务,更新失败
START TRANSACTION;
UPDATE EXAMPLE SETNAME="hassssh" WHERE id=1;
UPDATE EXAMPLE SETNAME="ksskssk" WHERE id=3;
ROLLBACK;
1.1.2 提交事务,更新成功
START TRANSACTION;
UPDATE EXAMPLE SETNAME="hassssh" WHERE id=1;
UPDATE EXAMPLE SETNAME="ksskssk" WHERE id=3;
COMMIT;
1.1.3 设置回滚点
START TRANSACTION;
UPDATE users SETusername='hhsssh' WHERE id=1;
SAVEPOINT rol01;
UPDATE users SETusername='kkk' WHERE id=2;
ROLLBACK TOSAVEPOINT rol01;
COMMIT;
由于并发操作带来的数据不一致性包括:丢失数据修改、读”脏”数据(脏读)、不可重复读、产生幽灵数据。
(1)丢失数据修改
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所做的更新,这将导致数据丢失。如上例。
再例如,两个编辑人员制作了同一文档的电子复本。每个编辑人员独立地更改其复本,然后保存更改后的复本,这样就覆盖了原始文档。最后保存其更改复本的编辑人员覆盖了第一个编辑人员所做的更改。如果在第一个编辑人员完成之后第二个编辑人员才能进行更改,则可以避免该问题。
(2)读“脏”数据(脏读)
读“脏”数据是指事务T1修改某一数据,并将其写回磁盘,事务T2读取同一数据后,T1由于某种原因被除撤消,而此时T1把已修改过的数据又恢复原值,T2读到的数据与数据库的数据不一致,则T2读到的数据就为“脏”数据,即不正确的数据。
例如:一个编辑人员正在更改电子文档。在更改过程中,另一个编辑人员复制了该文档(该复本包含到目前为止所做的全部更改)并将其分发给预期的用户。此后,第一个编辑人员认为所做的更改是错误的,于是删除了所做的编辑并保存了文档。分发给用户的文档包含不再存在的编辑内容,并且这些编辑内容应认为从未存在过。如果在第一个编辑人员确定最终更改前任何人都不能读取更改的文档,则可以避免该问题。
( 3)不可重复读
指事务T1读取数据后,事务T2执行更新操作,使T1无法读取前一次结果。不可重复读包括三种情况:
事务T1读取某一数据后,T2对其做了修改,当T1再次读该数据后,得到与前一不同的值。
(4)产生幽灵数据(幻读)
按一定条件从数据库中读取了某些记录后,T2删除了其中部分记录,当T1再次按相同条件读取数据时,发现某些记录消失
T1按一定条件从数据库中读取某些数据记录后,T2插入了一些记录,当T1再次按相同条件读取数据时,发现多了一些记录。
事务的四种隔离级别
前文中提到,事务的隔离性受到隔离级别的影响。那么事务的隔离级别是什么呢?事务的隔离级别可以认为是事务的"自私"程度,它定义了事务之间的可见性。隔离级别分为以下几种:
1.READ UNCOMMITTED(未提交读)。在RU的隔离级别下,事务A对数据做的修改,即使没有提交,对于事务B来说也是可见的,这种问题叫脏读。这是隔离程度较低的一种隔离级别,在实际运用中会引起很多问题,因此一般不常用。
2.READ COMMITTED(提交读)。在RC的隔离级别下,不会出现脏读的问题。事务A对数据做的修改,提交之后会对事务B可见,举例,事务B开启时读到数据1,接下来事务A开启,把这个数据改成2,提交,B再次读取这个数据,会读到最新的数据2。在RC的隔离级别下,会出现不可重复读的问题。这个隔离级别是许多数据库的默认隔离级别。
3.REPEATABLE READ(可重复读)。在RR的隔离级别下,不会出现不可重复读的问题。事务A对数据做的修改,提交之后,对于先于事务A开启的事务是不可见的。举例,事务B开启时读到数据1,接下来事务A开启,把这个数据改成2,提交,B再次读取这个数据,仍然只能读到1。在RR的隔离级别下,会出现幻读的问题。幻读的意思是,当某个事务在读取某个范围内的值的时候,另外一个事务在这个范围内插入了新记录,那么之前的事务再次读取这个范围的值,会读取到新插入的数据。Mysql默认的隔离级别是RR,然而mysql的innoDB引擎间隙锁成功解决了幻读的问题。
4.SERIALIZABLE(可串行化)。可串行化是最高的隔离级别。这种隔离级别强制要求所有事物串行执行,在这种隔离级别下,读取的每行数据都加锁,会导致大量的锁征用问题,性能最差。
以上四种隔离级别最高的是Serializable级别,最低的是Read uncommitted级别,当然级别越高,执行效率就越低。像Serializable这样的级别,就是以锁表的方式(类似于Java多线程中的锁)使得其他的线程只能在锁外等待,所以平时选用何种隔离级别应该根据实际情况。在MySQL数据库中默认的隔离级别为Repeatable read (可重复读)。
在MySQL数据库中,支持上面四种隔离级别,默认的为Repeatable read (可重复读);而在Oracle数据库中,只支持Serializable (串行化)级别和Read committed (读已提交)这两种级别,其中默认的为Read committed级别。
2 jdbcTransaction
2.1 sql异常例子
public void sqlException(){
Connection conn=null;
PreparedStatement pstate=null;
try{
conn=JdbcUtils.getConnection();
String sql1="updateaccount set money=money-100 where id=1";
pstate=conn.prepareStatement(sql1);
pstate.executeUpdate();
int a=10/0;
String sql2="updateaccount set money=money+100 where id=2";
pstate=conn.prepareStatement(sql2);
pstate.executeUpdate();
}catch(Exceptione){
e.printStackTrace();
}finally{
}
}
2.2 jdbc开启事务,处理sql并发数据安全
Connection
void | setAutoCommit(boolean autoCommit) |
setSavepoint() |
void | setTransactionIsolation(int level) |
void | rollback() |
void |
void | commit() |
2.2.1 没有设置指定回滚点的jdbcTransaction
public void jdbcStransation(){
Connection conn=null;
PreparedStatement pstate=null;
try{
conn=JdbcUtils.getConnection();
conn.setAutoCommit(false);
String sql1="updateaccount set money=money-100 where id=1";
pstate=conn.prepareStatement(sql1);
pstate.executeUpdate();
int a=10/0;
String sql2="updateaccount set money=money+100 where id=2";
pstate=conn.prepareStatement(sql2);
pstate.executeUpdate();
System.out.println("执行成功");
conn.commit();
}catch(Exceptione){
try {
conn.rollback();
} catch (SQLExceptione1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn,pstate);
}
}
2.2.2 设置指定回滚点的jdbcTransaction
// 需求:向数据库中插入10000条数据,每隔1000条设置一个回滚点。
//9850 制造异常,数据库保存多少条记录
public voidjdbcSetSavepoint(){
Connection conn=null;
PreparedStatement pstate=null;
Savepoint sp=null;
try{
conn=JdbcUtils.getConnection();
conn.setAutoCommit(false);
String sql="insertinto account values (null,?,?)";
pstate=conn.prepareStatement(sql);
for(inti=0;i<1000;i++){
pstate.setString(1,"haha"+i);
pstate.setDouble(2,i);
pstate.executeUpdate();
if(i==980){
inta=10/0;
}
if(i%100==0){
sp=conn.setSavepoint();
}
}
conn.commit();
}catch(Exceptione){
try {
conn.rollback(sp);
conn.commit();
} catch (SQLExceptione1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn,pstate);
}
}