事务
事务指的是逻辑上的一组操作,组成这组操作的各个单元要么全都成功,要么全都失败。
一组SQL,使用同一个连接对象执行的(SQL)。要么全都成功,要么全都失败。
/*
实现数据转账
tom账户-1000
jerry账户+1000
*/
update account set money = money-1000 where name = 'tom';
update account set money = money+1000 where name = 'jerry';
#如果第一个成功第二个失败了,那么就会发现钱丢了,反过来也一样,钱变多了。所以这两个sql应该放在同一个事务里,要么都成功,要么都失败。
复制代码
其实数据库中是有默认事务的,他是一条一条sql语句执行的,会自动提交,但是想要实现我们自己的需求,需要我们自己开启事务,控制事务。
MySQL中的事务操作
start transaction或者begin 开启事务
commit 提交事务(不能再改变了)
rollback 回滚(执行过程中发现有问题,撤回原来的状态,全部操作宣布失败)
JDBC的Connection事务操作
setAutoCommit(false)开启事务
void commit() 提交
void rollback() 回滚
示例:
无事务的转账操作:tom转账1000到jerry账户。
public class TestTranscaction {
public static void main(String[] args) {
QueryRunner qr = new QueryRunner();
Connection con = null;
try {
con = DruidUtils.getConnection();
String sql1 = "update account set money = money - ? where name = ?";
String sql2 = "update account set money = money + ? where name = ?";
int i = qr.update(con,sql1,1000,"tom");
int j = qr.update(con,sql2,1000,"jerry");
System.out.println(i);
System.out.println(j);
} catch (Exception e) {
e.printStackTrace();
}finally {
DruidUtils.release(null,null,con);
}
}
}
复制代码
为转账案例添加事务
1 关闭事务自动提交 con.setAutoCommit(false);
2 事务提交方法 con.commit();
3 事务回滚方法 con.rollback();
public class TestTranscaction {
public static void main(String[] args) {
QueryRunner qr = new QueryRunner();
Connection con = null;
try {
con = DruidUtils.getConnection();
con.setAutoCommit(false);
String sql1 = "update account set money = money - ? where name = ?";
String sql2 = "update account set money = money + ? where name = ?";
int i = qr.update(con,sql1,1000,"tom");
int j = qr.update(con,sql2,1000,"jerry");
con.commit();
System.out.println(i);
System.out.println(j);
} catch (Exception e) {
e.printStackTrace();
try {
con.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}finally {
DruidUtils.release(null,null,con);
}
}
}
复制代码
我们可以认为制造一个异常如计算1/0在两个操作之间,可以发现数据库并没有被改变,而是进入catch块进行事务的回滚
只要不遇到commit操作,事务就不会提交,那么为啥要执行rollback?
我们在开启事务时,会设置setAutoCommit(false),它实际上是给Connection对象上了一个锁,只有这个连接能够修改数据,所以commit和rollback都有释放锁的操作,所以事务失败一定要回滚,不然会占用连接池中的连接,效率降低。
如果停电怎么办?
事务具有超时回滚操作,来电后,事务会执行自己的超时操作,自动回滚。
DbUtils释放资源
在DbUtils工具类中有CommitAndCloseQuietly(Connection con)能够执行失败回滚以及提交操作,并且除此之外还帮我们捕获了commit方法的异常。DbUtils.commitAndCloseQuietly(con);
/**
* 为转账案例添加事务:使用QueryRunner
*
* 如何使用事务控制程序: 事务的关键对象:Connection
* Connection连接对象:
* 方法 void setAutoCommit(boolean autoCommit)
* 设置事务的提交模式,参数写false,开启事务,阻止事务自动提交,必须手控
*
* 方法 void commit() 提交事务,事务一旦提交,数据持久化,不可改变
*
* 方法 void rollback() 回滚事务,数据退回,和没有开启事务时候的数据一致
*
* 开发数据表:遇到同时执行多个SQL,是insert update delete 必须跟随事务
*
* QueryRunner : 执行SQL语句的对象,构造方法有2中
* new QueryRunner() 无参数
* new QueryRunner(DataSource ds)有参数,传递连接池对象
* 传递连接池对象,自己获取连接对象,连接对象的控制权,不在我们手里
* 事务控制,也就没有了
* 执行SQL语句的方法: update(传递连接对象)
*
* 本案例异常不能 throws 抛出异常
* 执行SQL语句,两次执行,SQL语句执行成功,没有异常
* SQL语句执行失败,抛出异常,catch抓
*
* DButils : 事务的操作
*/
public class TestAccount {
public static void main(String[] args) {
//创建对象QueryRunner,无参数构造器
QueryRunner qr = new QueryRunner();
Connection con = null;
try {
//获取连接对象
con = DruidUtils.getConnection();
//开启事务,阻止事务自动提交
con.setAutoCommit(false);
//拼写SQL语句,tom账户-1000
String sql1 = "update account set money = money - ? where name = ?";
//拼写SQL语句,jerry账户+1000
String sql2 = "update account set money = money + ? where name = ?";
//执行SQL语句,tom账户-1000
int i = qr.update(con, sql1, 1000, "tom");
//执行SQL语句,jerry账户+1000
System.out.println(1/0);
int j = qr.update(con, sql2, 1000, "jerry");
System.out.println("i = " + i);
System.out.println("j = " + j);
//SQL语句执行成功,提交事务,事务结束,数据持久存储
//con.commit();
//Apache的工具类:Dbutils
DbUtils.commitAndCloseQuietly(con);
}catch (Exception ex){
ex.printStackTrace();
//出现异常,SQL语句执行失败,回滚事务
/*try {
con.rollback();
}catch (SQLException e){
e.printStackTrace();
}*/
DbUtils.rollbackAndCloseQuietly(con);
}finally {
//释放资源
//DruidUtils.release(null,null,con);
}
}
}
复制代码
经典三层架构
分层中的框架技术
表现层:Struts、SpringMVC
业务层:事实上没有统一的框架,因为每个系统的业务方向千差万别。为了和表现层,业务层统一,所以赋予了它Spring,这个框架事实上的作用是统筹和集成表现层和业务层的框架的。
持久层:Hibernate、MyBatis、Spring Data Jpa(新)
分层的转账案例
分析
实现
pojo类:
@Data
public class Account {
private Integer id;
private String name;
private Double money;
}
复制代码
dao层(对数据库操作):
工具类提取重复代码(此处使用的是Druid数据库连接池)
/**
* 实现自定义的德鲁伊连接池工具类: 取出连接
* 德鲁伊连接池的配置文件,采用properties,文件名随意
*
* 我们要自己读取配置
*
* 德鲁伊连接池的接口实现类:
* DataSource接口实现类:DruidDataSource
*
* 德鲁伊连接池,提供了工具类,工厂类 (创建对象)
* 使用工厂类,获取 DruidDataSource对象
* DruidDataSourceFactory工厂类,静态方法 createDataSource(Properties p) 返回值就是接口实现类对象DruidDataSource
*
* 自己读取配置,键值对,存储在集合Properties中, 集合对象传递到createDataSource方法中
*/
public class DruidUtils {
private static DataSource dataSource;
static {
try {
//类的加载器中的字节输入流,读取配置文件
InputStream in =
DruidUtils.class.getClassLoader().getResourceAsStream("druid.properties");
Properties prop = new Properties();
//集合中,存储键值对
prop.load(in);
in.close();
//工厂类的静态方法,创建DataSource接口实现类对象
dataSource = DruidDataSourceFactory.createDataSource(prop);
}catch (Exception ex){
ex.printStackTrace();
}
}
/**
* 定义方法,返回DataSource接口实现类
*/
public static DataSource getDataSource(){
return dataSource;
}
/**
* 定义静态方法,返回数据库连接对象
*/
public static Connection getConnection() throws SQLException {
return dataSource.getConnection();
}
/**
* 静态方法:释放资源
* 释放连接对象,结果集对象,SQL语句执行对象
*/
public static void release(ResultSet rs, Statement stat, Connection con){
if (rs!=null)
try {
rs.close();
}catch (SQLException ex){}
if(stat!=null)
try {
stat.close();
}catch (SQLException ex){}
if(con!=null)
try {
con.close();
}catch (SQLException ex){}
}
}
复制代码
调用工具类连接数据库,对数据库进行操作
/**
* 操作account数据表的类
* 业务层调用,结果返回业务层
*/
public class AccountDao {
private QueryRunner qr = new QueryRunner();
/**
* 更新数据表,接收业务层的参数:Account对象
*/
public int updateAccount(Connection con,Account account) throws SQLException {
String sql = "update account set money = ? where name = ?";
//执行SQL语句
return qr.update(con,sql,account.getMoney(),account.getName());
}
/**
*
* @param con 数据库连接对象
* @param name 账户名
* @return 查询到的账户数据
* @throws SQLException
*/
public Account queryAccountByName(Connection con,String name) throws SQLException {
//查询账户的SQL
String sql = "select * from account where name = ?";
//执行查询语句,结果集BeanHandler
return qr.query(con,sql,new BeanHandler<Account>(Account.class),name);
}
}
复制代码
Service层(业务):
/**
* 实现转账案例的业务逻辑
* 接收表现层的参数: 付款人,收款人,金额
* 调用持久层,传递相关参数,完成转账逻辑
*/
public class AccountService {
private AccountDao accountDao = new AccountDao();
/**
*
* @param toName 付款人
* @param inName 收款人
* @param money 转账金额
*/
public void transfer(String toName,String inName,double money){
Connection con = null;
try {
//获取连接对象
con = DruidUtils.getConnection();
//开启事务
con.setAutoCommit(false);
//根据付款人的名字,查询付款人的账户信息
Account toAccount = accountDao.queryAccountByName(con, toName);
//根据收款人的名字,查询收款人的账户信息
Account inAccount = accountDao.queryAccountByName(con, inName);
//转账逻辑计算,toAccount对象的余额-参数money
toAccount.setMoney( toAccount.getMoney() - money );
//转账逻辑计算,inAccount对象的余额+参数money
inAccount.setMoney( inAccount.getMoney() + money );
//调用持久层的方法,更新数据,传递参数 Account对象
int i = accountDao.updateAccount(con, toAccount);
int a = 1 / 0;
int j = accountDao.updateAccount(con, inAccount);
if( i > 0 && j > 0)
System.out.println("转账成功");
//所有的SQL语句已经执行完毕,没有出现异常,提交事务
DbUtils.commitAndCloseQuietly(con);
}catch (Exception ex){
ex.printStackTrace();
//出现异常,事务回滚
DbUtils.rollbackAndCloseQuietly(con);
}
}
}
复制代码
(Web层)
//javaweb之后再讲
复制代码
事务的特点(ACID)
A:原子性,是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
C:一致性,事务前后数据的完整性必须保持一致。
I:隔离性,事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其他的事物所干扰,多个并发事务之间数据要相互隔离,正常情况下数据库是做不到这一点的,可以手动设置隔离级别,但是效率会特别低。
D:持久性,持久性是指一个事务一旦被提交,他对数据库的改变就是永久的,接下来即使是数据库发生古筝也不应该对其有任何影响。
并发访问问题
如果不考虑隔离性,事务存在三种并发访问问题。
1 脏读:一个事务读取到了另一个事务未提交的数据。
2 不可重复读:一个事务读到了另一个事务已经提交(update)的数据。引发另一个事务,在事务中的多次查询结果不一致。
3 幻读/虚读:一个事务读到了另一个事务已经提交(insert)的数据,导致另一个事务,在事务中多次查询结果不一致。
事务的隔离级别——用来解决并发访问问题
数据库规定了4种隔离级别,分别用于描述两个事务并发的所有情况。
1 read uncommitted:读未提交
存在问题:3个问题(脏读、不可重复读、幻读)
解决问题:无
2 read committed:读已提交
存在问题:2个问题(不可重复读、幻读)
解决问题:1个问题(脏读)
3 repeatable read:可重复读,在一个事务种读到的数据始终保持一致,无论另一个事务是否提交。
存在问题:1个问题(幻读)
解决问题:2个问题(脏读、不可重复读)
4 serializable:串行化,同时只能执行一个事务(相当于单线程)
存在问题:无
解决问题:3个问题(脏读、不可重复读、幻读)
隔离级别 和 并发访问问题之间的关系 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted(读未提交) | |||
read committed(读已提交) | √ | ||
repeatable read(可重复读) | √ | √ | |
serializable(串行化) | √ | √ | √ |
安全性和性能对比:
安全性:serializable > repeatable read > read committed > read uncommitted
性能:serializable < repeatable read < read committed < read uncommitted
常见数据库的默认隔离级别:
MySQL:repeatable read(可重复读)
Oracle:read committed(读已提交)
Connection接口中设置事务的隔离性
注意:实际开发中一般采用默认的隔离级别,由项目经理负责。
/**
* Java程序,控制事务的隔离级别
* 采用默认的隔离级别
*
* 方法参数int类型: 1,2,4,8 数字越低,隔离级别越低
* Connection接口中,定义了静态成员变量
*/
public class A {
public static void main(String[] args) throws SQLException {
//连接对象接口 Connection
//指针对这次连接有效
Connection con = null;
con.setTransactionIsolation(Connection.TRANSACTION_SERIALIZABLE); //设置当前连接对象的事务隔离级别,方法的参数是int类型
}
}
复制代码
这个方法是Connection接口中的
void setTransactionIsolation(int level)throws SQLException
复制代码
这个形参level的值应该取下列之一
Connection.TRANSACTION_READ_UNCOMMITTED(读未提交)
Connection.TRANSACTION_READ_COMMITTED(读已提交)
Connection.TRANSACTION_REPEATABLE_READ(可重复读)
Connection.TRANSACTION_SERIALIZABLE(串行化)
可以显而易见的是,这四个都是Connection接口中规定的成员变量值(static int),因为我们引用时使用的是 “接口名.成员变量名”。
在Connection接口中还规定了一个静态int变量TRANSACTION_NONE 它表示一个不支持事务的常量,这个值并不能用来指定连接的事务隔离级别。
//connection接口中成员变量的定义的值
int TRANSACTION_NONE = 0;
int TRANSACTION_READ_UNCOMMITTED = 1;
int TRANSACTION_READ_COMMITTED = 2;
int TRANSACTION_REPEATABLE_READ = 4;
int TRANSACTION_SERIALIZABLE = 8;
复制代码