在本篇文章中,会讨论以下问题:
- MySQL中的读问题、写问题是如何产生的?
- 如何解决读问题?
- 幻读与可重复的区别是什么?如何解决幻读呢?
- MVCC可以解决幻读吗?
- MySQL中产生写问题的原因,解决方法是什么?
- 哪些情况会产生丢失更新?
- 悲观锁与乐观锁如何解决写问题?
事务
事务指一组操作,要么全部执行,要么全都不执行。也就是最小的执行单位是定义的这一组事务。事务确保了逻辑的成功执行。 例如:银行的转账。
事务的特性 ACID
- 原子性 Atomicity
事务中包含的逻辑,不可分割。 - 一致性 Consistency
事务执行前后,数据完整性,从一个逻辑一致性状态到另一个逻辑一致性状态。 - 隔离性 Isolation
事务在执行期间不应该受到其他事务的影响,两个事务的结果不应该相互影响。 - 持久性 Durability
事务执行成功,那么数据应该持久保存到磁盘上。
事务安全问题
读问题
在读操作时可能会出现以下三种等级的错误 1.脏读 2.不可重读读 3.幻读
- 脏读
A事务读B事务还未提交的数据。 - 不可重复读
A事务读到了B事务提交的数据,造成了A前后两次查询结果不一致。 - 幻读
A事务读到了B事务insert的数据,造成了A前后两次查询结果不一致 。
脏读是因为读到了未提交的还会回滚的数据导致的两次结果不一致(第一次脏数据,第二次正确数据),不可重复读是由于B提交而读到两次结果不一样(第一次未提交,第二次已提交正确数据)。
读问题的解决方案–隔离级别
- 读未提交 Read Uncommite
A事务读到B事务未提交的事务,而B可能回滚,引起--脏读问题。 - 读已提交 Read Committed
A事务第一次读到B事务未提交的数据,第二次读到B数据提交的数据,造成了A事务前后两次不同的结果。这个隔离级别能够屏蔽 脏读 的问题, 但是引发了另一个问题-- 不可重复读 。 - 重复读 Repeatable Read
AB两个事务,B事务进行了修改还未提交,A查询还是B事务修改之前的结果。B事务进行了提交,A查询结果仍是B事务修改之前的结果。也就是在可重复读的隔离级别中,AB是两个相互独立的互不影响。可重复读虽然解决了不可重复读的问题,但是这个隔离级别某种程度来说,在实际中的作用并不大。并且这个隔离级别还没有解决--幻读的问题。 可串行化 Serializable - 串行化
解决了幻读问题。如果有一个连接的隔离级别设置为了串行化 ,那么谁先打开了事务, 谁就有了先执行的权利,谁后打开事务,谁就只能得着,等前面的那个事务,提交或者回滚后,才能执行。例如:B先打开了一个事务,A又打开了一个事物,此时B不管是否已经修改,只要还没有提交,A的查询都会一直阻塞着,一直等B提交之后A才会有查询的结果。但是这种隔离级别一般比较少用。容易造成性能上的问题。效率比较低。
MVCC可以通过一致性视图(快照读)来实现读已提交与可重复读,但是解决幻读的问题。既然有了一致性视图,那么为什么不从这个一致性视图中读取之前的数据,以此解决幻读的问题呢?普通情况的select是不会产生幻读的,但是使用select...for update(当前读)会产生幻读的问题,更具体的MVCC与幻读将会在下一篇文章中讨论。
按效率划分,从高到低
读未提交 > 读已提交 > 可重复读 > 可串行化
按拦截程度 ,从高到底
可串行化 > 可重复读 > 读已提交 > 读未提交
写问题
因为事务的写并发主要引起的写问题,主要就是更新丢失问题,又具体划分为因为数据库本身和因为业务逻辑造成的丢失更新问题。
数据库事务本身造成的丢失更新
| 事务A | 事务B |
|---|---|
| begin update t1 set k=k+1 where id=1; |
begin |
| update t1 set k=k+3 where id=1; | |
| commit |
上述流程中,AB事务 同时建立 ,A的update会被B的update覆盖吗?答案是不会,而且不管在MySQL的哪种隔离级别中都不会发生这种情况,原因就是在上一篇文章提到的两阶段锁协议,B在update的时候只能等到事务A提交之后才能获得锁,而这个锁是MySQL数据库为我们自动加上的。所以这种情况不会发生在MySQL事务中。
因为业务逻辑造成的丢失更新
| 事务A | 事务B |
|---|---|
| begin; | begin; |
| select count from t1 where id = 1; | select count from t1 where id = 1; |
| if(count > 0) update count - 1 for update; |
if(count > 0) update count - 1 for update; |
| begin; | |
| begin; |
上面表格中的操作更偏向于业务逻辑,先select 库存的count值,再根据这个count进行逻辑判断,很容易看出来这样会产生count值为负数的操作。虽然MySQL内部的update是按照正常的逻辑进行执行的,但是从整个业务来看却发生了错误,比如秒杀活动中的超卖问题。这个问题本质是事务A 中的select 与 update操作中间插入了事务B的操作,要解决其实也很简单,可以考虑悲观锁或者是乐观锁进行解决。
业务逻辑丢失更新的解决方案–锁
- 悲观锁,认为一定会出现丢失更新,所以每次都会进行加锁、解锁操作。
for update数据库的锁机制,也叫排他锁,用于锁定一行。
look table read/writeunlook...数据库的表锁。 这两个锁都能解决上面的问题,关键是使用的方法,例如上面我们在update时候也加了for update 但还是出现逻辑错误了,正确做法是我们应该把悲观锁加在select的时候,只让一个事务获得目标资源的状态,然后在事务commit的过程中会自动的释放锁。 - 乐观锁,认为一定不会出现丢失更新,先比较再更新CAS。
代表就是CAS锁,先比较再进行更新,但是直接使用里面的字段值进行更新的话会产生ABA问题,事务1将update name为A,事务2 update name为B, 事务3又把name update为A,这三个事务都是正常执行的,假如这个时候有一个和事务1一起开始的事务4,比较name字段值的时候发现还是A,并没有感知到事务2与事务3的操作,这时候事务4的操作就会导致ABA的问题。其实解决ABA的问题也很简单,就是再加一个字段进行比较。例如新加了一个字段version,AB两个事务同时select出来的version都为0,当A修改后提交时候将version修改为1并保存(UPDATE...WHERE id = 1 AND version = oldVersion),事务 B要提交修改的时候比较自己的version与数据库里的version值,如果一样(WHERE 条件满足),可以直接将本次的提交存下来,否则就将再次获取数据库里的数据,重新进行修改、提交。
嘿嘿😉 读到这里,如果你有什么疑惑或发现文中哪里有错误欢迎在留言区相互讨论,如果有帮到你一点,麻烦随手点个赞再走啦 😛