事务并发产生的四个问题
更新丢失
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存在,就会发生丢失更新问题——最后的更新覆盖了由其他事务所做的更新。
clipboard.png
脏读
一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此做进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象地叫做"脏读"。
image.png
不可重复读
一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
image.png
幻读
错误的理解:
一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。
这其实并不是幻读,这是不可重复读的一种,只会在 R-U R-C 级别下出现,而在 mysql 默认的 RR 隔离级别是不会出现的。
解决不可重复读使用的是一致性读模式,通过快照的方式,使得事务内的select只会以第一次查询为基准。
事实是,在RR级别下,如果事务A的两个select操作中间不穿插更新、删除、插入操作,那么即使别的事实B有更新、删除、插入操作,并且已经提交。事物A在事务B提交后的,第二次select操作所得到的查询结果,与第一次相同。(如果以上述错误的理解为准,那RR级别不就不存在幻读了?)
image.png
正确的理解:
重现更新丢失问题(错误的理解)
第一步:
我在数据库造了一条数据,如下图:
image.png
解释一下,
| 字段名 | 备注 |
|---|---|
| id | 唯一标识 |
| num | 计数器 |
第二步:
写一个事务
image.png
在事务中,分为三个步骤:
- 获取这条数据
- 对num加一
- 更新这条数据
第三步:
多线程并行执行10次事务
image.png
第四步:
多次执行,查看结果
image.png
运行三次,每次结果都不同(每次运行完都把计数器置零),但都没加到10,即存在更新丢失问题。
事务的隔离级别
MySQL/InnoDB定义的4种隔离级别:
-
Read Uncommited
读未提交:一个事务可以读取到另一个事务未提交的修改。这会带来脏读、幻读、不可重复读问题。(基本没用) -
Read Committed (RC)
读已提交:一个事务只能读取另一个事务已经提交的修改。其避免了脏读,但仍然存在不可重复读和幻读问题。 -
Repeatable Read (RR)
可重复读:同一个事务中多次读取相同的数据返回的结果是一样的。其避免了脏读和不可重复读问题,但幻读依然存在。 -
Serializable
串行化:从MVCC并发控制退化为基于锁的并发控制。不区别快照读与当前读,所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。事务串行执行。避免了以上所有问题。
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
于是我尝试提高事务的隔离级别到Serializable来防止更新丢失
image.png
执行上述多线程并发加程序时,缺发生了数据库死锁问题:
image.png
究其原因,是SERIALIZABLE隔离级别读写锁竞争导致的。
在SERIALIZABLE级别下,不会使用mysql的mvcc机制,而是在每一个select请求下获得读锁,在每一个update操作下尝试获得写锁。
在我们的例子中,在level_1中,事务A获得了id = 1的读锁A。
而在同时,事务B获得id = 1的读锁B。
在事务A level_2时,事务A尝试获得id = 1的写锁,这个时候,由于id = 1处不仅有事务A的读锁,还有事务B的读锁,因此事务A的update操作获取锁被阻塞。
此时,当事务B继续执行update操作时,由于事务A又拥有id = 1的读锁A,因此进入互相等待状态,造成死锁。
解决方案:
1 将select操作改为select for update,直接加写锁。
2 在业务层将此种类事务串行化。












网友评论