当前位置: 首页 > 科技观察

Weakisolationlevel&transactionconcurrencyissues

时间:2023-03-13 04:49:09 科技观察

介绍为什么weakisolationlevels要有weakisolationlevels如果两个事务操作的是不同的数据,即没有数据依赖,那么它们可以安全的并行执行。但是当一个事务修改数据而另一个事务要同时读取数据,或者两个事务同时修改同一个数据时,就会出现并发问题。在应用程序的开发中,我们通常会使用锁来进行并发控制,以保证多个线程不会同时读写临界区中的资源。这其实对应的是事务的最高隔离级别:serializable。可序列化隔离是指数据库保证事务的最终执行结果与串行执行一样(即一次一个,没有任何并发??)。那么为什么应用程序可以提供可序列化的隔离级别,而数据库不能呢?其实根本原因是大部分应用都是对临界区进行内存操作,而数据库需要将临界区的数据持久化到磁盘,以保证持久性。但是磁盘操作比内存操作要慢几个数量级,而随机存取内存、固态硬盘、机械硬盘,对应的操作时间是几十纳秒、几十微秒、几十毫秒,这会导致持有锁的时间变长,临界区的资源竞争会变得异常激烈,数据库的性能会大大降低。因此,数据库研究人员为事务定义了隔离级别的概念,即在高性能和正确性之间进行权衡,相当于明确告诉用户,我们提供的模型正确性较低但性能较好。,以及正确性较好但性能较差的模式,用户可以根据自己的业务场景选择合适的隔离级别。弱隔离级别的风险弱隔离级别是不可序列化的隔离级别。较弱的隔离级别,它可以防止一些并发问题,但不是所有的并发问题。使用这些弱隔离级别,当事务并发执行时,可能会出现异常情况,带来一些难以捉摸的隐患。因此,我们需要了解弱隔离级别的并发问题以及如何预防。然后,我们可以使用我们掌握的工具和方法来构建正确、可靠的应用程序。各种隔离级别SQL-92标准定义了四种事务隔离级别:未提交读(ReadUncommitted)、已提交读(ReadCommitted)、可重复读(RepeatableRead)和序列化(Serializable)。在后来的开发过程中,加入了快照隔离级别(SnapshotIsolation)。不同的弱隔离级别解决不同的并发问题(正确性问题),也会存在一定的并发问题。下面是各种隔离级别和对应的并发问题::heavy_check_mark:表示隔离级别解决了并发问题;:x:表示隔离级别没有解决并发问题。SQL标准中隔离级别的定义还存在一些缺陷。有些定义含糊不清,不够精确,不能独立于实施。因此,上表只是对常见隔离级别并发问题的定义。您可以将其用作一般标准参考。当您使用某个数据库时,您需要阅读其文档以确定其每个隔离级别的具体并发问题。MySQL的默认隔离级别是:RepeatableRead。Oracle和PostgreSQL默认的隔离级别是:readcommitted事务并发执行时,存在并发问题。如果两个事务对不同的数据进行操作,即没有数据依赖性,则可以安全地并行执行。但是当一个事务修改数据而另一个事务要同时读取数据,或者两个事务同时修改同一个数据时,就会出现并发问题。并发问题总结:脏写:一个事务覆盖了其他事务还没有提交的写。脏读:一个事务读取了其他事务未提交的写操作。不可重复读:在一个事务内,多次读取同一条记录的结果是不同的。丢失更新:两个事务同时执行“读-修改-写回”操作序列。事务A覆盖了事务B的写入,但不包含事务B的修改值,最终导致部分更新数据丢失。幻读:在一个事务中,多次读取满足指定条件的数据,读取结果不同。Writeskew:事务先查询数据,根据返回的结果做一些决策,然后修改数据库。当事务提交时,支持决策的先决条件不再成立。脏写一个事务会覆盖其他事务未提交的写操作。脏读一个事务读取其他事务未提交的写操作。比如脏读事务B修改了x,在事务B提交之前,事务A读取了x的修改数据。此时事务B被回滚,相当于事务A读取了一个无效数据(实际上没有提交到数据库的数据),事务A的读取是脏读。在不可重复读事务中,多次读取同一条记录的结果是不同的。(一个事务可以读取另一个事务对同一条记录的修改)比如不可重复读事务A读取x,然后事务B修改x并提交。此时事务A再次读取x,发现两次读取同一条记录的结果不同,为不可重复读。更新丢失两个事务同时执行“读-修改-写回”操作序列。事务A覆盖了事务B的写入,但不包括事务B的修改,最终导致更新的数据丢失了一部分。比如更新丢失的事务A先读取一条记录,然后事务B读取一条记录,事务B修改并写回,然后事务A修改并写入。事务A覆盖了事务B的写入,但是不包含事务B的修改,最终导致事务B的更新丢失。幻读在一个事务中,多次读取满足指定条件的数据,读取结果不同(一个事务可以读取另一个事务创建的满足条件的记录)。例如幻读事务A读取了一组满足条件1的条件,然后事务B创建满足条件1的数据,使其满足条件1并提交,如果事务A再次以同样的条件1读取,则会得到一组与第一次读取不同的数据。这称为幻读。不可重复读和幻读都是在一个事务内进行的,同一个查询执行多次,结果不同。两者有什么区别?幻读主要是指读取满足另一个事务插入或更新条件的记录。不可重复读主要是指读取另一个事务对同一条记录更新的写倾斜。writeskew是:事务先查询数据,根据返回的结果做一些决策,然后修改数据库。当事务提交时,支持决策的先决条件不再成立。如何预防并发问题现在我们知道了各个隔离级别可能出现的并发问题。如果当前的数据库使用了某种隔离级别,我们也知道这种隔离级别下的并发问题。有没有办法避免并发问题?以及如何避免并发问题?有些并发问题只能通过提高隔离级别来避免。接下来,我们将对每个并发问题一一讨论。防止脏写允许脏写这种并发问题的数据库基本上是不可用的。因此,所有的隔离级别都不允许出现脏写等并发问题。防止“脏写”是指在写入数据库时??,只有成功提交的数据才会被覆盖。防止脏写的通常方法是推迟第二个写请求,直到前一个事务已提交(或中止)。数据库通常使用行级锁来防止脏写:如果两个事务试图同时写入同一个对象,则使用锁来确保第二次写入等待前一个事务完成(包括中止或提交)。这种锁定是由数据库在读提交模式(或更强的隔离级别)下自动完成的。防止脏读防止“脏读”是指在读取数据库时,只能看到已经提交成功的数据。如果业务中不接受脏读,隔离级别应该等于或高于“readcommitted”隔离级别。当有以下需求时需要防止脏读:如果一个事务需要执行多个操作来更新多个对象,我们需要保证另一个事务或应用层要么看到所有操作执行前的状态,要么看到所有操作完成后的状态,而不是看到部分操作完成的中间状态。如果我们要提供这样的保证,就必须防止脏读。脏读意味着另一个事务可能会看到一些更新,但不是全部,观察部分更新的数据可能会使用户感到困惑。如果事务中止,所有的写操作都需要回滚,所以必须防止脏读,让用户观察不到一些后来回滚的数据,实际上并没有提交到数据库。防止脏读的解决方案:两阶段锁协议;存储数据的新旧版本。一种选择是使用同一个锁来防止脏写,所有尝试读取对象的事务都必须首先获取锁,并在事务完成后释放锁,从而确保不会发生读取脏的、未提交的值的情况。但是加锁在实际中是行不通的,因为长时间运行的写事务会导致很多只读事务等待时间过长,严重影响只读事务的响应时间。应用程序中的任何局部性能问题都会蔓延并影响整个应用程序,从而产生连锁反应。因此,大多数数据库都采用以下方法来防止脏读:对于每个要更新的对象,数据库都会维护该对象的两个版本(它的旧值和当前持锁事务要设置的新值)。在事务提交之前,其他事务的读操作读取的是旧值;只有在写入事务提交后,它才会切换到读取新值。虽然MySQL使用多版本并发控制来防止脏读,但多版本比两个版本更通用。防止不可重复读防止“不可重复读”是指在事务执行期间看到的数据始终与事务启动时看到的数据一致。不能容忍不可重复读的场景:备份场景:备份任务复制整个数据库,可能需要几个小时才能完成。在备份过程中,数据可以继续写入数据库。因此,备份可能包含一些旧版本数据和一些新版本数据。如果从这样的备份中恢复,会导致永久性的不一致。如果业务中不接受不可重复读,隔离级别应该等于或高于“可重复读”隔离级别。在MySQL中,可重复读隔离级别是快照级别隔离。快照级隔离的大致思路是,每个事务总是在某个时间点读取一致快照中的数据。为了实现快照级别的隔离,MySQL数据库使用了一种称为多版本并发控制(MVCC)的机制。防止更新丢失在应用程序从数据库中读取一些值,根据应用程序逻辑修改它,然后写回新值(读-中-写过程)的操作场景中可能会发生更新丢失。当两个事务对同一个数据对象进行相似的操作时,后一个写操作不包含前一个写操作的修改,最终丢失前一个写操作的修改。丢失的更新是写入事务并发冲突。为了防止丢失更新,目前有几种可行的解决方案。原子更新操作:很多数据库都提供原子更新操作来避免应用层代码中的“读-修改-写回”操作顺序。如果数据库支持原子更新操作,这通常是防止更新丢失的最佳解决方案。原子操作通常是通过对读取的对象进行独占锁定来实现的,这样在提交更新之前其他事务无法读取它。实现原子操作的另一种方法是强制所有原子操作在单个线程上执行。这也是Redis通过显式加锁来防止更新丢失的解决方案:既然原子操作是通过对读对象加排他锁来实现的,那么我们也可以显式加锁待更新对象,从而实现“读-修改-写回”一系列操作是连续执行的。比如使用MySQL的select......forupdate;原子更新操作和显式锁定都通过强制串行执行“读取-修改-写回”操作序列来防止丢失更新。自动检测更新丢失:首先让“读取-修改-写回”操作序列并发执行,但如果事务管理器检测到更新丢失的风险,它将中止当前事务并强制回退到安全的“读取”-修改-写“回”方式。比较和设置:首先让“读-修改-写回”操作顺序并发执行。如果读取的内容发生变化,值与“旧内容”不匹配,则update失败,应用层需要重新检查,必要时重试,例如updatet1setcol1='newcontent'whereid=1andcol1='oldcontent';自动检测updatelossPostgreSQL的repeatableread,Oracle的序列化和SQLServer的快照级隔离等,可以自动检测丢失更新的发生并中止违规事务,但是MySQL中InnoDB存储引擎的可重复读不支持自动检测丢失更新。PreventPhantomRead&WriteSkewPreventPhantomRead:使用serializableisolationlevel在MySQL的repeatablereadisolationlevel下,使用select......forupdate;使用可序列化隔离级别来防止幻读。可序列化隔离通常被认为是最强的隔离级别。使用Serializable隔离级别可以防止所有可能的竞争条件。可串行化隔离保证了即使事务可能并行执行,最终执行结果与一次执行一个事务(即串行执行)是一样的。实现可序列化隔离级别有几种方式:实际串行执行:二段锁+索引区间锁:使用二段锁和索引区间锁的组合实现可序列化隔离可序列化快照隔离:(我不懂thisyet)MySQL的可序列化隔离级别采用的是第二种方式(双阶段锁+索引区间锁)。Writeskew是:事务首先查询数据,根据返回的结果做出一定的决策。然后修改数据库。当事务提交时,支持决策的先决条件不再成立。这样的操作场景可能会出现写倾斜:Step1select:应用程序从数据库中读取一组满足条件1的数据Step2decision:应用层代码根据查询结果决定下一步操作(with可能会继续,也可能会报错并中止)Step3写入:如果应用程序决定继续,它会发起一次数据库写入(插入、更新或删除)并提交事务。第3步的写操作会改变第2步做决策的前提条件,如果两个事务同时执行这样一个“读-决策-写”的操作顺序,后面的写会改变前面的写。输入执行的先决条件会导致意想不到的结果。防止写偏斜写偏斜问题有几种可能的解决方案:只使用可序列化隔离级别来避免写偏斜(使用索引区间锁来防止其他事务写入满足条件的行)改变“read-”的执行顺序“decision-write”操作顺序为“write-read-decision”:先写,再select查询并加排它锁(select......forupdate),最后根据查询的结果决定是否commitorgiveup.Materializationconflict,也称为materializationconflict:在某些业务场景中,select查询不满足给定搜索条件的行(例如select*fromt1whereid!=1)。如果查询在step1根本没有返回任何行,那么select......forupdate是没有办法加锁的,只能考虑物化冲突,本质上三种可能的解决方案都是显式加锁事务所依赖的行。物化冲突的解释(materializationconflicts)如果问题的关键是查询结果中没有对象(空)可以加锁,可能是人为引入了一些可加锁的对象。这种称为物化冲突(或物化冲突)的方法将幻读问题转变为数据库中特定行集的锁冲突问题。然而,弄清楚如何实现物化通常具有挑战性且容易出错,而将并发控制机制降级为数据模型的想法总是不够优雅。由于这些原因,除非别无选择,否则不建议具体化冲突。