06-事务&原理&隔离级别

image-20200812132737977

参考资料:MySQL事务的实现原理

事务的隔离级别

读未提交:一个事务可以看到其他事务未提交的修改。允许脏读

读已提交:一个事务能看到其他事务已经提交的修改。允许不可重复读和幻读

可重复读:保证同一个事务多次读取的数据是一致的。MySQL innoDB默认隔离级别,不会出现幻读

串行化:并发事务之间是串行化。读取需要获取共享锁,更新需要获取排它锁————最高隔离级别

  • 脏读:一个事务读到了另一个事务未提交的数据
  • 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样
  • 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同(InnoDB实现的RR通过next-key lock机制避免了幻读现象)

image-20230605230001930

MySQL 默认隔离级别 RR(可重复读)解决脏读、不可重复读、幻读等问题,使用的是MVCC:

​ MVCC全称Multi-Version Concurrency Control,即多版本的并发控制协议,在同一时刻,不同的事务读取到的数据可能是不同的(即多版本)。

​ MVCC最大的优点是读不加锁,因此读写不冲突,并发性能好。

InnoDB实现的RR,通过锁机制、数据的隐藏列、undo log和类next-key lock,实现了一定程度的隔离性,可以满足大多数场景的需要。

脏读、不可重复读和幻读

首先来看并发情况下,读操作可能存在的三类问题:

  • 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据(脏数据),这种现象是脏读。

    举例如下(以账户余额表为例)

    image-20230605224859307

  • 不可重复读:在事务A中先后两次读取同一个数据,两次读取的结果不一样,这种现象称为不可重复读。

    脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。

    举例如下:

    image-20230605225016694

  • 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。

    不可重复读与幻读的区别可以通俗的理解为:前者是数据变了,后者是数据的行数变了。

    举例如下:

    image-20230605225103930

1.1 模拟转账

生活当中转账是转账方账户扣钱,收账方账户加钱。我们用数据库操作来模拟现实转账。

1.1.1 数据库模拟转账

1
2
3
4
5
6
7
8
9
10
11
12
# 模拟转账
CREATE TABLE account(
id INT,
money DOUBLE
)CHARSET = utf8;
SELECT * FROM account;
INSERT INTO account(id, money) VALUES(1, 20000);
INSERT INTO account(id, money) VALUES(2, 2000);

# 账户1 转给 账户2 金额 5000,账户1 -5000,账户2 +5000
UPDATE account SET money = money - 5000 WHERE id = 1; # 可能出现错误,需要回滚
UPDATE account SET money = money + 5000 WHERE id = 2;
  • 上述代码完成了两个账户之间转账的操作。

1.1.2 模拟转账错误

1
2
3
4
5
6
7
# A 账户转账给 B 账户 1000 元。
# A 账户减 1000
UPDATE account SET MONEY = MONEY-1000 WHERE id=1;
# 断电、异常、出错...

# B 账户加 1000
UPDATE account SET MONEY = MONEY+1000 WHERE id=2;
  • 上述代码在减操作后过程中出现了异常或出错,会发现,减钱仍旧是成功的,而加钱失败了!
  • 注意:每条 SQL 语句都是一个独立的操作,一个操作执行完对数据库是永久性的影响

1.2 事务的概念

事务,是一个原子操作,是一个最小执行单元,可以由一个或多个SQL语句组成。在同一个事务当中,所有的SQL语句都成功执行时,整个事务成功,有一个SQL语句执行失败,整个事务都执行失败。

1.3 事务的起始

开始:上一个事务结束后的第一条增删改的语句,即事务的开始。

结束:

​ 1). 提交:

​ a. 显示提交:COMMIT;

​ b. 隐式提交:一条创建/删除的语句,正常退出(客户端退出连接);

​ 2). 回滚:

​ a. 显示回滚:ROLLBACK;

​ b. 隐式回滚:非正常退出(断电/宕机)执行了创建/删除语句,但是失败了,会为这个无效的语句执行回滚。

1.4 事务的原理

数据库会为每一个客户端都维护一个空间独立的缓存区(回滚段),一个事务中所有的增删改语句的执行结果都会缓存在回滚段中,只有当事务中所有 SQL 语句均正常结束(commit),才会将回滚段中的数据同步到数据库。否则无论因为哪种原因失败,整个事务将回滚(rollback)。

1.5 事务的特性(ACID)

  • Atomicity(原子性)

  表示一个事务内的所有操作是一个整体,要么全部成功,要么全部失败。

  • Consistency(一致性)

  表示一个事务内有一个操作失败时,所有的更改过的数据都必须回滚到修改前状态

  • Isolation(隔离性)

  事务查看数据操作时数据所处的状态,要么是另一并发事务修改它之前的状态,要么是另一事务修改它之后的状态,事务不会查看中间状态的数据

  • Durability(持久性)

  持久性事务完成之后,它对于系统的影响是永久性的

1.6 事务应用

应用环境:基于增删改语句的操作结果(均返回操作后受影响的行数)可通过程序逻辑手动控制事务提交或回滚。

1.6.1 事务完成转账

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 账户1 给 账户2 转账(开启事务方式任选其一)
# 1.开启事务 方式一:
START TRANSACTION;
# 1.开启事务 方式二: (autoCommit = 0 关闭自动提交,1 开启自动提交)
SET autoCommit = 0;

UPDATE account SET money = money - 1000 WHERE id = 1;
UPDATE account SET money = money + 1000 WHERE id = 2;

# 2.事务内数据操作语句
UPDATE ACCOUNT SET MONEY = MONEY - 1000 WHERE ID = 1;
UPDATE ACCOUNT SET MONEY = MONEY + 1000 WHERE ID = 2;
# 3.事务内语句都成功了,执行 COMMIT
COMMIT;
# 4.事务内如果出现错误,执行 ROLLBACK;
ROLLBACK;
  • 注意:开启事务后,执行的语句均属于当前事务,成功再执行 COMIIT,失败要进行 ROLLBACK

1.7 多线程事务回滚

使用 sqlSession 控制手动提交事务

1.7.1 事务回滚示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
 @Resource
SqlContext sqlContext;

/**
* 测试多线程事务.
* @param employeeDOList
*/
@Override
public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException {
// 获取数据库连接,获取会话(内部自有事务)
SqlSession sqlSession = sqlContext.getSqlSession();
Connection connection = sqlSession.getConnection();
try {
// 设置手动提交
connection.setAutoCommit(false);
//获取mapper
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
//先做删除操作
employeeMapper.delete(null);
//获取执行器
ExecutorService service = ExecutorConfig.getThreadPool();
List<Callable<Integer>> callableList = new ArrayList<>();
//拆分list
List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5);
AtomicBoolean atomicBoolean = new AtomicBoolean(true);
for (int i =0;i<lists.size();i++){
if (i==lists.size()-1){
atomicBoolean.set(false);
}
List<EmployeeDO> list = lists.get(i);
//使用返回结果的callable去执行,
Callable<Integer> callable = () -> {
//让最后一个线程抛出异常
if (!atomicBoolean.get()){
throw new ServiceException("001","出现异常");
}
return employeeMapper.saveBatch(list);
};
callableList.add(callable);
}
//执行子线程
List<Future<Integer>> futures = service.invokeAll(callableList);
for (Future<Integer> future:futures) {
//如果有一个执行不成功,则全部回滚
if (future.get()<=0){
connection.rollback();
return;
}
}
connection.commit();
System.out.println("添加完毕");
}catch (Exception e){
connection.rollback();
log.info("error",e);
throw new ServiceException("002","出现异常");
}finally {
connection.close();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<insert id="saveBatch" parameterType="List">
INSERT INTO
employee (employee_id,age,employee_name,birth_date,gender,id_number,creat_time,update_time,status)
values
<foreach collection="list" item="item" index="index" separator=",">
(
#{item.employeeId},
#{item.age},
#{item.employeeName},
#{item.birthDate},
#{item.gender},
#{item.idNumber},
#{item.creatTime},
#{item.updateTime},
#{item.status}
)
</foreach>
</insert>

测试结果:抛出异常

image-20230420144232857

数据库中的数据:删除操作的数据回滚了,数据库中的数据依旧存在,说明事务成功了。

1.7.2 事务提交示例

成功执行操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Resource
SqlContext sqlContext;

/**
* 测试多线程事务.
* @param employeeDOList
*/
@Override
public void saveThread(List<EmployeeDO> employeeDOList) throws SQLException {
//获取数据库连接,获取会话(内部自有事务)
SqlSession sqlSession = sqlContext.getSqlSession();
Connection connection = sqlSession.getConnection();
try {
//设置手动提交
connection.setAutoCommit(false);
EmployeeMapper employeeMapper = sqlSession.getMapper(EmployeeMapper.class);
//先做删除操作
employeeMapper.delete(null);
ExecutorService service = ExecutorConfig.getThreadPool();
List<Callable<Integer>> callableList = new ArrayList<>();
List<List<EmployeeDO>> lists=averageAssign(employeeDOList, 5);
for (int i =0;i<lists.size();i++){
List<EmployeeDO> list = lists.get(i);
Callable<Integer> callable = () -> employeeMapper.saveBatch(list);
callableList.add(callable);
}
//执行子线程
List<Future<Integer>> futures = service.invokeAll(callableList);
for (Future<Integer> future:futures) {
if (future.get()<=0){
connection.rollback();
return;
}
}
connection.commit();
System.out.println("添加完毕");
}catch (Exception e){
connection.rollback();
log.info("error",e);
throw new ServiceException("002","出现异常");
//throw new ServiceException(ExceptionCodeEnum.EMPLOYEE_SAVE_OR_UPDATE_ERROR);
}
}

测试结果:

image-20230420144323377

数据库中数据:删除的删除了,添加的添加成功了,测试成功。


06-事务&原理&隔离级别
https://janycode.github.io/2017/06/18/05_数据库/01_MySQL/06-事务&原理&隔离级别/
作者
Jerry(姜源)
发布于
2017年6月18日
许可协议