[MySQL]数据库中如何处理多事务

0.README
本文介绍了事务的基本概念,常用的四个命令,事务的四个特性,最后介绍多事务处理中的可能出现的三种问题,以及对应的四个隔离等级。

1.什么是事务
事务处理用来维护数据库的完整性,保证成批的MySQL操作要么完全执行,要么完全不执行。

比如,假设有两个表,账户表money,和支出表pay,那么每一笔支出记录对应就要从余额表中删除一定金额,如果pay插入成功,money修改失败,那么就会造成数据不一致。

MySQL事务操作:

mysql> SELECT * FROM ordertotals;
+-----------+-----------------+-------------+
| order_num | total_not_taxed | total_taxed |
+-----------+-----------------+-------------+
|     20005 |          149.87 |      222.00 |
|     20009 |           38.47 |       40.78 |
|     20006 |           55.00 |       58.30 |
|     20007 |         1000.00 |     1060.00 |
|     20008 |          125.00 |      132.50 |
|     20008 |          125.00 |      132.50 |
|     11111 |          100.00 |      125.00 |
|     11111 |          100.00 |      125.00 |
+-----------+-----------------+-------------+
8 rows in set (0.00 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> DELETE FROM ordertotals WHERE order_num = 11111;
Query OK, 2 rows affected (0.00 sec)

mysql> SELECT * FROM ordertotals;
+-----------+-----------------+-------------+
| order_num | total_not_taxed | total_taxed |
+-----------+-----------------+-------------+
|     20005 |          149.87 |      222.00 |
|     20009 |           38.47 |       40.78 |
|     20006 |           55.00 |       58.30 |
|     20007 |         1000.00 |     1060.00 |
|     20008 |          125.00 |      132.50 |
|     20008 |          125.00 |      132.50 |
+-----------+-----------------+-------------+
6 rows in set (0.00 sec)

mysql> ROLLBACK;
Query OK, 0 rows affected (0.01 sec)

mysql> SELECT * FROM ordertotals;
+-----------+-----------------+-------------+
| order_num | total_not_taxed | total_taxed |
+-----------+-----------------+-------------+
|     20005 |          149.87 |      222.00 |
|     20009 |           38.47 |       40.78 |
|     20006 |           55.00 |       58.30 |
|     20007 |         1000.00 |     1060.00 |
|     20008 |          125.00 |      132.50 |
|     20008 |          125.00 |      132.50 |
|     11111 |          100.00 |      125.00 |
|     11111 |          100.00 |      125.00 |
+-----------+-----------------+-------------+
8 rows in set (0.00 sec)

mysql>

MySQL中默认每个非事务语句都是自动提交的,可以通过 mysql> SET autocommit=0;来设置为不自动提交,这里的提交是相对于客户端程序而言的,未提交时,一个客户端程序仍可以查看到自己修改的内容,断开连接后修改的内容就失效,只有在断开连接前提交,才能在服务器产生永久性修改。

在开启事务之后,必须手动用COMMIT来提交事务。

2.事务的四个特性
原子性:原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。

一致性:事务必须使数据库从一个一致性状态变换到另外一个一致性状态。比如转账:支付前money+pay=2000;支付后money+pay2000。

隔离性:事务的隔离性是多个用户并发访问数据库时,数据库为每一个用户开启的事务,不能被其他事务的操作数据所干扰,多个并发事务之间要相互隔离。

持久性:指一个事务一旦被提交,它对数据库中数据的改变就是永久性的。

3.多事务的问题
当多个客户端线程执行数据库操作时,可能出现三种问题:

脏读:一个线程中的事务读到了另一个线程中事务未提交的数据。

不可重复读:一个线程中的事务读到了另一个线程中提交的update的数据,前后两次读到的内容,即:事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。

      那么MySQL可重复读是如何实现的呢?

      使用的的一种叫MVCC的控制方式 ,即Mutil-Version Concurrency Control,多版本并发控制,类似于乐观锁的一种实现方式

虚读:一个线程中的事务读到了另一个线程中提交的insert或delete的数据,前后读到的记录条数不一致。

4.事务的隔离级别
为了解决这些问题,数据库管理软件采用了一些·办法设置了事务的隔离等级。隔离等级越高,数据越安全,但性能越低。

 MySQL查看当前数据库的隔离级别命令是: 
  mysql> SELECT @@tx_isolation; 
更改隔离级别(开启事务之前更改): 
  mysql> SET TRANSACTION ISOLATION LEVEL [level_name]

客户端1:

BEGIN;
SELECT score FROM a_table;
UPDATE a_table SET score = score - 3 WHERE a_id= 2;
SELECT SLEEP(20);
COMMIT;

客户端2:

BEGIN;
SELECT score FROM a_table;
UPDATE a_table SET score = score - 2 WHERE a_id= 2;
COMMIT;

客户端1先执行,然后客户端2在执行。客户端2会等待客户端1执行完毕后在再行,结果正确。

https://blog.csdn.net/weixin_40255793/article/details/79735665

参考文献:


[1] [MySQL]对于事务并发处理带来的问题,脏读、不可重复读、幻读的理解 
[2] 《MySQL必知必会》

Innodb中的MVVC

MVVC

  1. MySQL的大多数事务型存储引擎实现的其实都不是简单的行级锁。基于提升并发性能的考虑, 它们一般都同时实现了多版本并发控制(MVCC)。不仅是MySQL, 包括Oracle,PostgreSQL等其他数据库系统也都实现了MVCC, 但各自的实现机制不尽相同, 因为MVCC没有一个统一的实现标准。
  2. 可以认为MVCC是行级锁的一个变种, 但是它在很多情况下避免了加锁操作, 因此开销更低。虽然实现机制有所不同, 但大都实现了非阻塞的读操作,写操作也只锁定必要的行。
  3. MVCC的实现方式有多种, 典型的有乐观(optimistic)并发控制 和 悲观(pessimistic)并发控制。
  4. MVCC只在 READ COMMITTED 和 REPEATABLE READ 两个隔离级别下工作。其他两个隔离级别够和MVCC不兼容, 因为 READ UNCOMMITTED 总是读取最新的数据行, 而不是符合当前事务版本的数据行。而 SERIALIZABLE 则会对所有读取的行都加锁。

read view和快照snapshot

  1. 事务快照是用来存储数据库的事务运行情况。一个事务快照的创建过程可以概括为:
    • 查看当前所有的未提交并活跃的事务,存储在数组中
    • 选取未提交并活跃的事务中最小的XID,记录在快照的xmin中
    • 选取所有已提交事务中最大的XID,加1后记录在xmax中
  2. read view 主要是用来做可见性判断的
  3. 对于read view快照的生成时机, 也非常关键, 正是因为生成时机的不同, 造成了RC,RR两种隔离级别的不同可见性;
    1. 在innodb中(默认repeatable read级别), 事务在begin/start transaction之后的第一条select读操作后, 会创建一个快照(read view), 将当前系统中活跃的其他事务记录记录起来;
    2. 在innodb中(默认repeatable committed级别), 事务中每条select语句都会创建一个快照(read view);

相关概念

  1. undo-log
    undo log是为回滚而用,具体内容就是copy事务前的数据库内容(行)到undo buffer,在适合的时间把undo buffer中的内容刷新到磁盘。undo buffer与redo buffer一样,也是环形缓冲,但当缓冲满的时候,undo buffer中的内容会也会被刷新到磁盘;与redo log不同的是,磁盘上不存在单独的undo log文件,所有的undo log均存放在主ibd数据文件中(表空间),即使客户端设置了每表一个数据文件也是如此。
  2. InnoDB存储引擎在数据库每行数据的后面添加了三个字段
  • 6字节的事务ID(DB_TRX_ID)字段: 用来标识最近一次对本行记录做修改(insert|update)的事务的标识符, 即最后一次修改(insert|update)本行记录的事务id。
    至于delete操作,在innodb看来也不过是一次update操作,更新行中的一个特殊位将行表示为deleted, 并非真正删除。
  • 7字节的回滚指针(DB_ROLL_PTR)字段: 指写入回滚段(rollback segment)的 undo log record (撤销日志记录记录)。
    如果一行记录被更新, 则 undo log record 包含 ‘重建该行记录被更新之前内容’ 所必须的信息。
  • 6字节的DB_ROW_ID字段: 包含一个随着新行插入而单调递增的行ID, 当由innodb自动产生聚集索引时,聚集索引会包括这个行ID的值,否则这个行ID不会出现在任何索引中。
    结合聚簇索引的相关知识点, 我的理解是, 如果我们的表中没有主键或合适的唯一索引, 也就是无法生成聚簇索引的时候, InnoDB会帮我们自动生成聚集索引, 但聚簇索引会使用DB_ROW_ID的值来作为主键; 如果我们有自己的主键或者合适的唯一索引, 那么聚簇索引中也就不会包含 DB_ROW_ID 了 。

当前读和快照读

  1. MySQL的InnoDB存储引擎默认事务隔离级别是RR(可重复读), 是通过 “行排他锁+MVCC” 一起实现的, 不仅可以保证可重复读, 还可以部分防止幻读, 而非完全防止;
  2. 为什么是部分防止幻读, 而不是完全防止?
  • 效果: 在如果事务B在事务A执行中, insert了一条数据并提交, 事务A再次查询, 虽然读取的是undo中的旧版本数据(防止了部分幻读), 但是事务A中执行update或者delete都是可以成功的!!
  • 因为在innodb中的操作可以分为当前读(current read)和快照读(snapshot read):
  1. 快照读
    简单的select操作(当然不包括 select … lock in share mode, select … for update)

  2. 当前读

    • select … lock in share mode
    • select … for update
    • insert
    • update
    • delete

    在RR级别下,快照读是通过MVVC(多版本控制)和undo log来实现的,当前读是通过加record lock(记录锁)和gap lock(间隙锁)来实现的。
    innodb在快照读的情况下并没有真正的避免幻读, 但是在当前读的情况下避免了不可重复读和幻读!!!

参考

InnoDB怎么解决幻读的?

https://segmentfault.com/a/1190000012650596

https://www.jianshu.com/p/7e967d291c24

 

MySQL不可重复读和幻读的区别

不可重复读:A事务在执行过程中,B事务对数据进行了修改或删除,导致A两次读取的数据不一致;
重点在于update和delete(锁行即可解决),
幻读:A事务在执行过程中,B事务新增了符合A事务要查询的数据,导致A两次读取的数据不一致;
重点在于insert(需要锁表解决)。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

 
 

mysql 关于 不可重复读与幻读的解决方案

 不可重复读和幻读的区别

当然,   从总的结果来看,   似乎两者都表现为两次读取的结果不一致.

但如果你从控制的角度来看,   两者的区别就比较大
对于前者,   只需要锁住满足条件的记录
对于后者,   要锁住满足条件及其相近的记录

-----------------------------------------------------------

我这么理解是否可以?
避免不可重复读需要锁行就行
避免幻影读则需要锁表

------------------------

####不可重复读和幻读的区别####
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。

如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复 读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会 发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。

所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。

####悲观锁和乐观锁####

  • 悲观锁

正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处 于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机 制,也无法保证外部系统不会修改数据)。

在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。

  • 乐观锁

相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。

而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如 果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。

原文地址:https://www.cnblogs.com/leijiangtao/p/4509490.html