Skip to content

Commit

Permalink
Merge branch 'Transaction' into development
Browse files Browse the repository at this point in the history
事务和死锁的介绍及相关代码
  • Loading branch information
osxcn committed Sep 2, 2017
2 parents bef2e1c + 711d00b commit 8012a78
Show file tree
Hide file tree
Showing 11 changed files with 454 additions and 1 deletion.
224 changes: 223 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,31 @@
* [3.3 解决方案](#33-解决方案)
* [3.4 其他注意事项](#34-其他注意事项)
* [4. 事务](#4-事务)
* []()
* [4.1 事务原理与开发](#41-事务原理与开发)
* [4.1.1 事务特性](#411-事务特性)
* [4.1.1.1 原子性](#4111-原子性)
* [4.1.1.2 一致性](#4112-一致性)
* [4.1.1.3 隔离性](#4113-隔离性)
* [4.1.1.4 持久性](#4114-持久性)
* [4.1.2 什么是事务?](#412-什么是事务)
* [4.1.3 JDBC事务控制](#413-jdbc事务控制)
* [4.1.4 检查点](#414-检查点)
* [4.1.5 事务并发执行](#415-事务并发执行)
* [4.1.5.1 脏读](#4151-脏读)
* [4.1.5.2 不可重复读](#4152-不可重复读)
* [4.1.5.3 幻读](#4153-幻读)
* [4.1.6 事务隔离级别](#416-事务隔离级别)
* [4.1.7 设置隔离级别](#417-设置隔离级别)
* [4.2 死锁分析与解决](#42-死锁分析与解决)
* [4.2.1 事务并发执行](#421-事务并发执行)
* [4.2.2 死锁产生的必要条件](#422-死锁产生的必要条件)
* [4.2.3 MySQL中的锁](#423-mysql中的锁)
* [4.2.4 加锁方式](#424-加锁方式)
* [4.2.5 哪些SQL需要持有锁](#425-哪些sql需要持有锁)
* [4.2.6 SQL加锁分析](#426-sql加锁分析)
* [4.2.7 分析死锁的常用方法](#427-分析死锁的常用方法)
* [5. MyBatis](#5-mybatis)
* []()

## 1. JDBC

Expand Down Expand Up @@ -423,7 +446,206 @@ Select * from user where userName = ? AND password = ?
* mysql可以使用AES_ENCRYPT/AES_DECRYPT加密和解密

## 4. 事务
### 4.1 事务原理与开发
#### 4.1.1 事务特性
事务特性以银行转账为例:
业务逻辑为:开始交易->张三账户扣除100元->李四账户增加100元->结束交易
##### 4.1.1.1 原子性
张三扣除100元和李四增加100元,这两个过程必须作为一个整体来完成,要么全部执行,要么一个都不执行,不可分割。
整个交易必须是一个整体,要么全做,要不都不做!
##### 4.1.1.2 一致性
在整个交易中,交易前和交易后,钱的总量是不变的。交易前张三有100元,李四0元,交易后张三把钱给了李四,张三0元,李四100元。整个交易前后钱都是一致的状态,称之为一致性。
##### 4.1.1.3 隔离性
在张三和李四交易的同时,此时赵五需要给张三转账200元。假设两个交易在并发的执行:
<p align="center">
<img src="/img/JDBC/隔离性.png" alt="隔离性">
</p>
首先,交易一种,银行系统读取张三的账户,余额为100元。假如此时交易二同时在发生,银行系统也是读取了张三账户的余额,为100元。然后交易一中,因为张三给李四转账100元,张三的账户被扣除了100元,银行系统更新张三账户的余额为0元。此时交易二,赵五给张三转账200元,张三的账户因为之前读取了100元,此时加上200元,实际变为了300元。如果此时银行更新账户为300元,则银行实际上亏本了。
实际上,张三原来有100元,赵五给了200元,张三给李四转了100元,实际的账户余额应为200元。因为两个交易是并发执行的,没有进行必要的隔离,导致出现了银行系统的错误。
所谓隔离性,就是指两个交易同时发生时,在一个交易执行之前,不能受到另一个交易的影响。

##### 4.1.1.4 持久性
两个人去银行交易转账,如果说交易结束之后,过了一段时间,因为银行机器坏了,两个人的交易不生效了,这个是没办法接受的。
持久性:一旦交易结束,无论出现任何情况,交易都应该是永久生效的。

#### 4.1.2 什么是事务?
`事务(Transaction)`是并发控制的基本单位,指作为单个逻辑工作单元执行的一系列操作,而这些逻辑工作单元需要满足`ACID`特性。
* 原子性:atomicity
* 一致性:consistency
* 隔离性:isolation
* 持久性:durability
如果一个业务场景,有这四个特性的需求,就可以使用事务来实现。

#### 4.1.3 JDBC事务控制
如何实现事务控制?JDBC的`Connection`对象提供了三个方法可以帮助实现 一个事务逻辑:
* setAutoCommit() 开启事务
将这个方法设为false,则这个`Connection`对象后续所有的执行的SQL语句都会被当做JDBC的一个事务来执行。如果设置为true,则表示`Connection`对象后续的每一个SQL都作为一个单独的语句执行,都是以非事务的方式来执行的。
注:默认情况下,都是以非事务的方式执行的,除非开始事务,将`setAutoCommit`设置为`false`
* commit() 提交事务
开启事务模式之后,`Connection`对象后续执行的所有SQL都将会作为一个事务来执行,直到调用`Connection``commit`方法。`commit`方法表示这个事务被提交,也就是说事务结束,整个事务执行的SQL语句的结果都将生效。
* rollback() 回滚事务
如果当事务执行的过程中出现问题,需要回滚这个事务的时候,可以调用`Connection`对象的`rollback`方法。回滚的意思就是虽然执行了事务中的语句,但是可以回滚到事务开始之前的一个状态。

[构建实例:事务-未使用事务-TransactionTestInit](/src/main/java/com/micro/profession/jdbc/practice/Transaction/TransactionTestInit.java)

[构建实例:事务-使用事务-TransactionTest](/src/main/java/com/micro/profession/jdbc/practice/Transaction/TransactionTest.java)

#### 4.1.4 检查点
JDBC提供了断点处理和控制的功能。
* setSavePoint() 保存断点
* rollback(SavePoint savePoint) 回滚到断点

[构建实例:事务-回滚断点-TransactionTestSavePoint](/src/main/java/com/micro/profession/jdbc/practice/Transaction/TransactionTestSavePoint.java)

#### 4.1.5 事务并发执行
在事务的四个特性中,原子性、一致性、持久性都不难理解,但是对于隔离性,涉及到一个并发控制,理解起来较为困难。以下通过进一步的涉及隔离性的例子来帮助理解日常开发过程中经常涉及到的几个概念,依旧以银行转账为例:
##### 4.1.5.1 脏读
例子:张三转账100元给李四,同时张三存银行200元
<p align="center">
<img src="/img/JDBC/脏读.png" alt="脏读">
</p>
张三转账100元给李四:T1中,银行系统读取了张三的账户为100元,更新张三账户的余额为0元;

此时张三在银行又进行了存钱交易,这时候正好开始,同时也读取了张三账户余额,此时读取到的账户余额为0元,然后更新张三账户为200元;

如果此时T1张三转账给李四的交易失败了,交易进行了回滚,本来应该回滚到交易前的状态;但是此时由于余额为0元的状态被张三存钱的事务给读取了,对其他的事务产生了影响。本来最后应该有300元的张三,最后只有200元。

> 脏读:读取一个事务未提交的更新!
##### 4.1.5.2 不可重复读
例子:T1张三读取两次账户余额,T2张三存了200元
<p align="center">
<img src="/img/JDBC/不可重复读.png" alt="不可重复读">
</p>
T1中,张三读取了余额为100,然后在T2存钱后T1再次读取余额,变为300元,也就是说同一个事务两次读取同一个记录,结果不一样。

> 不可重复读:同一个事务中两次读取相同的记录,结果不一样!
##### 4.1.5.3 幻读
例子:T1读取所有的用户,包含张三和李四两个用户,T2新增用户赵五
<p align="center">
<img src="/img/JDBC/幻读.png" alt="幻读">
</p>

T1读取了所有的用户,包含张三和李四两个用户,但此时T2又新增了一个赵五的用户,然后T1再次读取用户列表的时候,发现读取的行记录的个数与之前读取的结果不一致。
> 幻读:两次读取的结果包含的行记录不一样!
> 幻读与不可重复读的区别是:幻读是读到的记录条数不一样,不可重复读是读到的值不一样。
#### 4.1.6 事务隔离级别
* 读未提交(read uncommitted)
该级别允许出现脏读的情况
* 读提交(read committed)
不允许出现脏读,但是允许出现不可重复读
* 重复读(repeatable read)
不允许出现不可重复读,但是允许出现幻读
* 串行化(serializable)
不允许出现幻读,但作为事务最高隔离级别,并发控制最为严格,所有的事务都是串行执行的,会导致数据库的性能变得极差。

> MySQL默认事务级别为repeatable read。
> 事务隔离级别越高,数据库性能越差,编程的难度越低。
#### 4.1.7 设置隔离级别
JDBC中,可以通过`Connection``getTransactionIsolation``setTransactionIsolation`来获得和设置事务的隔离级别。

### 4.2 死锁分析与解决
#### 4.2.1 事务并发执行
数据库记录:

| ID | UserName | Account | Corp |
| :-- | :------ | :------ | :--- |
| 1 | ZhangSan | 100 | Ali |
| 2 | LiSi | 0 | Ali |

事务一: 张三需要给李四转账100元钱
事务二: 将张三和李四的单位改为Netease

* 事务持锁
两事务并发执行:
首先:事务一更新记录一的Account数据,持有记录一的行锁;事务二更新记录二的Corp记录,持有记录二的行锁;此时两事务各自持有一个行锁
接着:事务一要更新记录二的Account数据,需要持有李四的行锁,但是李四的行锁被事务二占据,所以事务一只能等待事务二完成之后释放李四这行的行锁才能记录执行;而事务二此时需要更新张三的Corp数据,也需要持有张三这行的行锁,但是这个行锁被事务一占据,同样,事务二需要等待事务一执行完成之后才能继续执行。
这样就发生了事务一和事务二相互等待导致两个事务都无法执行下去的现象。

> 死锁: 指`两个`或者`两个以上`的事务在执行过程中,因`争夺锁资源`而造成的一种`互相等待`的现象。
#### 4.2.2 死锁产生的必要条件
1. 互斥(无法避免)
* 定义:并发执行的事务为了进行必要的隔离保证执行正确,在事务结束前,需要对修改的数据库记录持锁,保证多个事务对相同数据库记录串行修改。
* 对于大型并发系统无法避免。

2. 请求和保持(无法避免)
* 定义:一个事务需要申请多个资源,并且已经持有一个锁资源,在等待另一个锁资源
* 死锁仅发生在请求两个或者连个以上的锁对象
* 由于应用需要,难以消除

3. 不剥夺
* 定义:已经获得锁资源的事务,在未执行前,不能被其他事务强制剥夺,只能等本事务使用完时,由事务自己释放。
* 一般用于已经出现死锁时,通过破坏该条件达到解除死锁的目的

4. 环路等待
<p align="center">
<img src="/img/JDBC/环路等待.png" alt="环路等待">
</p>

* 定义:发生死锁时,必然存在一个事务——锁的环形链。
* 按照统一顺序获得锁,可以破坏该条件。
* 通过分析死锁事务之间的锁竞争关系,调整SQL的顺序,达到消除死锁的目的。

#### 4.2.3 MySQL中的锁
<p align="center">
<img src="/img/JDBC/MySQL中的锁.png" alt="MySQL中的锁">
</p>

X:排他锁:跟其他任何锁都是冲突的
S:共享锁:多个事务可以共享一把锁,多个锁可以兼容

#### 4.2.4 加锁方式
1. 外部加锁
* 由应用程序添加,锁依赖关系系比较容易分析
* 共享锁(S):select * from table lock in share mode
* 排他锁(X):select * from table for update

2. 内部加锁
* 为了实现ACID特性,由数据库系统内部自动添加
* 加锁规则繁琐,与SQL执行计划、事务隔离级别、表索引结构有关

#### 4.2.5 哪些SQL需要持有锁
1. 快照读
* Innodb实现了多版本控制(MVCC),支持不加锁快照度
* select * from table where ……
* 能够保证同一个Select结果集是一致的
* 不能保证同一个事务内部,Select语句和其他语句的数据一致性,如果业务需要,需通过外部显式加锁。

2. 当前读
* Select * from table lock in share mode
* Select * from table for update
* Update form table set ……
* Insert into ……
* Delete from table ……

#### 4.2.6 SQL加锁分析
| ID | UserName | Account | Corp |
| :-- | :------ | :------ | :--- |
| 1 | ZhangSan | 100 | Ali |
| 2 | LiSi | 0 | Ali |

```mysql
Update user set account = 0 where id = 1
```
这个Update操作就是直接在记录上加排他锁,此时如果是Select操作,是不会被阻塞的。因为是快照读,但是如果是`Select for update`或者`Select for share mode`的方式,都是会被阻塞的。
```mysql
Select UserName from user where id=1 in share mode
```
当然,如果执行`Select for share mode`这条语句的话,是对行记录加了一个共享锁。此时如果其他事务要执行`Select for share mode`的话,对同一行记录还是可以执行的,不会被阻塞。但是如果外部其他事务要执行一个`Select for update`的话,则一定会被阻塞。

#### 4.2.7 分析死锁的常用方法
MySQL数据库会自动的检测死锁并强制回滚代价最小的事务,这个不需要开发人员关心。死锁的解除,MySQL会自动的帮助我们去做,但是我们需要在死锁解除之后去分析死锁产生的SQL语句,避免死锁再次产生。
捕获死锁的命令:
```mysql
show engine innodb status
```
执行完这个命令后,会有一大段命令出现。其中有一个部分是包括死锁的,里面会列出发生死锁时,所等待的两个SQL语句,然后也会列出系统强制回滚的是哪个事务。知道了这些SQL语句,可以分析SQL语句的加锁方式,来调整SQL语句的顺序,或者改变SQL语句来保证按序获取锁资源,这样就可以有效避免死锁的产生。

## 5. MyBatis

Binary file added img/JDBC/MySQL中的锁.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/JDBC/不可重复读.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/JDBC/幻读.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/JDBC/环路等待.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/JDBC/脏读.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added img/JDBC/隔离性.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 43 additions & 0 deletions sql/5. 事务/cloud_study.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
Navicat Premium Data Transfer
Source Server : local
Source Server Type : MySQL
Source Server Version : 50719
Source Host : localhost:3306
Source Schema : cloud_study
Target Server Type : MySQL
Target Server Version : 50719
File Encoding : 65001
Date: 31/08/2017 20:26:56
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userName` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`sex` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL,
`account` int(11) DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'ZhangSan', '0', '123456', 100);
INSERT INTO `user` VALUES (2, 'LiSi', '0', '123456', 0);
INSERT INTO `user` VALUES (3, 'ZhaoWu', '0', '123456', 0);
INSERT INTO `user` VALUES (4, 'ZhangSi', '0', '123456', 0);
INSERT INTO `user` VALUES (5, 'LiSan', '0', '123456', 0);
INSERT INTO `user` VALUES (6, 'GuoYi', '0', '123456', 0);

SET FOREIGN_KEY_CHECKS = 1;
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package com.micro.profession.jdbc.practice.Transaction;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;

import org.apache.commons.dbcp2.BasicDataSource;

public class TransactionTest {

public static BasicDataSource ds = null;

static final String DRIVER_NAME = "com.mysql.jdbc.Driver";
static String DB_URL = "jdbc:mysql://localhost/cloud_study?useSSL=true";
static final String USER_NAME = "root";
static final String PASSWORD = "root";

public static void transactionInit() {
ds = new BasicDataSource();
ds.setUrl(DB_URL);
ds.setDriverClassName(DRIVER_NAME);
ds.setUsername(USER_NAME);
ds.setPassword(PASSWORD);
}

public static void transferAccount() throws ClassNotFoundException {
Connection conn = null;
PreparedStatement ptmt = null;
try {
conn = ds.getConnection();
conn.setAutoCommit(false);
ptmt = conn.prepareStatement("update user set account = ? where userName = ?");
ptmt.setInt(1, 0);
ptmt.setString(2, "ZhangSan");
ptmt.execute();
ptmt.setInt(1, 100);
ptmt.setString(2, "LiSi");
ptmt.execute();
conn.commit();
} catch (SQLException e) {
if(conn != null)
try {
conn.rollback();
} catch (SQLException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
}
} finally {
try {
if(conn != null) conn.close();
if(ptmt != null) ptmt.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) throws ClassNotFoundException {
transactionInit();
new TransactionTest().transferAccount();
}

}
Loading

0 comments on commit 8012a78

Please sign in to comment.