事务
事务的概念
事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。
例如:A——B转帐,对应于如下两条sql语句
update account set money=money-100 where name='a';
update account set money=money+100 where name='b';
在这个例子中,我们要保证这两条sql要么一起成功,要么一起失败,不允许一部分成功一部分失败,这就要靠数据库的事务来实现了。
管理事务
数据库默认的事务
数据库默认支持事务的,但是数据库默认的事务是一条sql语句独占一个事务,这种模式意义不大。
注意:MyISAM存储引擎是不支持事务处理的;可以通过SHOW ENGINES
查看
查看表使用的存储引擎
show table status from db_name where name=‘table_name’;
修改表的存储引擎
alter table table_name engine=innodb;
手动控制事务
sql控制事务
start transaction
:开启事务,在这条语句之后的所有的sql将处在同一事务中,要么同时完成要么同时不完成;事务中的sql在执行时,并没有真正修改数据库中的数据。commit
:提交事务,将整个事务对数据库的影响一起发生。CREATE TABLE account(id INT, name VARCHAR(20), money DOUBLE); INSERT INTO account(id,name,money) VALUES(0,'lili',200); INSERT INTO account(id,name,money) VALUES(1,'Tom',300); INSERT INTO account(id,name,money) VALUES(2,'Potter',400); SELECT * FROM account; START TRANSACTION; UPDATE account SET money=money+10 WHERE name = 'lili'; UPDATE account SET money=money-10 WHERE name = 'Potter'; COMMIT; SELECT * FROM account;
rollback
:回滚事务,将这个事务对数据库的影响取消掉。START TRANSACTION; UPDATE account SET money=money+10 WHERE name = 'lili'; UPDATE account SET money=money-10 WHERE name = 'Potter'; ROLLBACK;
JDBC中控制事务
当Jdbc程序向数据库获得一个Connection对象时,默认情况下这个Connection对象会自动向数据库提交在它上面发送的SQL语句。
若想关闭这种默认提交方式,让多条SQL在一个事务中执行,可使用下列语句:
conn.setAutoCommit(false)
: 关闭自动提交关闭后,conn将不会帮我们提交事务,在这个连接上执行的所有sql语句将处在同一事务中,需要我们是手动的进行提交或回滚
conn.commit()
:提交事务conn.rollback()
:回滚事务
public class TransJDBC {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
try {
conn = JDBCUtils.getConn();
// 开启事务
conn.setAutoCommit(false);
String sql1 = "UPDATE account SET money=money+10 WHERE name = ?;";
String sql2 = "UPDATE account SET money=money-10 WHERE name = ?;";
ps = conn.prepareStatement(sql1);
ps.setString(1,"lili");
ps.executeUpdate();
int a = 1 / 0;
ps = conn.prepareStatement(sql2);
ps.setString(2,"Potter");
ps.executeUpdate();
// 提交事务
conn.commit();
} catch (Exception e){
e.printStackTrace();
// 回滚事务
if (conn != null){
try {
conn.rollback();
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
} finally{
JDBCUtils.close(conn,ps,rs);
}
}
}
也可以设置回滚点回滚部分事务。
SavePoint sp = conn.setSavePoint();
conn.rollback(sp);
设置回滚点后,执行的语句超过回滚点,则不会全部回滚,只会回滚到回滚点上
注意:回到回滚点后,回滚点之前的代码虽然没被回滚但是也没提交呢,如果想起作用还要做commit操作
public class TransJDBC {
public static void main(String[] args) {
Connection conn = null;
PreparedStatement ps = null;
ResultSet rs = null;
Savepoint savepoint = null;
try {
conn = JDBCUtils.getConn();
// 开启事务
conn.setAutoCommit(false);
String sql1 = "UPDATE account SET money=money+10 WHERE name = ?;";
String sql2 = "UPDATE account SET money=money+10 WHERE name = ?;";
ps = conn.prepareStatement(sql1);
ps.setString(1,"lili");
ps.executeUpdate();
// 设置回滚点
savepoint = conn.setSavepoint();
int a = 1 / 0;
ps = conn.prepareStatement(sql2);
ps.setString(1,"lili");
ps.executeUpdate();
// 提交事务
conn.commit();
} catch (Exception e){
e.printStackTrace();
// 回滚事务
if (conn != null){
try {
if (savepoint != null){
// 走过了回滚点
conn.rollback(savepoint);
conn.commit();
}
else{ // 没有走过回滚点
conn.rollback();
}
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
} finally{
JDBCUtils.close(conn,ps,rs);
}
}
}
事务的四大特性
事务的四大特性是事务本身具有的特点。简称ACID。
- 原子性 (Atomicity): 原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
- 一致性 (Consistency): 事务前后数据的完整性必须保持一致。
- 隔离性 (Isolation): 事务的隔离性是指多个用户并发访问数据库时,一个用户的事务不能被其它用户的事务所干扰,多个并发事务之间数据要相互隔离。
- 持久性 (Durability): 持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发生故障也不应该对其有任何影响。
隔离性
数据库隔离性分析
- 数据库的其他三大特性数据库可以保证,而隔离性需要再讨论
- 数据库的隔离性问题本质上就是多线程并发安全性问题
- 可以用锁来解决多线成并发安全问题,但是如果用了锁,必然会造成程序的性能大大的下降,对于数据库这种高并发要求的程序来说这是不可接受的
具体分析下隔离性产生的细节:
- 如果两个线程并发修改,必然产生多线程并发安全问题,必须隔离开
- 如果两个线程并发查询,必然没有问题,不需要隔离
- 如果一个线程修改,一个线程查询,在不同的应用场景下有可能有问题,有可能没问题。
隔离性可能造成的问题
脏读:
打开两个mysql客户端,都执行以下语句:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
一个事务读取到另一个事务未提交的数据:a给b转账
----------------------------
CREATE TABLE account(id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(20), money INT);
INSERT INTO account(name,money) VALUES('a',1000); -- a:1000
INSERT INTO account(name,money) VALUES('b',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;
-----------------------------
脏数据:未提交的随后又被撤销的数据
不可重复读:
- 一个事务多次读取数据库中的同一条记录,多次查询的结果不同
- 一个事务读取到另一个事务已经提交的数据
打开两个mysql客户端,都执行以下语句:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
a在一次事务中查询自己的账户情况
----------------------------
-- 隔离性演示:不可重复读
-- 设置初始值 1000
UPDATE account SET money=1000 WHERE name = 'a';
UPDATE account SET money=1000 WHERE name = 'b';
----------------------------
客户端A:
START TRANSACTION;
SELECT * FROM account WHERE name = 'a'; -- 1000
SELECT * FROM account WHERE name = 'b'; -- 1000
-----------------------------
客户端B:
START TRANSACTION;
UPDATE account SET money=money-100 WHERE name = 'a';
UPDATE account SET money=money+100 WHERE name = 'b';
COMMIT
-----------------------------
客户端A:
SELECT * FROM account WHERE name = 'a'; -- 900
SELECT * FROM account WHERE name = 'b'; -- 1100
COMMIT;
-----------------------------
虚读 (幻读)
- 出现的概率非常低
- 一个事务多次查询整表数据,多次查询时,由于有其他事务增删数据, 造成的查询结果不同
- 一个事务读取到另一个事务已经提交的数据
打开两个mysql客户端,都执行以下语句:
SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
客户端A在一次事务中查询账户总额:
----------------------------
客户端A:
START TRANSACTION;
SELECT SUM(money) FROM account; -- 2000
-----------------------------
客户端B:
START TRANSACTION;
INSERT INTO account(name,money) VALUES('c',5000);
COMMIT;
-----------------------------
客户端A:
SELECT SUM(money) FROM account; -- 7000
COMMIT;
-----------------------------
数据库的隔离级别
数据库设计者在设计数据库时到底该防止哪些问题呢?防止的问题越多性能越低,防止的问题越少,则安全性越差。
数据库的四大隔离级别:
read uncommitted :不做任何隔离,可能造成脏读、不可重复读、虚读 (幻读) 问题
read committed: 可以防止脏读,但是不能防止不可重复读、虚读 (幻读) 问题
repeatable Read: 可以防止脏读、不可重复读,但是不能防止虚读 (幻读) 问题
serializable:可以防止所有隔离性的问题,但是数据库就被设计为串行化的数据库,性能很低
从安全性上考虑:
Serializable
>Repeatable Read
>Read Committed
>Read uncommitted
从性能上考虑:
Read uncommitted
>Read committed
>Repeatable Read
>Serializable
通常从Repeatable Read和Read committed中选择一个
如果需要防止不可重复读选择Repeatable Read,如果不需要防止选择Read committed
mysql数据库默认的隔离级别就是Repeatable Read
Oracle数据库默认的隔离级别是Read committed
操作数据库的隔离级别
查询数据库的隔离级别:
SELECT @@TX_ISOLATION;
修改数据库的隔离级别:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 隔离级别;
不写默认就是session,修改的是当前客户端和服务器交互时是使用的隔离级别,并不会影响其他客户端的隔离级别
如果写成global,修改的是数据库默认的隔离级别 (即新开客户端时,默认的隔离级别),并不会修改当前客户端和已经开启的客户端的隔离级别
如:
set global transaction isolation level serializable;
数据库中的锁
共享锁
共享锁和共享锁可以共存,共享锁和排他锁不能共存
在非Serializable隔离级别下做查询不加任何锁,在Serializable隔离级别下做查询加共享锁。
案例演示:
打开两个mysql客户端,将隔离级别都设置为Serializable级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE; -- 设置后查询加了共享锁
分别在两个客户端中查询:
START TRANSACTION;
SELECT * FROM account; -- 都能查询出数据,说明共享锁可以共存。
排他锁
- 排他锁和共享锁不能共存,排他锁和排他锁也不能共存
- 在任何隔离级别下做增删改都加排他锁
案例演示:
打开两个mysql客户端,将隔离级别都设置为Serializable级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
两个客户端都执行:
START TRANSACTION;
SELECT * FROM account;
-----------------------------
客户端A执行:
UPDATE account SET money=money-100 WHERE name = 'a';
-- 执行在等待,当另外一个客户端提交commit或者回滚rollback之后,修改才能成功。
-----------------------------
客户端B执行:
COMMIT; -- 客户端B提交后释放共享锁,客户端A执行修改
-----------------------------
客户端A执行:
COMMIT; -- 客户A提交修改,释放排它锁
死锁
- mysql可以自动检测到死锁,错误退出一方并执行另一方
案例演示:
打开两个mysql客户端,将隔离级别都设置为Serializable级别
SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
两个客户端都执行:
START TRANSACTION;
SELECT * FROM account;
-----------------------------
客户端A执行:
UPDATE account SET money = 1000 WHERE name = 'a';
-- 客户端A升级为排它锁,等待客户端B执行
-----------------------------
客户端B执行:
UPDATE account SET money = 1000 WHERE name = 'b';
-- 客户端B升级为排它锁,等待客户端A执行
-----------------------------
发现彼此等待,直到一方报错结束,死锁才结束。