事务的概念

  1. 事务 可以理解为一个 独立的工作单元, 在这个独立的工作单元中, 有一组操作; 放在事务(独立工作单元)中的多个操作, 要么全部执行成功, 要么全部执行失败。
  2. 不免俗套, 这还是通过最经典的银行转账应用来解释一下

    • 假设有两个角色 'Iron Man'(余额500), 'Wolverine'(余额15), 现在 Iron Man 通过该银行应用给 Wolverine 转账100元, 那么本次转账操作至少需要三个步骤:

      检查`Iron Man`余额`>=100`元
      从`Iron Man`余额中`-100`元
      给`Wolverine`余额`+100`元
    • 注意: 上面的三个步骤的操作必须打包在一个事务中, 从而可以作为一个 独立的工作单元 来执行。在这个 独立工作单元(即事务) 中的这三个操作, 只要有任何一个操作失败, 则事务就整体就是失败的, 那就必须回滚所有的步骤。
    • 假设第二步操作成功, 但是第三步操作失败, 那么整个事务也就应该是失败的, 那就必须将第二步的操作也回滚。(到这里我们也看到了事务最基本的特性之一: 保证数据的一致性)
  3. 要知道, 在真实的高并发场景下, 事务需要做的事情其实很多很多, 因为高并发会出现很多意想不到的问题, 接下来会分析这些问题。

事务的ACID特性

在分析高并发事务的问题前, 我们要先知道事务的几个标准特性, 因为一个运行良好的事务处理系统必须具备这些标准特性, 而且这些问题的解决离不开事务的这几个标准特性!!!

  1. Atomicity 原子性
    一个事务必须被视为一个不可分割的最小工作单元, 整个事务中的所有操作要么全部提交成功, 要么全部失败回滚。对于一个事务来说, 不能只成功执行其中的一部分操作, 这就是事务的原子性。
  2. Consistency 一致性
    虽然可数据表中的数据可能一直在变化, 但是事务的一致性特性会保证 数据库总是从一个一致性的状态 转换到 另一个一致性的状态;

    比如在之前的转账例子:

    转账前的一致性状态是: 'Iron Man'(余额500), 'Wolverine'(余额15)
    转账成功后的一致性状态是: 'Iron Man'(余额400), 'Wolverine'(余额115)
    转账如果失败的话, 一致性的状态应该回滚到转账前的状态: 'Iron Man'(余额500), 'Wolverine'(余额15)
  3. Isolation 隔离性

    • 通常来说, 一个事务所做的修改在最终提交以前, 对其他事务是不可见的;
      比如在之前的转账例子中, 在执行完成第二步, 但是第三步还没开始的时候, 此时有另一个账户汇总的程序开始运行, 那么这个程序所拿到的A账户余额应该是没有被 -100 的余额才对
    • 后面我们还会详细讨论事务隔离性的 隔离级别, 到时候就知道这里为什么说通常来说对其他事务是不可见的; (也就是还有特例, 比如最低隔离级别 READ UNCOMMITTED, 对其他事务的可见就造成了脏读问题的出现)
    • 事务有四种隔离级别(从低到高: READ UNCOMMITTEDREAD COMMITTEDREPEATABLE READSERIALIZABLE)
  4. Durability 持久性

    一旦事务被最终提交, 则在事务这个独立单元中的所有操作所做的修改将会 `永久保存到数据库中`; (这里所说的`永久`应该可以理解为 被事务修改的数据 是真正存放到了表中, 而不是存放在了诸如临时表之类的地方)
    

高并发事务的问题

在并发量比较大的时候, 很容易出现 多个事务同时进行 的情况。假设有两个事务正在同时进行, 值得注意的是: 它们两者之间是互相不知道对方的存在的, 各自都对自身所处的环境过分乐观, 从而并没有对自己所操作的数据做一定的保护处理, 所以最终导致了一些问题的出现;这些问题归结为5类,包括3类数据读问题(脏读、不可重复读、幻象读)以及两类数据更新问题(第一类丢失更新问题和第二类更新丢失问题)
接下来, 在分析高并发事务的问题时, 你可能已经了解了一些关于锁的概念, 但是在分析这些问题的时候, 先不要带入锁的概念, 本小节只会列出问题, 并直接告诉你各个问题是使用事务隔离性的哪个隔离级别解决掉的, 锁是解决方案, 如果带入锁的概念, 是无法去分析这些问题的。所以本节不需要带入!
[下一篇文章]()将会分析这些解决方案(各隔离级别)具体是如何解决问题的。

脏读

  1. 如果mysql中一个事务A读取了另一个并行事务B未最终提交的写数据, 那事务A的这次读取就是脏读。(因为事务A读取的是'脏数据', 是'非持久性'的数据)

    • 之所以说是'非持久性数据', '脏数据', 是因为事务B最终可能会因为内部其他后续操作的失败或者系统后续突然崩溃等原因, 导致事务最终整体提交失败, 那么事务A此时读取到的数据在表中其实会被回滚, 那事务A拿到的自然就是脏的数据了。
    • 图示:
  2. 事务A在T4阶段读取库存为20, 这个库存其实就属于脏数据, 因为事务B最终会回滚这个数据, 所以如果事务A使用库存20进行后续的操作, 就会引发问题, 因为事务A拿到的数据已经和表中的真实数据不一致了。
  3. 那么这个问题如何解决呢?
    在MySQL中, 其实事务已经用自身特性(隔离性的 -- READ COMMITED或以上隔离级别)解决了这个问题;

    **`READ COMMITED`级别保证了, 只要是当前语句执行前已经提交的数据都是可见的**。注意和`REPEATABLE READ`级别的区!!!

不可重复读

  1. 假设现在上面的 脏读问题 已经被完全解决了, 那就意味着事务中每次读取到的数据都是 持久性 的数据(被别的事务最终 提交/回滚 完成后的数据)。
  2. 但是你还需要知道的是: 解决了脏读问题, 只是能保证你在事务中每次读到的数据都是持久性的数据而已!!!!
  3. 如果在一个事务中多次读取同一个数据, 正好在两次读取之间, 另外一个事务确实已经完成了对该数据的修改并提交, 那问题就来了: 可能会出现多次读取结果不一致的现象。
  4. 那么这个问题如何解决呢?
    在MySQL中, 其实事务已经用自身特性(隔离性的 -- REPEATABLE READ或以上隔离级别)解决了这个问题;
    REPEATABLE READ级别保证了, 只要是当前事务执行前已经提交的数据都是可见的。注意和READ COMMITED级别的区!!!

测试  不可重复读,将客户端A的隔离属性设置为read commited 读已提交的,作用域为session当前会话。

B客户端为可重复读repeatable-read

(不可重复读 是针对同一条记录的更改与删除,导致两次查询的数据导致不一致。

此实验证明了,事务隔离属性为可重复读的时候,保证了同一个事务的多次查询同条记录是一致的。而事务隔离属性为读已提交的时候,会导致两次读取的数据行记录不一致情况。

注意:为了避免不可重复读问题,防止读到更改的数据,只需要对操作的数据添加行级锁,阻止操作中数据的变化,采用了MVCC(Multiversion Concurrency Control)多版本并发控制来解决的。

mysql中,默认的事务隔离级别是可重复读(repeatable-read),为了解决不可重复读,innodb采用了mvcc(多版本并发控制)来解决这一问题

mvcc是利用在每条数据后面加了隐藏的两列(创建版本号和删除版本号),每个事务在开始的时候都会有一个递增的版本号

新增:insert into user (id,name,age)values(1,"张三",10);

id    name    age    create_version    delete_version
1    张三    10    1     
 

更新:update user set age = 11 where id = 1;

更新操作采用delete+add的方式来实现,首先将当前数据标志为删除

id    name    age    create_version    delete_version
1    张三    10    1    2
 

然后新增一条新的数据:

id    name    age    create_version    delete_version
1    张三    10    1    2
1    张三    11    2     
 

删除:删除操作是直接将数据的删除版本号更新为当前事务的版本号

delete from user where id = 1;

id    name    age    create_version    delete_version
1    张三    11    2    3
 

查询操作:

select * from user where id = 1;

查询操作为了避免查询到旧数据或已经被其他事务更改过的数据,需要满足如下条件:

1、查询时当前事务的版本号需要大于或等于创建版本号

2、查询时当前事务的版本号需要小于删除的版本号

即:create_version <=  current_version  <  delete_version

这样就可以避免查询到其他事务修改的数据

以下是来自《高性能MySQL》摘抄

幻读 (间隙锁) 出现的场景一般发生在计算统计的时候

  1. 由于很多人(当然也包括本人), 容易搞混 不可重复读 和 幻读, 这两者确实非常相似。

    • 但 不可重复读 主要是说多次读取一条记录, 发现该记录中某些列值被修改过。
    • 而 幻读 主要是说多次读取一个范围内的记录(包括直接查询所有记录结果或者做聚合统计), 发现结果不一致(标准档案一般指记录增多, 记录的减少应该也算是幻读)。
  2. 其实对于 幻读, MySQL的InnoDB引擎默认的RR级别已经通过MVCC自动帮我们解决了, 所以该级别下, 你也模拟不出幻读的场景; 退回到 RC 隔离级别的话, 你又容易把幻读不可重复读搞混淆, 所以这可能就是比较头痛的点吧!
    具体可以参考《高性能MySQL》对 RR 隔离级别的描述, 理论上RR级别是无法解决幻读的问题, 但是由于InnoDB引擎的RR级别还使用了MVCC, 所以也就避免了幻读的出现!

幻读的延伸

MVCC虽然解决了幻读问题, 但严格来说只是解决了部分幻读问题, 接下来进行演示:

1.打开客户端1查看隔离级别及初始数据

mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)mysql> 

2.打开客户端2查看隔离级别及初始数据

mysql> SELECT @@SESSION.tx_isolation;
+------------------------+
| @@SESSION.tx_isolation |
+------------------------+
| REPEATABLE-READ        |
+------------------------+
1 row in set (0.00 sec)mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)mysql> 

3.在客户端2中开启事务, 然后查询数据

mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)mysql> 

4.在客户端1中插入一条id为4的新数据 (直接自动提交)

mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
Query OK, 1 row affected (0.00 sec)
mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
|  4 | 死侍    |  18 |      0 | A bad boy          |
+----+-----------+-----+--------+--------------------+
4 rows in set (0.00 sec)mysql> 

5.在客户端2事务中再次查询数据, 发现数据没有变化(表示可以重复读, 并且克服了幻读)!! 但是在客户端2事务中插入一条id为4的新数据, 发现提示数据已经存在!!!

mysql> begin;
Query OK, 0 rows affected (0.00 sec)mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)mysql> select * from test_transaction;
+----+-----------+-----+--------+--------------------+
| id | user_name | age | gender | desctiption        |
+----+-----------+-----+--------+--------------------+
|  1 | 金刚狼 | 127 |      1 | 我有一双铁爪 |
|  2 | 钢铁侠 | 120 |      1 | 我有一身铁甲 |
|  3 | 绿巨人 |   0 |      2 | 我有一身肉    |
+----+-----------+-----+--------+--------------------+
3 rows in set (0.00 sec)mysql> insert into test_transaction (`id`,`user_name`,`age`,`gender`,`desctiption`) values (4, '死侍', 18, 0, 'A bad boy');
1062 - Duplicate entry '4' for key 'PRIMARY'
mysql> //并且, 此时`update/delete`也是可以操作这条在事务中看不到的记录的!

6.那么这是什么问题呢?

  • 可以参考MySQL官方文档 -- 一致性非阻塞读

The snapshot of the database state applies to SELECT statements within a transaction, not necessarily to DML statements. If you insert or modify some rows and then commit that transaction, a DELETE or UPDATE statement issued from another concurrent REPEATABLE READ transaction could affect those just-committed rows, even though the session could not query them. If a transaction does update or delete rows committed by a different transaction, those changes do become visible to the current transaction. 
个人认为应该翻译为: 数据库状态的快照适用于事务中的SELECT语句, 而不一定适用于所有DML语句。 如果您插入或修改某些行, 然后提交该事务, 则从另一个并发REPEATABLE READ事务发出的DELETE或UPDATE语句就可能会影响那些刚刚提交的行, 即使该事务无法查询它们。 如果事务更新或删除由不同事务提交的行, 则这些更改对当前事务变得可见。

7.不少资料将MVCC并发控制中的读操作可以分成两类: 快照读 (snapshot read) 与 当前读 (current read)

- 快照读, 读取专门的快照 (对于RC,快照(ReadView)会在每个语句中创建。对于RR,快照是在事务启动时创建的)
```
简单的select操作即可(不需要加锁,如: select ... lock in share mode, select ... for update)
```
针对的也是select操作- 当前读, 读取最新版本的记录, 没有快照。 在InnoDB中,当前读取根本不会创建任何快照。
```
select ... lock in share mode
select ... for update
```
针对如下操作, 会让如下操作阻塞:
```
insert
update
delete
```
- 在RR级别下, 快照读是通过MVVC(多版本控制)和undo log来实现的, 当前读是通过手动加record lock(记录锁)和gap lock(间隙锁)来实现的。所以从上面的显示来看,如果需要实时显示数据,还是需要通过加锁来实现。这个时候会使用next-key技术来实现。

为了模拟出幻读情形,采用read-commited的事务隔离属性

针对幻读,防止读到新增的数据,往往是通过添加表级锁---将整张表锁定,防止新增数据。

更新丢失

  1. 最后聊一下高并发事务的另一个问题 -- 丢失更新问题, 该问题和之前几个问题需要区分开, 因为解决方案不是一类!
  2. 第一类丢失更新: A事务撤销时, 把已经提交的B事务的更新数据覆盖了。

    不过, 通过后面MVCC相关文章最后的小结你会了解到, 这类更新丢失问题是不会出现的, 因为InnoDB存储引擎的隔离级别都使用了排他锁, 即使是 MVCC也不是纯MVCC, 也用到了排他锁! 这样的话事务A在未完成的时候, 其他事务是无法对事务A涉及到的数据做修改并提交的。
  3. 第二类丢失更新: A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失。
  4. 此类更新丢失问题, 无法依靠前三种隔离级别来解决, 只能用最高隔离级别 Serializable 或者手动使用乐观锁悲观锁来解决。
  5. 最高隔离级别Serializable在实际应用场景中并不被采用, 对于手动使用乐观锁悲观锁的方案, 将会在以后关于锁的文章中一并给出!

8.当然, 使用隔离性的最高隔离级别SERIALIZABLE也可以解决幻读, 但该隔离级别在实际中很少使用!

高并发事务问题以及解决方案相关推荐

  1. 为什么Actor模型是高并发事务的终极解决方案?

    首先看看道友提出的一个问题: 用户甲的操作 1.开始事务 2.访问表A 3.访问表B 4.提交事务 乙用户在操作 1.开始事务 2.访问表B 3.访问表A 4.提交事务 如果甲用户和乙用户的两个事务同 ...

  2. MySQL高并发事务问题

    事务的概念 事务 可以理解为一个 独立的工作单元, 在这个独立的工作单元中, 有一组操作; 放在事务(独立工作单元)中的多个操作, 要么全部执行成功, 要么全部执行失败. 不免俗套, 这还是通过最经典 ...

  3. 高并发系统设计——分布式锁解决方案

    摘要 分布式应用进行逻辑处理时经常会遇到并发问题.比如一个操作要修改用户的状态,修改状态需要先读出用户的状态, 在内存里进行修改,改完了再存回去.如果这样的操作同时进行了,就会出现并发问题, 因为读取 ...

  4. MySQL - 并发事务问题及解决方案

    文章目录 生猛干货 Pre 脏读 不可重复读 幻读 Solutions 搞定MySQL 生猛干货 带你搞定MySQL实战,轻松对应海量业务处理及高并发需求,从容应对大场面试 Pre MySQL - 多 ...

  5. mysql高并发和大流量_高并发-高并发和大流量解决方案

    高并发架构相关概念 并发 并发,在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,并且这几个程序都是在同一个处理机上运行,担任一个时刻点上只有一个程序在处理机上运行. 我们所说的 ...

  6. 高并发和大流量解决方案

    #高并发架构相关概念# 并发: 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行,但任意一个时刻上只有一个程序在处理机上运行. 我们说的高并 ...

  7. 高并发与大流量解决方案

    1.相关概念 1.1.并发与并行 1.1.1.并发(Concurrent) 在操作系统中,是指一个时间段中有几个程序都处于已启动运行到运行完毕之间,且这几个程序都是在同一个处理机上运行 就想前面提到的 ...

  8. 零基础小白都能看懂的Java处理高并发的3种解决方案

    高并发是互联网应用的一大特色,也是互联网应用不可避免的问题;比如淘宝双11.京东618.12306春节火车票,促销.秒杀等等. 一.什么是高并发 高并发(High Concurrency)是互联网分布 ...

  9. 海量数据高并发的访问技术解决方案

    如今数据增长远远超过机器的计算性能,    ORACL RAC能满足百万千万的业务系统,  硬件成本太高, 垂直方向的发展不太现实,追求水平方向的发展, 使用普通商业服务器,多台协调完成计算 如今,能 ...

最新文章

  1. pip 安装指定版本
  2. Leetcode 剑指 Offer 58 - II. 左旋转字符串 (每日一题 20210830)
  3. 信息系统项目管理知识--项目整合管理
  4. 未到期的应收票据贴现时如何记账
  5. 移动Sql Server数据库的脚本
  6. eof怎么结束输入_SimRobot算法社第二次活动圆满结束啦!
  7. java加密证书生成_mkcert 1.3.0 发布,本地 HTTPS 加密证书生成工具
  8. [JAVA语法]怎样制作ear,war文件
  9. angular学习-入门基础
  10. Reeder的本地使用
  11. Java 的三种 Base64
  12. Tuxera NTFS2022产品密钥 mac读取ntfs格式驱动程序
  13. 前端拖拽时手型为禁用
  14. Spark 创建 hive表报错 ROW FORMAT DELIMITED is only compatible with 'textfile', not 'parquet'
  15. mysql大小写转换函数_MySQL字母大小写转换函数UPPER(s)、UCASE(s)、LOWER(s)和LCASE(s)
  16. incrby redis 最大值_redis incr incrby decr decrby命令
  17. 什么是X.509证书?X.509证书工作原理及应用?
  18. 加速新基建,优锘科技的数字孪生可视化运营平台迎来最好时代
  19. 小程序UI——样式的使用-CSS
  20. 【SVM】基于matlab的SVM支持向量机训练和测试仿真

热门文章

  1. 山外山通过注册:拟募资12亿 大健康与华盖信诚是股东
  2. kakadu——JPEG2000图像压缩软件的安装和使用
  3. ubuntu 剪辑视频/音频软件 ffmpeg
  4. iStat Menus for mac中文
  5. android ratingbar不可点击,Android评分控件RatingBar使用实例解析
  6. C++ 模板类的继承
  7. 菊花厂 vs 互联网公司
  8. animals中文谐音_animals中文谐音_张杰pretty white lies中文音译歌词
  9. 如何使用快捷键在网页中调用扫描仪进行扫描 - Dynamic Web TWAIN
  10. 蓝桥杯真题2017-2021