事务Transaction: mysql
什么是事务?sql
事务是并发控制的基本单位,指做为单个逻辑工做单元执行的一系列操做,且逻辑工做单元需知足ACID特性。数据库
i.e. 银行转帐:开始交易;张三帐户扣除100元;李四帐户增长100元;结束交易。编程
事务的特性:ACID并发
原子性 Atomicity:整个交易必须做为一个总体来执行。(要么所有执行,要么所有不执行)app
一致性 Consistency:整个交易整体资金不变性能
隔离性 Isolation:单元测试
case1: 若张三给李四转帐过程当中,赵五给张三转帐了200元。两个交易并发执行。 测试
T1 T2ui
读取张三余额100;
读取张三余额100;
给李四转帐100,
更新张三余额为0;
交易结束 赵五转入200,
更新张三余额为300
交易结束
case2: 脏读:张三给别人转帐100以后张三存钱200,存钱后转帐因为系统缘由失败回滚。
读取一个事务未提交的更新
T1 T2
读取张三余额100
(转帐) 更新张三余额0
读取张三余额0
T1 Rollback() (存钱) 更新张三余额200
T2结束(张三帐户余额为200)
case3: 不可重复读:同一个事务,两次读取同一数值的结果不一样,成为不可重复读。
T1张三读取本身余额为100;T2读取张三余额100;T2存钱更新为300;T1张三读取余额为300。T1中两次读取张三余额即为不可重复读。
case4: 幻读:两次读取的结果包含的行记录不同。
T1读取全部用户(张3、李四);T2新增用户赵五;T1读取全部用户(3个);T1/T2结束。T1中两次读取的结果中行记录数不一样,称为幻读。
须要避免上述cases的产生
隔离性:交易之间相互隔离,在一个交易完成以前,不能受到其余交易的影响
持久性 Durability:整个交易过程一旦结束,不管出现任何状况,交易都应该是永久生效的
使用JDBC进行事务控制:
Connection类中
.setAutoCommit():开启事务(若为false,则该Connection对象后续的sql都将做为事务来处理;若为true,则该Connection对象后续的全部sql都将做为单独的语句执行(默认为true))
.commit():事务被提交,即事务生效并结束
.rollback():回滚,回退到事务开始以前的状态
i.e.
ALTER TABLE user ADD Account int; UPDATE User SET Account = 100 WHERE id = 1; UPDATE User SET Account = 0 WHERE id > 1;
实现ZhangSi(1)给LiSan(2)转帐的过程:
(非事务:)
public static void TransferNonTransaction() { Connection conn = null; PreparedStatement ptmt = null; try { conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD); String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;"; // transfer 100 from ZhangSi(1) to LiSan(2) ptmt = conn.prepareStatement(sql); ptmt.setInt(1, 0); ptmt.setString(2, "ZhangSi"); ptmt.setInt(3, 1); ptmt.execute(); ptmt.setInt(1, 100); ptmt.setString(2, "LiSan"); ptmt.setInt(3, 2); ptmt.execute(); } catch (SQLException e) { e.printStackTrace(); } finally { try { if (conn != null) conn.close(); if (ptmt != null) ptmt.close(); } catch (SQLException e) { e.printStackTrace(); } } }
执行完第一个ptmt.execute()后,数据库中ZhangSi的Account=0, LiSan的Account=0;
出现了一个中间状态,对于整个业务逻辑的实现是不可接受的。若是此时程序崩溃了将不可挽回。
(事务:)
public static void TransferByTransaction() { Connection conn = null; PreparedStatement ptmt = null; try { conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD); // Using Transaction mechanism conn.setAutoCommit(false); String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;"; ptmt = conn.prepareStatement(sql); ptmt.setInt(1, 0); ptmt.setString(2, "ZhangSi"); ptmt.setInt(3, 1); ptmt.execute(); ptmt.setInt(1, 100); ptmt.setString(2, "LiSan"); ptmt.setInt(3, 2); ptmt.execute(); // Commit the transaction conn.commit(); } catch (SQLException e) { // if something wrong happens, rolling back if(conn != null) { try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } e.printStackTrace(); } finally { try { if (conn != null) conn.close(); if (ptmt != null) ptmt.close(); } catch (SQLException e) { e.printStackTrace(); } } }
若在第一个ptmt.execute()时断点,并查询数据库,结果为事务执行以前的状态,并非中间状态。
直到conn.commit()方法执行完毕,事务中的全部操做在数据库中才有效。
Connection类中的检查点功能:
.setSavePoint():在执行过程当中建立保存点,以便rollback()能够回滚到该保存点
.rollback(SavePoint savePoint):回滚到某个检查点
i.e.
public static void rollbackTest() { Connection conn = null; PreparedStatement ptmt = null; // save point Savepoint sp = null; try { conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD); conn.setAutoCommit(false); String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;"; ptmt = conn.prepareStatement(sql); ptmt.setInt(1, 0); ptmt.setString(2, "ZhangSi"); ptmt.setInt(3, 1); ptmt.execute(); // create a save point sp = conn.setSavepoint(); ptmt.setInt(1, 100); ptmt.setString(2, "LiSan"); ptmt.setInt(3, 2); ptmt.execute(); // throw an exception manually for the purpose of testing throw new SQLException(); } catch (SQLException e) { // if something wrong happens, rolling back to the save point created before // and then transfer the money to Guoyi(3) if(conn != null) { try { conn.rollback(sp); System.out.println("Transfer from ZhangSi(1) to LiSan(2) failed;\n" + "Transfer to GuoYi(3) instead"); // other operations ptmt.setInt(1, 100); ptmt.setString(2, "GuoYi"); ptmt.setInt(3, 3); ptmt.executeQuery(); conn.commit(); } catch (SQLException e1) { e1.printStackTrace(); } } e.printStackTrace(); } finally { try { if (conn != null) conn.close(); if (ptmt != null) ptmt.close(); } catch (SQLException e) { e.printStackTrace(); } } }
事务的隔离级别:4个级别
读未提交(read uncommited):可能致使脏读
读提交(read commited):不可能脏读,可是会出现不可重复读
重复读(repeatable read):不会出现不可重复读,可是会出现幻读
串行化(serializable):最高隔离级别,不会出现幻读,但严格的并发控制、串行执行致使数据库性能差
N.B. 1. 事务隔离级别越高,数据库性能越差,但对于开发者而言编程难度越低。
2. MySQL默认事务隔离级别为重复读 repeatable read
JDBC设置隔离级别:
Connection对象中,
.getTransactionIsolation();
.setTransactionIsolation();
上节讲到数据库的隔离性,开发者通常会使用加锁来保证隔离性,但会遇到死锁的问题。
场景:
数据库:
ID | UserName | Account | Corp |
1 | ZhangSan | 100 | Ali |
2 | Lisi | 0 | Ali |
事务1:张三给李四转帐100元钱
事务2:张三和李四的单位改成Netease
事务持锁:
MySQL是以行加锁的方式来避免不一样事务对同一行数据的修改
事务1对张三这行记录的修改要使用到对这一行的行锁。
事务2同时并发执行,事务2先修改李四的行记录的Corp,使用了对李四的行锁。
事务1想要更新李四记录,须要持有李四的行锁,可是事务2占据了李四的行锁,因而事务1等待事务2执行完成后对李四行锁的释放。
事务2想要更新张三记录,须要持有张三的行锁,可是事务1占据了张三的行锁,因而事务2等待事务1执行完成后对张三行锁的释放。
事务1和事务2相互等待,两个事务都没法继续进行。
-->死锁
死锁:
两个或两个以上的事务在执行过程当中,因争夺锁资源而形成的一种互相等待的现象。
死锁产生的必要条件:
互斥:并发执行的事务为了进行必要的隔离保证执行正确,在事务结束前,须要对修改的数据库记录持锁,保证多个事务对相同数据库记录串行修改。对于大型并发系统而言是没法避免的。
请求和保持:一个事务须要申请多个资源,而且已经持有一个资源,在等待另外一个资源锁。死锁仅发生在请求两个或者两个以上的锁对象时。因为业务须要修改多行数据库记录,难以免。
不剥夺:已经得到锁资源的事务,在未执行完成前,不能被强制剥夺,只能使用完时由事务本身释放。通常用于已经出现死锁时,经过破坏该条件达到解除死锁的目的--数据库系统一般经过必定的死锁检测机制发现死锁,强制回滚持有锁的代价相对较小的事务,让另一个事务执行完毕,就能解除死锁的问题。
环路等待:发生死锁时,必然存在一个事务-锁的环形链,如事务1由于锁1等待事务2,事务2由于锁2等待事务一、等等。产生缘由:每一个事务获取锁的顺序不一致致使。解决方法:按照同一顺序获取锁,能够破坏该条件。经过分析死锁事务之间的锁竞争关系,调整SQL的顺序,达到消除死锁的目的。i.e. 若事务1和事务2刚开始都想获取锁1,就不会造成环路,就不会出现环路等待,不会出现死锁了。----按序获取锁资源:预防死锁。
MySQL中的锁:
排它锁 X:与其余任何锁都是冲突的
共享锁 S:多个事务能够共享一把锁。若事务1获取了共享锁,事务二还想获取共享锁,则不需等待(是兼容的)
欲加锁 已有锁 |
X | S |
X | 冲突 | 冲突 |
S | 冲突 | 兼容 |
加锁方式:
外部加锁:由应用程序执行特定sql语句进行显式添加,锁依赖关系较容易分析
共享锁(S):select * from table lock in share mode;
排它锁(X):select * from table for update;
内部加锁:
为了实现ACID特性,由数据库系统内部自动添加。
加锁规则繁琐,与SQL执行计划、事务隔离级别、表索引结构有关。
哪些SQL须要持有锁?
不须要:快照读:Innodb实现了多版本控制(MVCC),支持不加锁快照读。全部select语句不加锁,能够保证同一个select的结果集是一致的。可是不能保证同一个事物内部,select语句和其余语句的数据一致性,若是业务须要,需经过外部显式加锁。
须要:当前读:
加了外部锁的select语句
Update from table set ......
Insert into ......
Delete from table ......
SQL加锁分析:
i.e.
ID | UserName | Account | Corp |
1 | ZhangSan | 100 | Ali |
2 | LiSi | 0 | Ali |
Update user set account = 0 where id = 1;
update语句直接在ID=1行数据处加排它锁,此时若为select操做 (是快照读),则不会被阻塞。
Select UserName from user where id = 1 in share mode;
该语句对行记录加了共享锁,此时若其余事务也对该行记录加共享锁,是不会阻塞的
分析死锁的经常使用办法:
MySQL数据库会自动分析死锁并回滚代价最小的事务处理死锁。
可是开发人员须要在死锁处理之后避免死锁再次发生。
show engine innodb status;
其中有发生死锁时相关的sql语句,也会列出被系统强制回滚的事务
分析死锁产生的缘由,能够经过改变sql顺序等操做有效避免死锁再次产生。
事务的隔离性是指?
设有两个事务T一、T2,其并发操做如图所示,下面描述正确的是:
JDBC 实现事务控制,开启事务使用哪一个方法?
如下哪一个事务隔离级别不存在脏读,可是存在不可重复读?
如下哪项不是死锁产生的必要条件?
关于死锁描述不正确的是?
如下描述正确的是?
事务的单元做业,包括一道编程题目。
有一个在线交易电商平台,有两张表,分别是库存表和订单表,以下:
如今买家XiaoMing在该平台购买bag一个,须要同时在库存表中对bag库存记录减一,同时在订单表中生成该订单的相关记录。
请编写Java程序,实现XiaoMing购买bag逻辑。订单表ID字段为自增字段,无需赋值。
答:
建立数据库:
mysql> CREATE TABLE Inventory ( -> ID int auto_increment primary key, -> ProductName varchar(20) not null, -> Inventory int not null); mysql> INSERT INTO Inventory VALUES (null, "watch", 25); mysql> INSERT INTO Inventory VALUES (null, "bag", 20); mysql> CREATE TABLE Orders ( -> Id int auto_increment primary key, -> Buyer varchar(20) not null, -> ProductName varchar(20) not null);
业务逻辑:
public static void purchase() throws ClassNotFoundException { Connection conn = null; PreparedStatement ptmt = null; ResultSet rs = null; String sql = ""; int currNumberofBags = -1; String buyer = "XiaoMing"; String productToBuy = "bag"; Class.forName(DRIVER_NAME); try { conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD); conn.setAutoCommit(false); // the number of bags in the inventory sql = "SELECT Inventory FROM Inventory WHERE ProductName = ?"; ptmt = conn.prepareStatement(sql); ptmt.setString(1, productToBuy); rs = ptmt.executeQuery(); if (rs.next()) { currNumberofBags = rs.getInt("Inventory"); } if (currNumberofBags > 0) { // Buy one bag sql = "UPDATE Inventory SET Inventory = ? WHERE ProductName = ?"; ptmt = conn.prepareStatement(sql); ptmt.setInt(1, currNumberofBags-1); ptmt.setString(2, productToBuy); ptmt.execute(); sql = "INSERT INTO Orders VALUES (null, ?, ?);"; ptmt = conn.prepareStatement(sql); ptmt.setString(1, buyer); ptmt.setString(2, productToBuy); ptmt.execute(); } conn.commit(); } catch (SQLException e) { e.printStackTrace(); try { conn.rollback(); } catch (SQLException e1) { e1.printStackTrace(); } } finally { try { if (conn != null) conn.close(); if (ptmt != null) ptmt.close(); if (rs != null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static void main(String[] args) throws ClassNotFoundException { purchase(); }
第一次执行后:
mysql> select * from inventory; +----+-------------+-----------+ | Id | ProductName | Inventory | +----+-------------+-----------+ | 1 | watch | 25 | | 2 | bag | 19 | +----+-------------+-----------+ 2 rows in set (0.00 sec) mysql> select * from orders; +----+----------+-------------+ | Id | Buyer | ProductName | +----+----------+-------------+ | 3 | XiaoMing | bag | +----+----------+-------------+ 1 row in set (0.00 sec)
第二次执行后:
mysql> select * from inventory; +----+-------------+-----------+ | Id | ProductName | Inventory | +----+-------------+-----------+ | 1 | watch | 25 | | 2 | bag | 18 | +----+-------------+-----------+ 2 rows in set (0.00 sec) mysql> select * from orders; +----+----------+-------------+ | Id | Buyer | ProductName | +----+----------+-------------+ | 3 | XiaoMing | bag | | 4 | XiaoMing | bag | +----+----------+-------------+ 2 rows in set (0.00 sec)