@
n0099 https://github.com/n0099/TiebaMonitor/issues/32#issuecomment-1401548572github.com/n0099/TiebaMonitor/issues/32#issuecomment-1401418933> 其实你只要说这句就够了,其它诸如持久化等在这个问题当中似乎是无关的吧。
github.com/n0099/TiebaMonitor/issues/32#issuecomment-1401116944> 提供 [CAS](
en.wikipedia.org/wiki/Compare-and-swap) 原子操作:如果满足[条件]那么[写入]否则[失败]
其实你只要说这句就够了,其它诸如 intel/arm risc/cisc 指令集之争等在这个问题当中似乎是无关的吧。
---
> 对于多个写操作,`TRANSACTION`保证了它们整体的原子性——同时的或将来的其它会话要么看到所有这些写入,要么全都不看到。是吧?
在`READ COMMITTED`及以上事务隔离级别中是能保证不发生`dirty read`的,只有降低到
回顾经典之`3.数据库事务隔离级别从 READ COMMITED 降至 READ UNCOMMITED`节的图
![image](
user-images.githubusercontent.com/13030387/214240624-e183eff5-e1d5-4733-99ad-8911c0d5c54f.png)
请注意本讨论串中的所有`dirty read`(除了上述那一个)都应该查找替换为`phantom read`(以及 typo 之`COMMITTED`少打了一个`T`),因为我最开始看着这图时就写错了:
> 可见降至 READ UNCOMMITED 后允许 dirty read 的发生
另外请注意尽管`non-repeatable read`和`phantom read`之间看起来很相似(他们的 UML 时序图甚至是完全相同的),但本质完完全全两码事:
stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read---
> 这完全能够理解,不理解的部分是`TRANSACTION`对事务内的读取(`SELECT`)如何起作用。如果读取同样被包含在这种原子性当中,那您开头所说的进程全局锁理应是多余的
`SELECT`是否对其所读出来的行资源进行锁定是取决于您有没有追加`FOR SHARE/UPDATE`的,建议复习:
dev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-intention-locksdev.mysql.com/doc/refman/8.0/en/innodb-locking.html#innodb-insert-intention-locksdev.mysql.com/doc/refman/8.0/en/innodb-locking-reads.html---
> 反之,在只有一个写入语句的简化情况下`TRANSACTION`的存在似乎并没有任何作用的样子。
dev.mysql.com/doc/refman/8.0/en/commit.html 对此早有预言:
> 默认情况下,MySQL 在 启用[自动提交](
dev.mysql.com/doc/refman/8.0/en/glossary.html#glos_autocommit)模式的情况下运行。这意味着,当不在事务内时,每个语句都是原子的,就好像它被 START TRANSACTIONand 包围一样 COMMIT 。您不能使用 ROLLBACK 来撤销效果;但是,如果在语句执行期间发生错误,则回滚该语句。
---
> 或者这么说:如果将`SELECT`移到`START TRANSACTION`之前(事务外),行为上是否会有任何可以依赖的差别呢?
移出去会导致单条`SELECT`由于[`AUTO_COMMIT`](
dev.mysql.com/doc/refman/8.0/en/innodb-autocommit-commit-rollback.html)而变成一个只有他一个语句的事务(其隔离级别由于没有显式指定所以会使用 mysql 默认的`REPEATABLE READ`,然而实际上对于单语句事务而言任何隔离级别都是相同的),如果`SELECT`后面没有`FOR UPDATE`那么移出去也是一样的,即便有由于这是单语句事务所以 IX 锁也立即被释放了那还是没有造成差别
而在
stackoverflow.com/questions/1976686/is-there-a-difference-between-a-select-statement-inside-a-transaction-and-one-th/1976701#1976701 的中他所说的
> 是的,事务内部的人可以看到该事务中其他先前的 Insert/Update/delete 语句所做的更改;事务外的 Select 语句不能。
在`SELECT`所在的事务的隔离级别是`READ UNCOMMITTED`时应该是不成立的
然而 so 人早已道明真相:
stackoverflow.com/questions/1171749/what-does-a-transaction-around-a-single-statement-do> 这可能归因于“迷信”编程,或者它可能表明对数据库事务性质的根本误解。一种更仁慈的解释是,这只是过度应用一致性的结果,这是不恰当的,这是爱默生委婉语的另一个例子:
> 愚蠢的一致性是小脑袋的妖精,
> 受到小政治家、哲学家和神学家的崇拜
我的评价是:疑似当代
en.wikipedia.org/wiki/Cargo_cult en.wikipedia.org/wiki/Cargo_cult_programming stevemcconnell.com/articles/cargo-cult-software-engineering/---
> `生效`的意思当然就是说能够被读取看到。`COMMIT`之前的`INSERT`不会被看到效果
这就是`READ COMMITTED`事务隔离级别
---
> 但,在只需要一个`INSERT`的简化例子当中,彻底把`TRANSACTION`删掉,让`INSERT`立即生效(比起`INSERT`之后该线程紧接着执行`COMMIT`让`INSERT`生效,并没有在这之间和全局锁发生任何交互),跟开头的例子有什么差别呢?这是我所没能理解的。
开头的例子里也不是只有一个`INSERT`啊,任何线程在`INSERT`前都必须先`SELECT`以排除已存在的行
---
> 调用者基于先前`SELECT`到的,而现在已经被修改掉的数据,所作出的呢?是否有这样的事务隔离级别,从而在这种情况下也会`ROLLBACK`整个事务?
无,所以需要乐观并发控制,如 MSSQL 中基于一个单调自增的`ROW_VERSION`字段来跟踪有无`UPDATE`
建议回顾
www.v2ex.com/t/909762#r_12593822> 乐观并发控制是要求每个客户端的后续读写都得依赖于此前查询所获得的的`ROW_VERSION`
> 比如事务 1`SELECT yi, ver FROM t`获得`(a,0)`
> 那么事务 1 后续的所有对该行的读写( SELECT/UPDATE )都得依赖于`ver=0`这个此前获得的事实
> 也就是对于读:事务 1 期望重新`SELECT yi, ver FROM t`获得的还是`(a,0)`,这叫做`REPEATABLE READ`(避免了幻读`phantom read`),注意 mysql 默认的事务隔离级别就是`REPEATABLE READ`所以默认是已经保证了在同一事务内不断重新执行`SELECT yi, ver FROM t`返回的永远都是`(a,0)`
> 对于写:事务 1 期望`UPDATE t SET 某个其他字段 = 某值 WHERE yi = a AND ver = 0`所返回的`affected rows`是 1 行,而如果不是 1 行而是 0 行就意味着表 t 中已经不存在符合约束`WHERE yi = a AND ver = 0`的行,也就是说行`yi=a`已经被其他事务修改了
> 建议参考以某企业级 orm EFCore 为背景的 MSDN 微软谜语:
learn.microsoft.com/en-us/ef/core/saving/concurrency> 以及我之前对于类似的场景(但是是 INSERT-only 而不是 UPDATE )使用了数据库层提供的悲观并发控制,毕竟在 SQL 末尾加`FOR UPDATE`可比在 mysql 里用 TRIGGER 模拟单调递增的自增 sql server 的 ROW_VERSION 类型然后在程序业务逻辑里写乐观控制所带来的一大堆 if 简单多了:
www.v2ex.com/t/908047然而更现实的问题是乐观并发控制是用于协调`SELECT+UPDATE`而不是`SELECT+INSERT`的,而我这里又只有`INSERT`没有`UPDATE`,所以加了`ROW_VERSION`和对应的程序中乐观并发控制业务逻辑也无济于事
---
> 举个例子,假设自行实现某种自增计数器,而且并没有在数据库中设置相应的约束。线程 1 读取了计数器的当前值,根据当前值递增后写入递增后的值,这些操作被放在一个事务当中。线程 2 也做同样的事,但是刚好在线程 1 的两个操作之间完成了线程 2 的所有操作。于是,线程 1 准备写入的值跟线程 2 是一样的。
这就是阁下之后所提到的
en.wikipedia.org/wiki/ABA_problem 而乐观并发控制本质上也是为了解决这个
---
> 像这样的情况下就算这个写入操作是合法的,不违反数据库的约束,也理应让线程 1 的事务回滚而非成功提交。广泛一点来说,线程 1 的事务的 读取集 (事务中读过哪些数据——准备写入的东西可能是调用者根据这些数据推算出的)跟线程 2 的事务的 写入集 (事务中写入了哪些数据)有重合,这样的情况下很可能不应该让两个事务都成功提交。写入的数据可能依赖于先前的读取,而且调用者(在得知事务成功提交后)的后续操作可能依赖于`确实是在读取到的这样的数据的基础上发生了这样的写入`。
然而将单个值上升到集合层面就麻烦的多(什么 pythonic )
我能想出来的是线程 1 读取后就改一下`ROW_VERSION`,也就是说`ROW_VERSION`跟踪的不再是这行被改动了几次而是被读了几次
但这实际上就意味着又回到了`并行度=1`的[serializability](
en.wikipedia.org/wiki/Serializability),因为这时只有强迫同时只有一个线程在读写才能保证每个线程的`ROW_VERSION`都是他想要的(即`COMMIT`时也没被其他事务由于`SELECT`而改变)
---
> 实际上是可能的,只不过大概不是你要的效果。那就是所有需要原子操作的地方获取全局锁。意味着不论这些原子操作访问了什么,都无法并行。
然而很明显没人想要`四叶头子 CS 硕士 PLT 理论中级高手仏皇 irol 阁下`写 java 遇到并行问题时就无脑套一个大`synchronized() {}`block 来降低`并行度=1`,这只能作为性能不重要但一致性和事务成功率重要的金融军事等企业级场景
---
> 对于根本没有什么硬件原子操作支持的平台,就可以这样实现原子操作。但截至目前我并未知悉有这样的平台(多处理器,但没有硬件原子操作支持)。
> 实现 自旋锁 或者 互斥体 并不需要 CAS ,只需要比它弱得多的 [test-and-set](
en.wikipedia.org/wiki/Test-and-set)
然而 testandset 同样是一种 atomic 操作,他的名字就已经暗示了他是把两个常见的原本是独立的原子操作给封装成又一个原子操作
---
> 显然用 CAS 或者 LL/SC 很容易实现 test-and-set ,反过来则不行
en.wikipedia.org/wiki/Consensus_(computer_science)#Consensus_number 的表格中也进一步指出:
Consensus number|Objects
-|-
$1$|atomic read/write registers, mutex
$\infty$|compare-and-swap, load-link/store-conditional,[40] memory-to-memory move and swap, queue with peek operation, fetch&cons, sticky byte
> 根据层次结构,即使在 2 进程系统中,读 /写寄存器也无法解决一致性问题。栈、队列等数据结构只能解决两个进程之间的共识问题。然而,一些并发对象是通用的(在表中用$\infty$),这意味着它们可以解决任意数量的进程之间的共识,并且可以通过操作序列模拟任何其他对象
---
> 除了这种完全舍弃并行的做法以外,既然这个事务性是跟 读取集 写入集 相关联的,那么我当然认为这种事务约束理应由数据库而不是调用者实现。
事实核查:截止 2023 年 1 月,仍然没有 RDBMS 实现了杨博文阁下所提出的这种全新的具有颠覆性的基于 changeset 的事务隔离机制
---
> 另外其实也不是不可以,比方说在 GC 环境下可以这么做:
> ```c
> 指针 全局值;
> 读对象() {
> 指针 临时值 = 原子读取(全局值);
> return 解引用(临时值);
> }
> CAS 对象(对象 expected, 对象 desired) {
> 指针 临时值 = 原子读取(全局值);
> if (对象相等(解引用(临时值), expected)) {
> 指针 临时值 2 = new 对象(desired);
> return 原子 CAS(全局值, 临时值, 临时值 2);
> } else {
> return false;
> }
> }
> ```
您在写`贴吧辅助工具皇帝鸡血神` @
bakasnow 最爱的易语言?
---
> 也就是说不修改对象,而是每次都创建新的对象,然后对指针做 CAS
那阁下实际上是在对机器字长 32/64 位的指针做 CAS ,而 CPU 指令集当然会提供能对机器字长长的数据进行 CAS 的指令
> 在 GC 环境下是可行的,在非 GC 环境下则会面临释放时机的问题,还有 ABA 问题 。
为什么这里会有 ABA
> 这个对象可以是任意大的数据结构,比方说可以是一整个 dict 数据结构,只不过为了修改一个值拷贝整个数据结构很容易让它完全丧失性能优势,还不如直接全局加锁。
疑似`新创无际 rust 人生信壬西兔人迺逸夫` @
Prunoideae 的内存安全性和 ios 的跨 app share 文件必须完整复制粘贴
> 总之这里只是说不能简单地组合多个窄 CAS 来代替一个宽 CAS 。事实上,之所以处理器通常提供到两倍机器字长的 CAS ,一个主要原因是上述(无锁数据结构)方案,再加上一个额外的整数跟这个指针一同被 CAS ,就需要两倍机器字长的 CAS 。
什么整数?