事务指逻辑上的一组操作,组成这组操作的各个单元,要么全部成功,要么全部不成功。
例如: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。
具体分析下隔离性产生的细节:
打开两个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客户端,将隔离级别都设置为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执行
-----------------------------
发现彼此等待,直到一方报错结束,死锁才结束。