一、事务的概念
事务指的是逻辑上的一组操作,组成这组操作的各个单元,要么不全部成功,要么不全部不成功。
例如:A--B转账,对应于如下两条SQL语句:
Update from account set money = money + 100 where name='B';
Update from account set money = money - 100 where name='A';
二、MySQL数据库中操作事务命令
SQL脚本:
/*创建账户表*/
use jdbcstudy;
create table account(
id int primary key auto_increment,
name varchar(40),
money float
);
/*插入测试数据*/
insert into account(name,money) values('Amy',100);
insert into account(name,money) values('Belly',100);
insert into account(name,money) values('Carry',100);
2.1、开启事务(Start transaction)
使用“start transacion”开启MySQL数据库的事务,如下图:
在数据库中模拟转账失败的场景,首先执行Update语句让Amy的money减少10块钱,如下图:
然后关闭命令窗口,这要就导致了刚刚执行的Update语句的数据库的事务没有被提交,那么我们对A用户的修改就不算是是真正的修改了,下次在查询Amy的money时,依然还是之前的100,如下图:
2.2、提交事务(commit)
模拟A--B转账成功的场景:
2.3、回滚事务(rollback)
通过手动回滚事务,让所有的操作都失效,这样数据就会回到最初的初始状态!
三、JDBC中使用事务
当JDBC程序向数据库获取一个Connection对象,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列的JDBC控制事务语句:
▶Connection.setAutoCommit(false); // 开启事务(Start Transaction)
▶Connection.rollback(); // 回滚事务(rollback)
▶Connection.commit(); // 提交事务(commit)
3.1、JDBC使用事务范例
在JDBC代码中演示银行转账案例,使用如下转账操作在同一事务中执行:
"update account set money=money-100 where name='A' "
"update account set money=money+100 where name='B' "
代码如下:
package me.zl.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import me.zl.utils.JdbcUtils;
/**
* ClassName:TransactionDemo1
* Description:jdbc中使用事务来模拟转账
*/
public class TransactionDemo1 {
/**
* @Method:testTransaction1
* @Description:模拟转账成功时的业务场景
*/
public static void testTransaction1() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);//通知数据库开启事务(Start Transaction)
String sql1 = "update account set money=money-20 where name='Amy'";
st = conn.prepareStatement(sql1);
st.executeUpdate();
String sql2 = "update account set money=money+20 where name='Belly'";
st = conn.prepareStatement(sql2);
st.executeUpdate();
conn.commit();//上面两条SQL执行Update语句成功之后就通知数据库提交事务(commit)
System.out.println("成功!!!");
} catch (SQLException e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
/**
* @Method:testTransaction2
* @Description:模拟转账过程中出现异常导致有一部分SQL执行失败后让数据库自动回滚事务
*/
public static void testTransaction2() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);//通知数据库开启事务(Start Transaction)
String sql1 = "update account set money=money-13 where name='Amy'";
st = conn.prepareStatement(sql1);
st.executeUpdate();
//用这行代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法执行,事务也无法正常提交,此时数据库会自动执行回滚操作
int x = 1/0;
String sql2 = "update account set money=money+13 where name='Belly'";
st = conn.prepareStatement(sql2);
st.executeUpdate();
conn.commit();//上面的两条SQL执行Update语句成功之后就通知数据库提交事务(commit)
System.out.println("成功!!!");
} catch (SQLException e) {
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
/**
* @Method:testTransaction3
* @Description:模拟转账过程中出现异常导致有一部分SQL执行失败时手动通知数据库回滚事务
*/
public static void testTransaction3() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);//通知数据库开启事务
String sql1 = "update account set money=money-12 where name='Belly'";
st = conn.prepareStatement(sql1);
st.executeUpdate();
//用这句代码模拟执行完SQL1之后程序出现了异常而导致后面的SQL无法正常执行,事务也无法正常提交
int x = 1/0;
String sql2 = "update account set money=money+12 where name='Amy'";
st = conn.prepareStatement(sql2);
st.executeUpdate();
conn.commit();//通知数据库提交事务
System.out.println("成功!!!");
} catch (SQLException e) {
try {
conn.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
public static void main(String[] args) {
// testTransaction1();
// testTransaction2();
// testTransaction3();
}
}
3.2、设置事务回滚点
在开发中,有时候可能需要手动设置事务的回滚点,在JDBC中使用如下的语句设置事务回滚点
Savapoint sp = conn.setSavepoint();
conn.rollback(sp);
conn.commit(); //回滚后必须通知数据库提交事务
设置事务回滚点范例:
package me.zl.demo;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Savepoint;
import me.zl.utils.JdbcUtils;
public class TransactionDemo2 {
public static void testTransaction1() {
Connection conn = null;
PreparedStatement st = null;
ResultSet rs = null;
Savepoint sp = null;
try {
conn = JdbcUtils.getConnection();
conn.setAutoCommit(false);
String sql1 = "update account set money=money-15 where name='Carry'";
st = conn.prepareStatement(sql1);
st.executeUpdate();
//设置事务的回滚点
sp = conn.setSavepoint();
String sql2 = "update account set money=money+15 where name='Amy'";
st = conn.prepareStatement(sql2);
st.executeUpdate();
//程序执行到这里出现异常,后面的sql3语句
// int x = 1/0;
String sql3 = "update account set money=money-10 where name='Carry'";
st = conn.prepareStatement(sql3);
st.executeUpdate();
conn.commit();
} catch (SQLException e) {
try {
/*
* 上面向数据库发送了3条Update语句,
* sql3语句由于程序出现异常导致无法正常执行,数据库事务而已无法正常提交
* 由于设置的事务回滚点是在sql1语句正常执行完成之后,sql2语句正常执行之前
* 那么通知数据库回滚事务时,不会回滚sql1执行的update操作,只会回滚到sql2执行的update操作
* 也就是说,上面三条update语句中,sql1这条语句的修改操作起作用了
* sql2的修改操作由于事务回滚没有起作用,sql3由于程序异常没用机会执行
*/
conn.rollback(sp);//回滚到设置的事务回滚点
conn.commit();//通知数据库提交事务
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}finally{
JdbcUtils.release(conn, st, rs);
}
}
public static void main(String []args) {
testTransaction1();
}
}
四、事务的四大特性(ACID)
4.1、原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么全部成功,要么全部失败。比如在同一个事务中的SQL语句,要么全部执行成功,要么全部执行失败。
4.2、一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。以转账为例子,A向B转账,假设转账之前这两个用户的钱加起来总共是2000,那么A向B转账之后,不管这两个账户怎么转,A用户的钱和B用户的钱加起来的总额还是2000,这个就是事务的一致性。
4.3、隔离性(Isolation)
事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。
4.4、持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响
事务的四大特性中最麻烦的是隔离性,下面重点介绍一下事务的隔离级别
五、事务的隔离级别
多个线程开启各自事务操作数据库中数据时 ,数据库系统要负责隔离操作,以保证各个线程在获取数据时的准确性。
5.1事务不考虑隔离性可能引发的问题
▶ 脏读
指一个事务读取了另外一个事务未提交的数据。
▶不可重复读
指在一个事务内部读取表中的某一行数据,多次读取结果不同。、
▶虚读
指在一个事务内读取到了别的事务插入的数据,导致前后读取的不一致。
脏读和不可重复读的区别是:脏读是读取前一事务未提交的脏数据,不可重复读是重读了前一事务已提交的事务。
5.2、事务隔离性的设置语句
mysql数据库共定义了四种隔离级别:
▶Serializable(可串行化):可避免脏读、不可重复读、虚读。
▶Repeatable read(可重复读):可避免脏读、不可重复读。
▶Read committed(读已提交):可避免脏读。
▶Read uncommitted(读未提交):最低级别,以上情况无法保证。
mysql数据库查询当前当前事务隔离级别:select @@tx_isolation;
mysql数据库默认的事务隔离级别是:Repeatable read(可重复读)
mysql数据库设置事务的隔离级别:set Transaction isolation level 隔离级别名;
5.3、使用MySQL数据库演示不同隔离级别下的并发问题
同时打开两个窗口模拟2个用户并发访问数据库
▶当把事务的隔离级别设置为read uncommitted时,会引发脏读、不可重复读和虚读
A窗口
set transaction isolation level read uncommitted;--设置A用户的数据库隔离级别为Read uncommitted(读未提交)
start transaction; --开启事务
select * from account; --查询A账户中现有的钱,转到B窗口进行操作
select * from account ; --发现a多了100元,这时候A读到了B未提交的数据(脏读)
B窗口
start transaction; --开启事务
update account set money=money+100 where name='A'; --不要提交,转到A窗口查询
▶当把事务的隔离级别设置为read uncommitted时,会引发脏读、不可重复读,避免了虚读
A窗口
set transaction isolation level read committed;
start transaction;
select * from account;--发现a帐户是1000元,转到b窗口
select * from account;--发现a帐户多了100,这时候,a读到了别的事务提交的数据,两次读取a帐户读到的是不同的结果(不可重复读)
B窗口
start transaction;
update account set money=money+100 where name='aaa';
commit;--转到a窗口
▶当把事务的隔离级别设置为repeatable read(mysql默认)时,会引发虚读,避免了虚读和不可重复读
A窗口
set transaction isolation level repeatable read;
start transaction;
select * from account;--发现表有4个记录,转到b窗口
select * from account;--可能发现表有5条记录,这时候发生了a读取到另外一个事务插入的数据(虚读)
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);
commit;--转到a窗口
▶当把事务的隔离级别设置为Serializable时,会避免所有问题
A窗口
set transaction isolation level Serializable;
start transaction;
select * from account;--转到b窗口
B窗口
start transaction;
insert into account(name,money) values('ggg',1000);--发现不能插入,只能等待a结束事务才能插入