@
n0099 https://github.com/n0099/TiebaMonitor/issues/32#issuecomment-1404661123> 所以对事务的原子性有一定的违背吧? 为了避免这种违背原子性和隔离的情形
事务的所谓`atomic`只是说 要么所有语句都成功 要么都失败
因此单个事务中复数个 DML 所造成的影响 要么都会生效 要么都不生效
https://github.com/n0099/TiebaMonitor/issues/32#issuecomment-1403243840> 本来把多个 SQL 语句套进一个事务里就只是为了让他们变成一个原子操作,使得这些语句所造成的影响(`INSERT/UPDATE`造成写)要么都执行成功(`COMMIT`),要么都执行失败(`ROLLBACK`),所以保证了数据一致性
> 而这的所谓原子很明显不保证在并行事务时不会有任何`race condition`,只有事务隔离级别才能用来控制允许哪些类型的`race condition`发生
---
> 大概就需要为每个事务维护一个状态 snapshot (实现上可以不用拷贝整个状态
mysql innodb 中对 MVCC 的实现是[undo log](
https://dev.mysql.com/doc/refman/8.0/en/innodb-undo-logs.html):https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_consistent_read> - 一致性读取
> 一种读取操作,它使用 快照信息根据时间点呈现查询结果,而不管同时运行的其他事务执行的更改。如果查询到的数据已经被另一个事务更改,则根据 undo log 的内容重建原始数据。这种技术通过强制事务等待其他事务完成来 避免一些可能会降低并发性的锁定问题。
> 使用 REPEATABLE READ 隔离级别,快照基于执行第一次读取操作的时间。使用 READ COMMITTED 隔离级别,快照将重置为每个一致读取操作的时间。
> 一致读取是默认模式,在该模式下 InnoDB 处理处于 READ COMMITTED 和 REPEATABLE READ 隔离级别的 SELECT 语句。因为一致读取不会在它访问的表上设置任何锁,所以其他会话可以在对表执行一致读取时自由修改这些表。
因此`REPEATABLE READ`或者说 mysql 所谓的[`Consistent Nonlocking Reads`](
https://dev.mysql.com/doc/refman/8.0/en/innodb-consistent-read.html)就像是一种无锁数据结构---
> 也就是跟 non-repeatable read 差不多,但是针对插入行而不是修改行
`INSERT/UPDATE/DELETE`3 大 DML 都属于`phantom read`的控制范围,只是我举的例子是`COUNT(*)`所以必须得是`INSERT/UPDATE`才会影响前后`COUNT(*)`的结果
而 so 人
https://stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read/11044968#11044968 举的例子就更准确:
> 幻读:查询中的所有行前后都具有相同的值,但正在选择不同的行(因为 B 删除或插入了一些)。示例:select sum(x) from table;如果行已添加或删除,即使没有更新受影响的行本身,也会返回不同的结果。
---
> 在最高的隔离级别`SERIALIZED`当中,如果两个事务各自执行了一次`SELECT COUNT(*) FROM table`(都读到了一开始的`COUNT`),然后各自`INSERT`了一行,而且在`INSERT`的行的某一列记录了`INSERT`时的`COUNT`。
这得看两个事务 A 和 B 分别执行`SELECT`和`INSERT`之间的时序:
事务隔离级别|时序|事件
-|-|-
所有|B 在 A 执行`SELECT+INSERT+COMMIT`后`SELECT`|B**会**看到 A 多`INSERT`的一行
`READ UNCOMMITTED`|B 在 A 执行`SELECT+INSERT`( A 尚未 COMMIT )后`SELECT`|B**会**看到 A 多`INSERT`的一行
`READ COMMITTED`|B 在 A 执行`SELECT+INSERT`( A 尚未 COMMIT )后`SELECT`|B**不会**看到 A 多`INSERT`的一行
`READ COMMITTED`|B 在 A 执行`SELECT+INSERT+COMMIT`后`SELECT`|B**会**看到 A 多`INSERT`的一行
`REPEATABLE READ`|B 在 A 执行`SELECT`后`SELECT`,然后 A 执行`INSERT+COMMIT`|B 的`SELECT`(仅限`SELECT`,因为上文提及通过`UPDATE/INSERT`的 WHERE 子句造成的 read 会绕过`SNAPSHOT`)<br>**永远不会**看到 A 多`INSERT`的一行即便 A 已 COMMIT
`SERIALIZED`|B 在 A 执行完`SELECT`后`SELECT`时阻塞等待 A 执行`INSERT+COMMIT`|B**会**看到 A 多`INSERT`的一行<br>但这并不是`phantom read`因为 B 此前根本没有读到那一行( B 一直在等待 A`COMMIT`)
---
> 那么,是否仍然可以这两个事务全部成功提交
`COMMIT`几乎不会失败
---
> 而且得到了两个这个记录相同(都是“两边都没有`INSERT`之前的`COUNT`”)的行?
无法,因为事务 B 会阻塞等待 A 的`COMMIT`生效因为 A 的`SELECT COUNT(*) ... FOR SHARE`给这表加了`IS 锁`
---
> 显然如果所有访问全都带全局互斥锁的话这种情形是不可能的。但在没有互斥锁但有最高隔离级别的事务的情况下呢?
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html 早已指出`SERIALIZABLE`的本质就是给所有`SELECT`末尾追加`FOR SHARE`:
> - SERIALIZABLE
> This level is like [REPEATABLE READ](
https://dev.mysql.com/doc/refman/8.0/en/innodb-transaction-isolation-levels.html#isolevel_repeatable-read), but InnoDB implicitly converts all plain [SELECT](
https://dev.mysql.com/doc/refman/8.0/en/select.html) statements to [SELECT ... FOR SHARE](
https://dev.mysql.com/doc/refman/8.0/en/select.html) if [autocommit](
https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_autocommit) is disabled
而`FOR SHARE`就是给行加`IS 锁`:
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html> 在读取的任何行上设置共享模式锁。其他会话可以读取这些行,但在您的事务提交之前不能修改它们。如果这些行中的任何一行被另一个尚未提交的事务更改,您的查询将等待该事务结束,然后使用最新的值。
https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-shared-exclusive-locks 进一步指出:
> InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such as [LOCK TABLES ... WRITE](
https://dev.mysql.com/doc/refman/8.0/en/lock-tables.html) takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses [intention locks](
https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_intention_lock). Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table. There are two types of intention locks:
> - An [intention shared lock](
https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_intention_shared_lock) (IS) indicates that a transaction intends to set a shared lock on individual rows in a table.
> - An [intention exclusive lock](
https://dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_intention_exclusive_lock) (IX) indicates that a transaction intends to set an exclusive lock on individual rows in a table.
> For example, [SELECT ... FOR SHARE](
https://dev.mysql.com/doc/refman/8.0/en/select.html) sets an IS lock, and [SELECT ... FOR UPDATE](
https://dev.mysql.com/doc/refman/8.0/en/select.html) sets an IX lock.
> The intention locking protocol is as follows:
> - Before a transaction can acquire a shared lock on a row in a table, it must first acquire an IS lock or stronger on the table.
> - Before a transaction can acquire an exclusive lock on a row in a table, it must first acquire an IX lock on the table.