【基础系列二十】--事务
事务
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。
例如:A——B转帐,对应于如下两条sql语句
update account set money=money-100 where name=‘a’;
update account set money=money+100 where name=‘b’;
在这个例子中,我们要保证这两条sql要么一起成功,要么一起失败,不允许一部分成功一部分失败,这就要靠数据库的事务来实现了。
管理事务
数据库默认的事务
数据库(MySQL)默认支持事务的,但是数据库默认的事务是一条sql语句独占一个事务,这种模式意义不大。
手动控制事务
start transaction;
开启事务,在这条语句之后的所有的sql将处在同一事务中,要么同时完成要么同时不完成。
事务中的sql在执行时,并没有真正修改数据库中的数据。
commit;
提交事务,将整个事务对数据库的影响一起发生。
rollback;
回滚事务,将这个事务对数据库的影响取消掉。
SQL代码示例:
create database tdb;
use tdb;
create table account (
id int primary key auto_increment,
name varchar(255),
money double
);
insert into account values (null,'a',1000.0);
insert into account values (null,'b',1000.0);
update account set money = money-100 where name='a';
update account set money = money+100 where name='b';
select * from account;
开启事务
start transaction;--开启事务
update account set money = money-100 where name='a';
update account set money = money+100 where name='b';
commit;
start transaction;
update account set money = money-100 where name='a';
update account set money = money+100 where name='b';
rollback;
JDBC中控制事务
JDBC获取一个连接时,默认是自动提交事务的。
我们可以选择关闭JDBC自动提交事务
conn.setAutoCommit(false);
关闭自动连接后,conn将不会帮我们提交事务,在这个连接上执行的所有sql语句将处在同一事务中,需要我们是手动的进行提交或回滚
提交事务
conn.commit();
回滚事务
conn.rollback();
也可以设置回滚点回滚部分事务。
avePoint sp = conn.setSavePoint();
conn.rollback(sp);
注意:回到回滚点后,回滚点之前的代码虽然没被回滚但是也没提交呢,如果想起作用还要做commit操作。
代码示例:
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Savepoint sp = null;
try{
Class.forName("com.mysql.jdbc.Driver");
conn = DriverManager.getConnection("jdbc:mysql:///tdb", "root", "root");
//开启事务
conn.setAutoCommit(false);
ps = conn.prepareStatement("update account set money = money - ? where name = ?");
ps.setDouble(1, 100.0);
ps.setString(2, "a");
ps.executeUpdate();
ps = conn.prepareStatement("update account set money = money + ? where name = ?");
ps.setDouble(1, 100.0);
ps.setString(2, "b");
ps.executeUpdate();
//设置回滚点
sp = conn.setSavepoint();
ps = conn.prepareStatement("update account set money = money - ? where name = ?");
ps.setDouble(1, 100.0);
ps.setString(2, "a");
ps.executeUpdate();
int i = 1/0;
ps = conn.prepareStatement("update account set money = money + ? where name = ?");
ps.setDouble(1, 100.0);
ps.setString(2, "b");
ps.executeUpdate();
//提交事务
conn.commit();
}catch (Exception e) {
//回滚事务
if(conn != null){
try {
if(sp!=null){//走过了回滚点
conn.rollback(sp);
conn.commit();
}else{//没有到回滚点
conn.rollback();
}
} catch (SQLException e1) {
e1.printStackTrace();
}
}
e.printStackTrace();
}finally{
if(rs != null){
try {
rs.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
rs = null;
}
}
if(ps != null){
try {
ps.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
ps = null;
}
}
if(conn != null){
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
} finally {
conn = null;
}
}
}
}
事务的四大特性
原子性(Atomicity):原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
一致性(Consistency):事务前后数据的完整性必须保持一致。
隔离性(Isolation):事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
持久性(Durability):持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
隔离性
数据库的其他三大特性数据库可以帮我们保证,而隔离性需要我们讨论。
数据库隔离性分析
隔离性问题本质上就是多线程并发安全性问题。
可以用锁来解决多线成并发安全问题,但是如果用了锁,必然会造成程序的性能大大的下降.对于数据库这种高并发要求的程序来说这是不可接受的.
我们可以具体分析下隔离性产生的细节:
如果两个线程并发修改,必然产生多线程并发安全问题,必须隔离开
如果两个线程并发查询,必然没有问题,不需要隔离
如果一个线程修改,一个线程查询,在不同的应用场景下有可能有问题,有可能没问题。
隔离性可能造成的问题
脏读
打开两个mysql客户端,都执行以下语句。
set session transaction isolation level read uncommitted;
一个事务读取到另一个事务未提交的数据:a买鞋,b卖鞋
----------------------------
a 1000
b 1000
----------------------------
客户端a:
start transaction;
update account set money = money-100 where name='a';
update account set money = money+100 where name='b';
-----------------------------
客户端b:
start transaction;
select * from account;
a 900
b 1100
commit;
-----------------------------
客户端a:
rollback;
-----------------------------
客户端b:
start transaction;
select * from account;
a 1000
b 1000
commit;
-----------------------------
不可重复读
一个事务多次读取数据库中的同一条记录,多次查询的结果不同(一个事务读取到另一个事务已经提交的数据) a:银行账户,W:银行工作人员(领导让W统计a的账户情况)
------------------------------
活期 定期 固定资产
a 1000 1000 1000
------------------------------
W:
start transaction;
select 活期 from account where name='a'; -- 活期存款:1000W元
select 定期 from account where name = 'a'; -- 定期存款:1000W元
select 固定 from account where name = 'a'; -- 固定资产:1000W元
---------------------------
a:
start transaction;
update account set 活期=活期-1000 where name= 'a';
commit;
---------------------------
select 活期+定期+固定 from account where name='a'; ---总资产:2000W元
虚读(幻读)
有可能出现,有可能不出现,概率非常低:一个事务多次查询整表数据,多次查询时,由于有其他事务增删数据, 造成的查询结果不同(一个事务读取到另一个事务已经提交的数据)
------------------------------
a 1000
b 2000
------------------------------
工作人员d:
start transaction;
select sum(money) from account; --- 总存款3000元
select count(*) from account; --- 总账户数2个
-----------------
c:
start transaction;
insert into account values (null,'c',3000);
commit;
-----------------
select avg(mone) from account; --- 平均每个账户:2000元
数据库的四大隔离级别
数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,则安全性越差。
到底该防止哪些问题应该由数据库使用者根据具体的业务场景来决定,所以数据库的设计者并没有把放置哪类问题写死,而是提供了如下选项:
read uncommitted; 不做任何隔离,可能造成脏读、不可重复度、虚读(幻读)问题
read committed; 可以防止脏读,但是不能防止不可重复度、虚读(幻读)问题
repeatable Read; 可以防止脏读、不可重复度,但是不能防止虚读(幻读)问题
serializable; 可以防止所有隔离性的问题,但是数据库就被设计为了串行化的数据库,性能很低
从安全性上考虑: Serializable Repeatable Read Read Committed Read uncommitted 从性能上考虑: Read uncommitted Read committed Repeatable Read Serializable
我们作为数据库的使用者,综合考虑安全性和性能,从四大隔离级别中选择一个在可以防止想要防止的问题的隔离级别中性能最高的一个。
其中Serializable性能太低用的不多,Read uncommitted安全性太低用的也不多,我们通常从Repeatable Read和Read committed中选择一个。
如果需要防止不可重复读选择Repeatable Read,如果不需要防止选择Read committed
mysql数据库默认的隔离级别就是Repeatable Read
Oracle数据库默认的隔离级别是Read committed
操作数据库的隔离级别
查询数据库的隔离级别:
select @@tx_isolation;
修改数据库的隔离级别:
set [session/global] transaction isolation level xxxxxx;
不写默认就是session,修改的是当前客户端和服务器交互时是使用的隔离级别,并不会影响其他客户端的隔离级别
如果写成global,修改的是数据库默认的隔离级别(即新开客户端时,默认的隔离级别),并不会修改当前客户端和已经开启的客户端的隔离级别
set global transaction isolation level serializable;
数据库中的锁
共享锁和共享锁可以共存,共享锁和排他锁不能共存.在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁。
案例演示1:打开两个mysql客户端,将隔离级别都设置为Serializable级别,
set session transaction isolation level Serializable;--设置后查询加了共享锁
分别在两个客户端中查询:
start transaction;
select * from account;--都能查询出数据,说明共享锁可以共存。
排他锁
排他锁和共享锁不能共存,排他锁和排他锁也不能共存,在任何隔离级别下做增删改都加排他锁。
在案例1基础上,在其中一个客户端执行修改操作,将一个客户端的共享锁升级为排他锁:
两个客户端都执行:
start transaction;
select * from account;
一个客户端执行:
update account set money = 900;-- #发现执行在等待,当另外一个客户端提交commit或者回滚rollback之后,修改才能成功。
另外一个客户端执行:
rollback/commit;
死锁
mysql可以自动检测到死锁,错误退出一方并执行另一方。
在案例1基础上:
两个客户端都执行:
start transaction;
select * from account;
一个客户端执行:
update account set money = 900;
另外一个客户端执行:
update account set money = 800;
发现彼此等待,直到一方报错结束,死锁才结束。