数据库之事务并发问题与事务的隔离级别
事物的并发问题:
事物的并发问题主要分四个方面,即丢失更新,脏读,不可重复读,幻读。如果没有锁定且多个用户同时访问一个数据库,则当他们的事务同时使用相同的数据时,则可能会发生以上几种问题。
1.丢失更新
当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。每个事务都不知道其它事务的存在。最后的更新将重写由其它事务所作的更新,这样就会导致数据丢失。丢失更新可分为两类:
第一类丢失更新
例如:
时 间 取款事务 转账事务
--------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 汇入100元把余额改为1100元
T6 提交事务
T7 取出100元把余额改为900元
T8 事务回滚余额恢复为1000元
由于每个事务都不知道其它事务的存在,若按以上时间次序并发执行,转账事务就会被取款事务的回滚所覆盖。
第二类丢失更新
例如:
时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7
T8 汇入100元把余额改为1100元
T9 提交事务
同理,若按以上时间次序执行,取款事务会被转账事务的更新所覆盖。
总结:丢失更新本质上是写操作的冲突,解决办法:事务A写完并保存后事务B再写即可避免丢失更新问题。
2.脏读(未确认的相关性)
如果一个事务读取了另一个事务尚未提交的更新,则称为脏读。
例如:
时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4
T5 取出100元把余额改为900元
T6 查询账户余额为900元(脏读)
T7 撤销事务余额恢复为1000元
T8 汇入100元把余额改为1000元
T9 提交事务
由于转账事务读取了取款事务尚未提交的更新,并且对数据做出了修改并提交,若取款事务因某种原因撤销回滚,例如超时,则转账事务读取的是假数据,会导致用户损失100元。
总结:脏读的本质是读与写的冲突,解决办法:一个事务写完并确认无误后另一个事务再进行读取。
3.不可重复读(不一致的分析)
当事务多次访问同一行数据,并且每次读取的数据不一致的时候,即会发生不一致的分析。
例如:
时 间 取款事务 转账事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 查询账户余额为1000元
T4 查询账户余额为1000元
T5 取出100元把余额改为900元
T6 提交事务
T7 查询账户余额为900元
T8 余额到底是1000元还是900元?
由于在转账事务第一次查询到数据后,取款事务又对该行数据做出了修改并提交,而转账事务第二次读取了修改后的数据,即得到了不同查询结果。
总结:不可重复读本质上是读与写的冲突,解决办法:一个事务在另一个事务全部修改后再读取数据。
4.幻读
当一个事务的更新结果影响到另一个事务的时候,将会发生幻读。事务第一次读的行范围显示出其中一行已经不存在于第二次读或后续读中,因为该行已经被其它事务删除
例如:
时 间 注册事务 统计事务
----------------------------------------------------------------------------------------------
T1 开始事务
T2 开始事务
T3 统计注册客户总数为10000人
T4 注册一个新用户
T5 提交事务
T6 统计注册客户总数为10001人
T7 到底哪一个统计数据有效?
统计事务无法相信查询的结果,因为查询结果是不确定的,随时可能被其他事务改变。
对于实际应用,在一个事务中不会对相同的数据查询两次,假定统计事务在T3时刻统计注册客户的总数,执行SELECT语句,在T6时刻不再查询数据库,而 是直接打印统计结果为10000,这个统计结果与数据库当中数据是有所出入的,确切的说,它反映的是T3时刻的数据状态,而不是当前的数据状态。应该根据 实际需要来决定是否允许虚读。以上面的统计事务为例,如果仅仅想大致了解一下注册客户总数,那麽可以允许虚读;如果在一个事务中,会依据查询结果来做出精 确的决策,那麽就必须采取必要的事务隔离措施,避免虚读。
总结:幻读的本质也是读写操作的冲突,解决办法:读完在写。
事物的隔离级别:
解决并发问题的有效途径即采取有效的隔离级别
数据库系统采取了四种事务隔离级别供用户选择。(一般默认为)
1.SERIALIZABLE(串行化):一个事务在执行过程中完全看不到其他事务对数据库所做的更新(事务执行的时候不允许别的事务并发执行。事务串行化执行,事务只能一个接着一个地执行,而不能并发执行。)。
添加范围锁(比如表锁,页锁等,关于range lock,我也没有很深入的研究),直到transaction A结束。以此阻止其它transaction B对此范围内的insert,update等操作。
幻读,脏读,不可重复读等问题都不会发生。
2.Repeatable Read(可重复读):一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,但是不能看到其他其他事务对已有记录的更新。
对于读出的记录,添加共享锁直到事务 A结束。其它事务 B对这个记录的试图修改会一直等待直到事务 A结束。
可能发生的问题:当执行一个范围查询时,可能会发生幻读。
3.Read Commited(读已提交数据):一个事务在执行过程中可以看到其他事务已经提交的新插入的记录,而且能看到其他事务已经提交的对已有记录的更新。
在事务 A中读取数据时对记录添加共享锁,但读取结束立即释放。其它事务 B对这个记录的试图修改会一直等待直到A中的读取过程结束,而不需要整个事务 A的结束。所以,在事务 A的不同阶段对同一记录的读取结果可能是不同的。
可能发生的问题:不可重复读
4.Read Uncommitted(读未提交数据):一个事务在执行过程中可以看到其他事务没有提交的新插入的记录,而且能看到其他事务没有提交的对已有记录的更新。
不添加共享锁。所以其它事务 B可以在事务 A对记录的读取过程中修改同一记录,可能会导致A读取的数据是一个被破坏的或者说不完整不正确的数据。
另外,在事务A中可以读取到事务B(未提交)中修改的数据。比如事务B对R记录修改了,但未提交。此时,在事务A中读取R记录,读出的是被B修改过的数据。
可能发生的问题:脏读。
总结:对数据库使用何种隔离级别要审慎分析,因为:
1. 维护一个最高的隔离级别虽然会防止数据的出错,但是却导致了并行度的损失,以及导致死锁出现的可能性增加。
2. 然而,降低隔离级别,却会引起一些难以发现的bug。
知识需刨根问底!