第4章--事务

事务原理与开发

事务Transaction:

什么是事务?

事务是并发控制的基本单位,指作为单个逻辑工作单元执行的一系列操作,且逻辑工作单元需满足ACID特性。

i.e. 银行转账:开始交易;张三账户扣除100元;李四账户增加100元;结束交易。

事务的特性:ACID

原子性 Atomicity:整个交易必须作为一个整体来执行。(要么全部执行,要么全部不执行)

一致性 Consistency:整个交易总体资金不变

隔离性 Isolation:

case1: 若张三给李四转账过程中,赵五给张三转账了200元。两个交易并发执行。

T1   T2

读取张三余额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读取所有用户(张三、李四);T2新增用户赵五;T1读取所有用户(3个);T1/T2结束。T1中两次读取的结果中行记录数不同,称为幻读。

需要避免上述cases的产生

隔离性:交易之间相互隔离,在一个交易完成之前,不能受到其他交易的影响

持久性 Durability:整个交易过程一旦结束,无论出现任何情况,交易都应该是永久生效的

使用JDBC进行事务控制:

Connection类中

.setAutoCommit():开启事务(若为false,则该Connection对象后续的sql都将作为事务来处理;若为true,则该Connection对象后续的所有sql都将作为单独的语句执行(默认为true))

.commit():事务被提交,即事务生效并结束

.rollback():回滚,回退到事务开始之前的状态

i.e.

  1. ALTER TABLE user ADD Account int;
  2. UPDATE User SET Account = 100 WHERE id = 1;
  3. UPDATE User SET Account = 0 WHERE id > 1;

实现ZhangSi(1)给LiSan(2)转账的过程:

(非事务:)

  1. public static void TransferNonTransaction() {
  2. Connection conn = null;
  3. PreparedStatement ptmt = null;
  4.  
  5. try {
  6. conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
  7. String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
  8. // transfer 100 from ZhangSi(1) to LiSan(2)
  9. ptmt = conn.prepareStatement(sql);
  10. ptmt.setInt(1, 0);
  11. ptmt.setString(2, "ZhangSi");
  12. ptmt.setInt(3, 1);
  13. ptmt.execute();
  14.  
  15. ptmt.setInt(1, 100);
  16. ptmt.setString(2, "LiSan");
  17. ptmt.setInt(3, 2);
  18. ptmt.execute();
  19. } catch (SQLException e) {
  20. e.printStackTrace();
  21. } finally {
  22. try {
  23. if (conn != null) conn.close();
  24. if (ptmt != null) ptmt.close();
  25. } catch (SQLException e) {
  26. e.printStackTrace();
  27. }
  28. }
  29. }

执行完第一个ptmt.execute()后,数据库中ZhangSi的Account=0, LiSan的Account=0;

出现了一个中间状态,对于整个业务逻辑的实现是不可接受的。如果此时程序崩溃了将不可挽回。

(事务:)

  1. public static void TransferByTransaction() {
  2. Connection conn = null;
  3. PreparedStatement ptmt = null;
  4. try {
  5. conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
  6.  
  7. // Using Transaction mechanism
  8. conn.setAutoCommit(false);
  9. String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
  10. ptmt = conn.prepareStatement(sql);
  11. ptmt.setInt(1, 0);
  12. ptmt.setString(2, "ZhangSi");
  13. ptmt.setInt(3, 1);
  14. ptmt.execute();
  15.  
  16. ptmt.setInt(1, 100);
  17. ptmt.setString(2, "LiSan");
  18. ptmt.setInt(3, 2);
  19. ptmt.execute();
  20.  
  21. // Commit the transaction
  22. conn.commit();
  23.  
  24. } catch (SQLException e) {
  25. // if something wrong happens, rolling back
  26. if(conn != null) {
  27. try {
  28. conn.rollback();
  29. } catch (SQLException e1) {
  30. e1.printStackTrace();
  31. }
  32. }
  33. e.printStackTrace();
  34. } finally {
  35. try {
  36. if (conn != null) conn.close();
  37. if (ptmt != null) ptmt.close();
  38. } catch (SQLException e) {
  39. e.printStackTrace();
  40. }
  41. }
  42. }

若在第一个ptmt.execute()时断点,并查询数据库,结果为事务执行之前的状态,并不是中间状态。

直到conn.commit()方法执行完毕,事务中的所有操作在数据库中才有效。

Connection类中的检查点功能:

.setSavePoint():在执行过程中创建保存点,以便rollback()可以回滚到该保存点

.rollback(SavePoint savePoint):回滚到某个检查点

i.e.

  1. public static void rollbackTest() {
  2. Connection conn = null;
  3. PreparedStatement ptmt = null;
  4. // save point
  5. Savepoint sp = null;
  6. try {
  7. conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
  8.  
  9. conn.setAutoCommit(false);
  10. String sql = "UPDATE User SET Account = ? WHERE userName = ? AND id = ?;";
  11. ptmt = conn.prepareStatement(sql);
  12. ptmt.setInt(1, 0);
  13. ptmt.setString(2, "ZhangSi");
  14. ptmt.setInt(3, 1);
  15. ptmt.execute();
  16. // create a save point
  17. sp = conn.setSavepoint();
  18.  
  19. ptmt.setInt(1, 100);
  20. ptmt.setString(2, "LiSan");
  21. ptmt.setInt(3, 2);
  22. ptmt.execute();
  23.  
  24. // throw an exception manually for the purpose of testing
  25. throw new SQLException();
  26.  
  27. } catch (SQLException e) {
  28. // if something wrong happens, rolling back to the save point created before
  29. // and then transfer the money to Guoyi(3)
  30. if(conn != null) {
  31. try {
  32. conn.rollback(sp);
  33. System.out.println("Transfer from ZhangSi(1) to LiSan(2) failed;\n"
  34. + "Transfer to GuoYi(3) instead");
  35.  
  36. // other operations
  37. ptmt.setInt(1, 100);
  38. ptmt.setString(2, "GuoYi");
  39. ptmt.setInt(3, 3);
  40. ptmt.executeQuery();
  41. conn.commit();
  42. } catch (SQLException e1) {
  43. e1.printStackTrace();
  44. }
  45. }
  46.  
  47. e.printStackTrace();
  48. } finally {
  49. try {
  50. if (conn != null) conn.close();
  51. if (ptmt != null) ptmt.close();
  52. } catch (SQLException e) {
  53. e.printStackTrace();
  54. }
  55. }
  56. }

事务的隔离级别: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等待事务1、等等。产生原因:每个事务获取锁的顺序不一致导致。解决方法:按照同一顺序获取锁,可以破坏该条件。通过分析死锁事务之间的锁竞争关系,调整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顺序等操作有效避免死锁再次产生。

事务单元测试

本次得分为:70.00/70.00, 本次测试的提交时间为:2017-08-25
1单选(5分)

事务的隔离性是指?

  • A.一个事务一旦提交成功,则事务对数据的改变将永久生效。
  • B.事务包含的所有操作,要么全部完成,要么全部不完成。
  • C.事务执行前和事务执行后,数据必须处于一致的状态。
  • D.一个事务内部的操作及使用的数据对并发的其他事务是隔离的。5.00/5.00
2单选(5分)

设有两个事务T1、T2,其并发操作如图所示,下面描述正确的是:

  • A.该操作读取“脏”数据。5.00/5.00
  • B.该操作存在更新丢失。
  • C.该操作不可重复读。
  • D.该操作保证ACID特性。
3单选(5分)

JDBC 实现事务控制,开启事务使用哪个方法?

  • A..setSavePoint()
  • B..commit()
  • C..setAutoCommit(false)5.00/5.00
  • D..rollback()
4单选(5分)

以下哪个事务隔离级别不存在脏读,但是存在不可重复读?

  • A.read uncommitted
  • B.repeatable read
  • C.read committed5.00/5.00
  • D.serializable
5单选(5分)

以下哪项不是死锁产生的必要条件?

  • A.单个事务。5.00/5.00
  • B.互斥。
  • C.不剥夺。
  • D.环路等待。
6单选(5分)

关于死锁描述不正确的是?

  • A.MySQL数据库会自动解除死锁,随机回滚一个事务,解除事务持有的锁资源。5.00/5.00
  • B.单个事务是不会发生死锁的。
  • C.Show engine innodb status 可以查看发生死锁的SQL语句。
  • D.死锁产生的根本原因是由于两个事务之间的加锁顺序问题。
7多选(40分)

以下描述正确的是?

  • A.为了预防死锁,在完成应用程序时,必须做到按序加锁,这主要是破坏死锁必要条件的不剥夺条件。
  • B.MySQL 数据库实现了多版本控制,支持快照读,读不加锁。20.00/40.00
  • C.在MySQL中存在共享锁和排他锁两种加锁模式,一个事务对某行记录加了共享锁,则另外一个事务无论是添加共享锁还是排他锁,都可以添加。
  • D.MySQL数据库实现了事务死锁检测和解决机制,数据库系统一旦发现死锁,会自动强制回滚代价最小的事务,解除死锁。

事务作业

事务的单元作业,包括一道编程题目。

1(100分)

有一个在线交易电商平台,有两张表,分别是库存表和订单表,如下:

现在买家XiaoMing在该平台购买bag一个,需要同时在库存表中对bag库存记录减一,同时在订单表中生成该订单的相关记录。

请编写Java程序,实现XiaoMing购买bag逻辑。订单表ID字段为自增字段,无需赋值。

答:

创建数据库:

  1. mysql> CREATE TABLE Inventory (
  2. -> ID int auto_increment primary key,
  3. -> ProductName varchar(20) not null,
  4. -> Inventory int not null);
  5. mysql> INSERT INTO Inventory VALUES (null, "watch", 25);
  6. mysql> INSERT INTO Inventory VALUES (null, "bag", 20);
  7. mysql> CREATE TABLE Orders (
  8. -> Id int auto_increment primary key,
  9. -> Buyer varchar(20) not null,
  10. -> ProductName varchar(20) not null);

业务逻辑:

  1. public static void purchase() throws ClassNotFoundException {
  2. Connection conn = null;
  3. PreparedStatement ptmt = null;
  4. ResultSet rs = null;
  5. String sql = "";
  6. int currNumberofBags = -1;
  7. String buyer = "XiaoMing";
  8. String productToBuy = "bag";
  9.  
  10. Class.forName(DRIVER_NAME);
  11. try {
  12. conn = DriverManager.getConnection(DB_URL, USER_NAME, PASSWORD);
  13.  
  14. conn.setAutoCommit(false);
  15. // the number of bags in the inventory
  16. sql = "SELECT Inventory FROM Inventory WHERE ProductName = ?";
  17. ptmt = conn.prepareStatement(sql);
  18. ptmt.setString(1, productToBuy);
  19. rs = ptmt.executeQuery();
  20. if (rs.next()) {
  21. currNumberofBags = rs.getInt("Inventory");
  22. }
  23. if (currNumberofBags > 0) {
  24. // Buy one bag
  25. sql = "UPDATE Inventory SET Inventory = ? WHERE ProductName = ?";
  26. ptmt = conn.prepareStatement(sql);
  27. ptmt.setInt(1, currNumberofBags-1);
  28. ptmt.setString(2, productToBuy);
  29. ptmt.execute();
  30.  
  31. sql = "INSERT INTO Orders VALUES (null, ?, ?);";
  32. ptmt = conn.prepareStatement(sql);
  33. ptmt.setString(1, buyer);
  34. ptmt.setString(2, productToBuy);
  35. ptmt.execute();
  36. }
  37. conn.commit();
  38. } catch (SQLException e) {
  39. e.printStackTrace();
  40. try {
  41. conn.rollback();
  42. } catch (SQLException e1) {
  43. e1.printStackTrace();
  44. }
  45. } finally {
  46. try {
  47. if (conn != null) conn.close();
  48. if (ptmt != null) ptmt.close();
  49. if (rs != null) rs.close();
  50. } catch (SQLException e) {
  51. e.printStackTrace();
  52. }
  53. }
  54. }
  55.  
  56. public static void main(String[] args) throws ClassNotFoundException {
  57. purchase();
  58. }

第一次执行后:

  1. mysql> select * from inventory;
  2. +----+-------------+-----------+
  3. | Id | ProductName | Inventory |
  4. +----+-------------+-----------+
  5. | 1 | watch | 25 |
  6. | 2 | bag | 19 |
  7. +----+-------------+-----------+
  8. 2 rows in set (0.00 sec)
  9.  
  10. mysql> select * from orders;
  11. +----+----------+-------------+
  12. | Id | Buyer | ProductName |
  13. +----+----------+-------------+
  14. | 3 | XiaoMing | bag |
  15. +----+----------+-------------+
  16. 1 row in set (0.00 sec)

第二次执行后:

  1. mysql> select * from inventory;
  2. +----+-------------+-----------+
  3. | Id | ProductName | Inventory |
  4. +----+-------------+-----------+
  5. | 1 | watch | 25 |
  6. | 2 | bag | 18 |
  7. +----+-------------+-----------+
  8. 2 rows in set (0.00 sec)
  9.  
  10. mysql> select * from orders;
  11. +----+----------+-------------+
  12. | Id | Buyer | ProductName |
  13. +----+----------+-------------+
  14. | 3 | XiaoMing | bag |
  15. | 4 | XiaoMing | bag |
  16. +----+----------+-------------+
  17. 2 rows in set (0.00 sec)

Java开发工程师(Web方向) - 03.数据库开发 - 第4章.事务的更多相关文章

  1. Java开发工程师(Web方向) - 03.数据库开发 - 第5章.MyBatis

    第5章--MyBatis MyBatis入门 Abstract: 数据库框架的工作原理和使用方法(以MyBatis为例) 面向对象的世界与关系型数据库的鸿沟: 面向对象世界中的数据是对象: 关系型数据 ...

  2. Java开发工程师(Web方向) - 03.数据库开发 - 第3章.SQL注入与防范

    第3章--SQL注入与防范 SQL注入与防范 经常遇到的问题:数据安全问题,尤其是sql注入导致的数据库的安全漏洞 国内著名漏洞曝光平台:WooYun.org 数据库泄露的风险:用户信息.交易信息的泄 ...

  3. Java开发工程师(Web方向) - 03.数据库开发 - 第1章.JDBC

    第1章--JDBC JDBC基础 通过Java Database Connectivity可以实现Java程序对后端数据库的访问 一个完整的数据库部署架构,通常是由客户端和服务器端两部分组成 客户端封 ...

  4. Java开发工程师(Web方向) - 03.数据库开发 - 第2章.数据库连接池

    第2章--数据库连接池 数据库连接池 一般而言,在实际开发中,往往不是直接使用JDBC访问后端数据库,而是使用数据库连接池的机制去管理数据库连接,来实现对后端数据库的访问. 建立Java应用程序到后端 ...

  5. Java开发工程师(Web方向) - 03.数据库开发 - 期末考试

    期末考试 编程题 本编程题包含4个小题,覆盖知识点从基础的JDBC.连接池到MyBatis. 1(10分) 有一款在线教育产品“天天向上”主要实现了在手机上查看课程表的功能.该产品的后端系统有一张保存 ...

  6. Java开发工程师(Web方向) - 04.Spring框架 - 第2章.IoC容器

    第2章.IoC容器 IoC容器概述 abstract: 介绍IoC和bean的用处和使用 IoC容器处于整个Spring框架中比较核心的位置:Core Container: Beans, Core, ...

  7. Java开发工程师(Web方向) - 04.Spring框架 - 第1章.Spring概述

    第1章.Spring概述 Spring概述 The Spring Framework is a lightweight solution and a potential one-stop-shop f ...

  8. Java开发工程师(Web方向) - 02.Servlet技术 - 第3章.Servlet应用

    第3章.Servlet应用 转发与重定向 转发:浏览器发送资源请求到ServletA后,ServletA传递请求给ServletB,ServletB生成响应后返回给浏览器. 请求转发:forward: ...

  9. Java开发工程师(Web方向) - 02.Servlet技术 - 第2章.Cookie与Session

    第2章--Cookie与Session Cookie与Session 浏览器输入地址--HTTP请求--Servlet--HTTP响应--浏览器接收 会话(session):打开浏览器,打开一系列页面 ...

随机推荐

  1. 【luogu P2296 寻找道路】 题解

    题目链接:https://www.luogu.org/problemnew/show/P2296 题意:给定起点终点,找一条从起点到终点的最短路径使路上的每个点都能有路径到达终点. 我们先反着建一遍图 ...

  2. Winodws SNMP服务安装和配置(Windows 2003 & 2008 R2)

    简单网络管理协议SNMP服务起着代理的作用,它会收集可以向SNMP管理站或控制台报告的信息.您可以使用SNMP服务来收集数据,并且在整个公司网络范围内管理基于Windows Server 2003.M ...

  3. Java基础随笔3

    一. 键盘录入数据概述 我们目前在写程序的时候,数据值都是固定的,但是实际开发中,数据值肯定是变化的,所以,把数据改进为键盘录入,提高程序的灵活性. 键盘录入数据的步骤: A:导包(位置放到class ...

  4. maven中的坐标和仓库

    1.坐标 pom.xml中的groupId.artifactId和version都可以构成项目的坐标. <dependency>    <groupId></groupI ...

  5. 关于nodejs下载组件经常失败的问题

    由于最近在刚开始做一个前台element和mybatisplus的项目,但是在使用nodejs下载vue的脚手架和各种组件时,会经常出现下载失败的问题,进而导致前台无法启动. 在网上查询之后发现在下载 ...

  6. 复习宝典之Spring

    查看更多宝典,请点击<金三银四,你的专属面试宝典> 第六章:Spring Spring容器是Spring的核心,一切Spring bean都存储在Spring容器内,并由其通过IoC技术管 ...

  7. CentOS7 yum命令

    1.yum 清理缓存 [hado@localhost /]# yum clean all [hado@localhost /]# rm -rf /var/cache/yum/*

  8. Filebeat使用模块收集日志

    1.先决条件 在运行Filebeat模块之前: 安装并配置Elastic stack 完成Filebeat的安装 检查Elasticsearch和Kibana是否正在运行,以及Elasticsearc ...

  9. 浅谈CSS高度坍塌

    高度坍塌情况: 当父元素没有设置高度,且子元素块都向左(右)浮动起来,那么父元素就会出现坍塌的现象. 解决办法: 在父元素包含块中加一个div: 优点:兼容性强,适合初学者. 缺点:不利于优化. 方法 ...

  10. [翻译]Hystrix wiki–How it Works

    注:本文并非是精确的文档翻译,而是根据自己理解的整理,有些内容可能由于理解偏差翻译有误,有些内容由于是显而易见的,并没有翻译,而是略去了.本文更多是学习过程的产出,请尽量参考原官方文档. 流程图 下图 ...