• 数据一致性和并发性


    转载自http://papmp.blog.163.com/blog/static/131716223201181933821313/

    包含下列主题:

    u 多用户环境中的数据并发性和一致性介绍

    u Oracle如何管理数据并发性和一致性

    u Oracle如何锁定数据

    u Oracle闪回查询概述

    多用户环境中数据并发性和一致性介绍

    在单用户数据库中,用户修改数据库中的数据,不用担心其他用户同时修改相同的数据。但是,在多用户数据库中,同时执行的多个事务中的语句可以修改同一数据。同时执行的事务需要产生有意义的和一致性的结果。因而,在多用户数据库中,数据并发性和数据一致性的控制非常重要:

    u 数据并发性:指很多用户可以同时访问数据

    u 数据一致性:指每个用户可以看到数据的一致性结果,包括自身事务和其他事务产生的改变。

    为给同时运行的事务描述一致性事务行为,Oracle研究员定义了一个事务隔离级别:串行性(serializability)。事务行为的的可串行模式试图确保事务运行的方式看起来像是一次运行一个事务(或者串行性),而不是并发运行。

    虽然事务间的这种隔离级别经常使用,但这种模式下的很多应用程序会降低吞吐量。并发运行事务的完全隔离意味着一个事务不能对另外一个事务正在查询的表执行插入。简而言之,真实环境中经常要在优秀的事务隔离级别和性能之间做一个折衷。

    Oracle提供了两个隔离级别,为应用程序开发人员提供可选的模式来兼顾性能和一致性。

    可预防现象和事务隔离级别

    ANSI/IOS SQL标准(SQL 92)定义了4个事务隔离级别,对事务处理性能的影响也个不相同。这些隔离级别是考虑了事务并发执行必须避免的3个现象提出的。

    3个应该避免的现象为:

    u 脏读:一个事务可以读取其他事务写入但还没有提交的数据。

    u 不可重复读(模糊读):一个事务重复读到以前读到的和查询到的数据,这些数据是其他的已提交事务已经修改或者删除的数据。

    u 幻影读:一个事务重复运行查询返回的一些列行,这些行包括其他已经提交的事务已经插入的额外的行。

    SQL92根据这些对象定义了4个隔离级别,事务运行在特定的隔离级别允许特别的一些表现。如表13-1所示。

    表13-1 隔离级别阻止的读现象

    隔离级别

    脏读

    不可重复读

    幻影读

    非提交读(read uncommitted)

    允许

    允许

    允许

    提交读(read committed)

    不允许

    允许

    允许

    重复读(Repeatable read)

    不允许

    不允许

    允许

    串行性(serializable)

    不允许

    不允许

    不允许

    Oracle提供提交读(read commited)和串行性(serializable)隔离级别,而只读模式不是SQL92的一部分。提交读是默认的。

    锁机制概述

    通常来说,多用户数据库使用多种类型的数据锁来解决相关数据并发、一致性和完整性的问题。锁是防止访问同一资源的事务的破坏性干扰的一种机制。

    资源包括两种通用的类型:

    u 用户对象,例如表、行(结构和数据)

    u 用户不可见的系统对象,比如内存中的共享数据结构和数据字典行

    Oracle如何管理数据并发性和一致性

    Oracle在一个多用户环境中维护数据一致性,是通过使用多版本一致性模型和不同类型的锁和事务来做到的。这一部分包含下列主题:

    u 多版本并发控制

    u 语句级别读一致性

    u 事务级别读一致性

    u 真正应用集群的读一致性

    u Oracle隔离级别

    u 提交读和串行性隔离级别的比较

    u 隔离级别的选项

    多版本并发控制

    Oracle自动为一个查询提供读一致性,就是说查询结果来源于一个单个的时间点(语句级别读一致性)。Oracle还为事务中所有的查询提供读一致性(事务级别读一致性)。

    Oracle使用回滚段中维护的信息来提供这些一致性视图。回滚段包含未提交的事务或最近提交的事务修改的数据的原始值。图13-1显示了Oracle如何通过回滚段中的数据提供语句级别的读一致性。

    图13-1 读一致性和事务

    当一个查询进入执行阶段,就确定了当前系统修改号(SCN)。在图13-1中,系统修改号为10023。在查询读取数据块时,只有对写入SCN号可见的块才会使用。块中的修改数据(更近的SCN)从回滚段中重新构建数据,这些重构数据返回给查询。因而,每个查询返回查询开始的时间的SCN涉及的所有提交数据。在查询执行时其他事务造成的修改不被采用,确保每个查询返回的都是一致性数据。

    语句级别读一致性

    Oracle总是执行语句级别读一致性。这可以确保单个查询返回的所有数据来源于单个时间点(查询开始的时间点)。因而,一个查询看不到脏数据或者查询执行期间其他事务提交的任何改变。查询执行开始后,只有查询开始之前提交的数据可以被查询到。查询不能看到语句开始执行之后提交的任何数据。

    Oracle自动为每个查询提供一致性结果,确保数据一致性,而不需要用户参与。包含子查询的SELECT、INSERT子句,UPDATE和DELETE所有查询数据,不管是显式的还是隐式的SQL语句都返回一致性数据。这些语句使用一个查询来确定要影响哪些数据(SELECT、INSERT、UPDATE或者DELETE)。

    一个SELECT语句是一个显式的查询,可以包含嵌套查询或者关联操作符。一个INSERT语句可以使用嵌套查询。UPDATE和DELETE语句可以使用WHERE子句或者子查询来只影响表中的一些行。

    使用INSERT、UPDATE或者DELETE语句确保一系列一致性的结果。但是,他们并不能看到本身DML语句所做的改变。换句话说,这些操作的查询只能看到这些操作发生改变之前的数据。

    注意:如果一个SELECT列表包含一个函数,那么数据库在运行PL/SQL函数代码中SQL的语句级别提供语句级别读一致性,而不是父级别。例如,一个函数可以访问另一个用户已经修改和提交的表数据。对于函数中的每个SELECT执行,都会创建一个新的读一致性快照。

    事务级别读一致性

    Oracle还提供事务级别读一致性选项。当一个事务运行在串行性(serializable)摸式下,所有的数据访问反映的数据库状态是事务开始的时间。这意味着同一个事务中所有查询看到的数据都和一个时间点保持一致,当然这个事务本身所做的改变自己可以看到。事务级别的读一致性会产生重复读,但不会产生幻影读。

    真正应用集群的读一致性

    真正应用集群(RAC)使用cache到cache的块传输机制(cache融合)来从一个实例到另一个实例传输块的读一致映像。RAC使用高速度、低等待时间的互联方式完成传输,满足远程对数据块的请求。

    Oracle隔离级别

    Oracle提供了3种隔离级别:

    隔离级别

    描述

    提交读(read commited)

    这是默认的事务隔离级别。事务中每个执行的查询只能看到查询(不是事务)开始之前提交的数据。Oracle查询不会读取脏(没有提交的)数据。

    因为Oracle不阻止其他事务通过查询修改本事务读取的数据,这个数据可以在本事务的两个查询之间由其他事务修改。因而,一个事务中运行给定的查询两次会出现幻影读和不可重复读现象。

    串行性(Serializable)

    串行性事务只能看到事务开始时提交的修改,和本事务通过INSERT、UPDATE和DELETE语句修改的数据。串行性事务不会出现不可重复读或幻影读。

    只读(read-only)

    只读事务只能看到事务开始时提交的修改,不允许执行INSERT、UPDATE、DELETE语句。

    设置隔离级别

    应用程序设计人员、应用程序开发人员和数据库管理员可以根据应用程序和负载情况来为不同的事务选择合适的隔离级别。你可以在事务开始时通过使用下列语句来设置事务隔离级别。

    SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; 

    SET TRANSACTION READ ONLY; 

    在每个事务开始用SET TRANSACTION语句设置隔离级别,会增加网络和处理负担,也可以使用ALTER SESSION语句来为所有以后的事务设定隔离级别:

    ALTER SESSION SET ISOLATION_LEVEL SERIALIZABLE; 

    ALTER SESSION SET ISOLATION_LEVEL READ COMMITTED; 

    提交读隔离级别

    Oracle默认隔离级别是提交读。这个隔离级别对于事务很少冲突的环境是合适的。Oracle让每个查询根据它自己的实体化视图时间运行,因而允许不可重复读和多次执行一个查询导致的幻影读,但会提供很高的性能。提交读隔离级别是事务很少冲突的环境的合适的隔离级别。

    串行性隔离级别

    串行性隔离级别适合如下环境:

    u 大数据库中存在影响少量行的小事务的情况下

    u 两个并行事务很少修改同一行的情况下

    u 相对较长时间运行的事务主要是只读的

    串行性隔离级别允许并行事务只能修改那些他们可以修改的部分,就如同事务被调度依次运行一样。明确来说,Oracle只有确定串行性事务开始时,其他事务修改一个行的修改已经提交,才会允许在这个数据行上使用串行性事务。

    为加快这个规则的效率,Oracle在数据块上保留控制信息,显示了块中哪些行包含提交的和未提交的数据。某种程度上来说,块包含最近影响块上每一行的事务的历史。可以保留的历史由CREATE TABLE和ALTER TABLE语句的INITRANS参数来控制。

    在某些情况下,Oracle没有足够的历史信息来确定是否一行被最近的事务修改过。当很多事务并发的修改同一个数据块或者在很短的时间发生会出现这种情况(没有足够的空间记录历史)。如果一个表经常被很多事务修改同一个块,你可以通过对表的INITRANS设置很高的值来避免这种情况(没有足够的空间记录历史)。这使得Oracle在每个块中分配足够的空间来记录最近访问块的事务的历史。

    如果串行性事务试图修改和删除其他在本事务开始之后的其他事务提交的数据,会产生一个错误:

    ORA-08177: Cannot serialize access for this transaction 

    当串行性事务因为Cannot serialize access错误而失败时,应用程序可以采取下列措施:

    u 提交这个时间点执行的工作

    u 执行另外(但是不同)的语句(可能回滚到事务早期构建的保存点)

    u 撤销整个事务

    图13-2 显示了Cannot serialize acess错误导致的失败时,一个应用程序回滚和重试事务的例子:

    图13-2 串行性事务失败

    提交读和串行性隔离级别的比较

    Oracle为应用开发人员提供了两种不同规格的隔离级别的选项。提交读和串行性隔离级别都提供很高级别的一致性和并发性。两个级别都为降低竞争而提供了读一致性多版本并发性控制模型和行级互斥锁的实现,并且都是为真实世界的应用部署设计的。

    事务系列一致性

    在Oracle中查看提交读和串行性隔离级别的一个很有用的办法是考虑下列场景:假定你有一系列数据库表(或者任何系列数据),这些表的一个特定顺序的行读,在任何特定时间提交的一系列事务。一个操作(查询或者事务)如果它的所有数据读是同一系列的提交事务写入的,那么这个操作是事务系列一致性的。如果它的数据读一部分由一系列事务影响,一部分由另一系列事务影响,那么它就不是事务系列一致性的。操作是否事务系列一致性实际上看是数据库所处状态是否反映了单个系列的提交事务。

    Oracle为提交读模式的事务执行的每个语句提供事务系列一致性。串行性模式岁每个事务提供事务系列一致性。

    图13-2汇总了Oracle中提交读和串行性事务的主要不同。

    图 13-2 提交读和串行性事务

    提交读

    串行性

    脏写(dirty write)

    不允许

    不允许

    脏读(dirty read)

    不允许

    不允许

    不可重复读(Nonrepeatable read)

    允许

    不允许

    幻影读(Phantoms)

    允许

    不允许

    遵从ANSI/ISO SQL 92

    读取实体化视图时间

    语句

    事务

    事务系列一致性

    语句级别

    事务级别

    行级别锁

    读阻塞写

    写阻塞读

    不同行写入阻塞写入

    同一行写入阻塞写入

    等待锁定事务

    导致cannot serialize access错误

    锁定事务结束产生错误

    锁定事务提交产生错误

    行级别锁

    提交读和串行性事务都使用行级锁,都会在试图修改一个没有提交的并行事务更新的行时产生等待。第二个事务事务等待其他事务提交或者撤销来释放它的锁,才能更新给定的行。如果其他事务回滚了,不管等待的事务是什么隔离级别,都可以对以前锁定的行进行修改,就好像其他事务不存在一样。

    但是,如果其他事务提交并释放了它的锁,提交读事务可以执行它想要的修改。一个串行性事务会产生一个Cannot serialize access错误而失败,因为其他事务提交的修改是串行性事务开始之后的事情了。

    引用完整性

    因为Oracle在读一致性或串行性事务不使用读锁,一个事务读取的数据可以被另外一个覆盖。在应用级别执行数据完整性检查的事务不能假设他们读取的数据在事务执行期间保持不变,即使这些改变对事务不可见也不行。即使在串行性事务的情况下,如果应用级别的一致性检查如果不用心编写,也会导致数据库不一致。

    注意:你可以在真正应用集群中使用提交读和串行性事务隔离级别。

    分布式事务

    在分布式数据库环境中,给定事务在多个物理数据库中更新数据的通过两步提交确保所有的节点都提交或者都没有提交。在这样的环境中,参加串行性事务的所有的服务器,不论是Oracle还是非Oracle,都要求支持串行性模式。

    如果串行性事务试图在一个不支持串行性事务的服务器的数据库的数据,事务会返回一个错误。事务会撤销,只有在远程服务器支持串行性事务时才进行新的尝试。

    相比之下,读一致性事务可以在不支持串行性事务的服务器之间执行分布式事务。

    隔离级别选项

    应用程序设计人员和开发人员需要依据应用程序性能、一致性需求和应用程序编码需求而选择一个隔离级别。

    在多并发用户快速提交事务的环境中,设计人员必须基于期望的事务发生率和需要的回应时间来评估事务性能。对于大多数高性能环境来说,隔离级别的选择会在并发性和一致性之间做一个平衡。

    检查数据库一致性的应用程序逻辑必须考虑一个事实:任何模式下读不阻塞写。

    Oracle隔离级别通过行级别锁和Oracle多版本并发控制系统提供了高水平的一致性、并发性和性能。Oracle中读写互相不阻塞。因而,虽然查询仍然看到一致性数据,不需要读取未提交数据,但是提交读和串行性隔离提供了高水平的高性能的并发。

    Oracle提交读隔离级别为每个查询提供事务系列一致性。就是说,每个查询看到的数据是一致性状态的。因而,读一致性隔离级别可以满足大部分应用程序,如果在其他没有使用多版本并发控制的数据库管理系统上,类似程序可能需要更高的隔离级别。

    提交读隔离模式并不会让应用程序陷入Cannot serialize access错误并循环重新开始一个事务。在大多数应用程序中,事务很少需要重复发布同一个查询,所以对于大部分应用程序来说,避免幻影读和不可重复读并不重要。因而很多开发人员选择提交读模式来避免在每个事务中编写这样的错误检查和重试代码。

    串行性隔离级别

    Oracle串行性隔离级别适合于两个并发事务很少修改同一行和长时间运行的事务主要是只读的环境。它还适合大型数据库且只更新少量行的短事务的环境。

    串行性隔离模式某种程度上通过避免不可重复读和幻影读来提供了更多的一致性,如果在一个读写事务中存在一个执行多次的查询,串行性隔离模式非常重要。

    和其他串行性隔离的实现不同,这些实现将读和写一样锁定。Oracle提供了非锁定查询和行级别锁的良好的颗粒度,上述两点降低了读写的争用。对于发生大量读写争用的应用来说,Oracle串行性隔离比其他系统可以提供更高的吞吐量。因而,适合Oracle串行性的应用程序未必适合其他系统。

    Oracle串行性事务的所有查询将数据库整个作为一个时间点,所以这个隔离级别适合在读/写事务中发布多个一致性查询。一个包含写入功能的报表应用程序指的是汇总数据并保存在数据库中,这样的程序可以使用串行性模式,因为它提供了READ ONLY事务提供的一致性,还允许INSERT、DELETE和UPDATE。

    注意:事务中的DML语句包含子查询应该使用串行性隔离来确保一致性读。

    编写串行性事务需要额外的工作,应用程序开发人员需要检查Cannot serialize acess错误并且撤销或重试事务。类似的额外代码在其他数据库管理系统中是用来管理死锁的。为适应公用标准或在多个数据库管理系统中运行的应用程序,可能需要为串行模式事务单独设计。检查串行性失败和重启的事务应该使用提交读模式,这样就不会产生串行性错误。

    串行性模式在相对较长事务更新的行和大量短更新事务访问的行一样时并不适合。因为长时间运行的事务未必是给定行的第一个更新,它需要重复回滚,浪费一些工作。注意,串行性模式的通常的读锁、悲观锁实在这种环境中也不适合,因为长时间运行的事务,即使是读锁,也会阻塞短更新事务的进度,反过来也是如此。

    在使用串行性模式时,应用开发人员需要考虑事务回滚和重试的开销。在读锁环境中,死锁经常发生,使用串行性模式需要通过结束事务来回滚工作,然后重试。在一个高争用的环境中,这种操作会占用大量资源。

    在大部分环境中,一个接收到Cannot serialize access错误而重启的事务未必会遇到另一个冲突的事务。出于这个原因,这些语句在串行性事务中尽早和其他事务竞争会比较有帮助。但是,并不能保证事务可以成功完成,所以应用程序应该编码来限制重试的次数。

    虽然Oracle串行性模式是兼容SQL92,并且和读锁实现比较还提供很多优势,但是它不能对那样的系统提供语法上的标识。应用设计人员必须考虑这样的事实:Oracle并不像其他系统一样读阻塞写。在应用程序级别检查数据库一致性的事务需要使用代码:SELECT FOR UPDATE。在其他使用串行性模式的事务的环境移植到Oracle可以考虑使用这个语句。

    静默数据库

    你可以将系统设置为静默数据库。处于静默状态的系统没有活动的会话,除非是用户SYS和SYSTEM。一个活动事务是指会话中包含一个事务、一个查询、一个数据获取或者一个PL/SQL过程,或者当前持有任何共享资源的会话(例如:队列共享着一些串行访问数据库资源的内存结构,且和一个会话或事务关联)。数据库管理员是仅有的可以在系统处于静默状态还可以操作的用户。

    数据库管理员可以在静默状态执行一个特定的操作,这些操作在非静默状态可能是不安全的。这些动作包括:

    u 如果存在并发用户事务或查询可能失败的动作。例如,如果一个并发事务访问的表的模式无法修改

    u 动作的中间结果对于用户并发事务或查询可能有害。例如,假设有一个大表T和一个PL/SQL包在上面操作。你可以将表T分成两个表T1和T2,修改PL/SQL包来引用信标T1和T2,而不是旧表T。

    当数据库处于静默状态,你可以这样做:

    CREATE TABLE T1 AS SELECT ... FROM T; 

    CREATE TABLE T2 AS SELECT ... FROM T; 

    DROP TABLE T; 

    然后你可以删除旧的PL/SQL包,重新创建它。

    对哪些必须持续工作的系统来说,不需要关闭数据库就能够执行这样动作的能力是很重要的。

    数据库资源管理器在系统静默状态时阻塞了除SYS和SYSTEM的所有新用户。这些动作在系统返回正常状态(非静默)时可以执行。用户不会从静默状态获得任何额外的错误信息。

    数据库如果设定为静默

    数据库管理员使用ALTER SYSTEM QUIESCE RESTRICTED语句来静默数据库。只有用户SYS和SYSTEM才能发布ALTER SYSTEM QUIESCE RESTRICTED语句。对于数据库开放状态的所有实例,发布这个语句有如下影响:

    u Oracle指示所有实例中的数据库资源管理器阻止所有除SYS和SYSTEM用户之外的所有非活动会话变为活动会话。除SYS和SYSTEM之外的用户无法启动一个新会话、新事务、新查询、新的数据获取或者新的PL/SQL操作。

    u Oracle等待所有实例中的非SYS或SYSTEM的用户所有现存事务完成(提交或者结束)。Oracle还等待所有非SYS或SYSTEM的用户运行的不在事务中的查询、数据获取和PL/SQL过程完成。如果查询由多个连续的OCI获取执行,Oracle并不等待所有的获取结束。它等待当前的获取结束,阻塞新的数据获取。Oracle还等待所有持有任何共享资源(比如队列)的所有会话(非SYS或者SYSTEM)释放这些资源。这些操作完成之后,Oracle将数据库置入静默状态,完成QUIESCE RESTRICTED语句的执行。

    u 如果实例运行在共享服务器模式,Oracle指示数据库资源管理器来阻塞这个实例上的登陆(非SYS和SYSTEM)。如果一个实例运行在非共享服务器模式,Oracle并不对实例的登陆施加任何限制。

    在静默状态期间,你不能修改任何实例的资源管理计划。

    ALTER SYSTEM UNQUIESCE语句将所有运行实例返回到正常状态,所以所有阻塞动作可以继续处理。一个管理员可以通过查询v$blocking_quiesce视图来确定锁定的会话。

    Oracle如何锁定数据

    锁是一种阻止访问同一资源的事务破坏性干扰的一种机制,资源可以是用户对象(如表和行)或用户不可见的系统对象(比如内存中的共享数据结构和数据字典行)。

    在所有的情况下,Oracle都是在SQL语句执行时自动获得需要的锁,所以用户不需要关心这些细节。Oracle自动使用最低的、适当的限制来提供高水平的数据并发,还提供数据完整性防护。Oracle还允许用户手动锁定数据。

    事务和数据并发

    Oracle在事务之间使用锁机制来提供数据并发性和完整性。因为Oracle的锁机制和事务控制紧密结合,应用设计人员只需要适当定义事务,Oracle自动管理锁。

    记住Oracle锁是完全自动的,不需要任何用户干预。所有SQL语句执行时隐式加锁,所以数据库用户不需要显式锁定任何资源。Oracle默认的锁机制在数据上提供低级别的限制来确保数据完整性,还允许高水平的数据并发性。

    锁模式

    Oracle在多用户数据库中使用两种模式的锁:

    u 排他锁:阻止相关资源被共享。修改数据自动获得这个锁模式。如果排他锁没有释放,排他锁定一个资源的第一个事务是仅有的一个可以修改该资源的事务。

    u 共享锁:允许相关资源被共享(和涉及的操作有关)。多个用户读取数据可以共享数据、持有共享锁来阻止并发的写入权限(需要排他锁)。多个事务可以在同一个资源上获得共享锁。

    锁持续期

    事务内语句获得的所有锁在事务存续期间持有,阻止并发事务的脏读、丢失更新和破坏性的DDL操作之类的破坏性干扰。一个事务的SQL语句造成的改变,只有提交之后其他的事务才能看到。

    在事务提交或者撤销时,Oracle会释放事务中的语句获得的锁。回滚到保存点时,Oracle也释放保存点后的锁。但是,只有事务不再等待以前锁定的资源时,才可以在现在可用的资源上获得锁。等待事务一直等待,除非原始事务完全提交或者回滚之后。

    数据锁转换和锁升级

    一个事务持有事务内插入、更新、删除行的排他锁。因为行锁是在最高级别的限制上获得,不需要执行锁转换。

    Oracle根据需要自动将低限制的表锁转换为一个高限制的锁。例如,假定一个事务使用了包含FOR UPDATE子句的SELECT语句来锁定了表的行。因而,它获得了表的排他行锁和行共享表锁。如果事务后来更新了一个或多个锁行,共享表锁的行自动转换为一个排他表锁的行。

    在大量同一级别的颗粒(如行)拥有锁时,一个数据库将锁升级为高水平的颗粒(如表),这个叫做锁升级。例如,如果一个用户锁定了一个表很多行,某些数据库自动逐步将用户行锁升级到单个表。锁的数量减少了,但是锁定的限制行增加了。

    Oracle不会升级锁。升级锁大大的增加了死锁的可能性。想象一种情况,系统试图在事务T1中升级锁,但是因为事务T2持有锁,而无法获得。如果事务T2也对同样的数据需要升级锁,就会发生死锁。

    死锁

    在两个或者更多的客户等待彼此的数据锁时就会发生死锁。死锁阻止了某些事务继续工作。图13-3是一个死锁中两个事务的假想说明。

    在图 13-3中,A点没有任何问题,因为每个事务都持有它视图更新的行的锁。每个事务不需要中断就可以处理。但是,每个事务都视图更新当前被其他事务持有的行。因而,死锁在B点发生,因为没有事务可以获得它执行或者结束的需要的资源。这是一个死锁,因为无论每个事务等待多久,冲突锁还是不释放。

    图13-3 死锁中两个事务

    死锁检测

    Oracle自动检测死锁情况,并通过回滚死锁中包含的一个语句来解决死锁,从而释放一系列冲突的行锁。遭受语句级别回滚的事务会返回对应的消息。回滚的语句属于检测到死锁的事务。通常,被告知的事务应该显式回滚,但也可以等待之后重试回滚语句。

    注意:在分布式事务中,本地死锁通过分析等待数据检测,全局死锁通过超时来检测。检测之后,分布式死锁和非分布式死锁的数据库和应用处理方式相同。

    死锁通常是事务显式的覆盖Oracle的默认锁时发生。因为Oracle本身没有锁升级,查询也不使用读锁,只使用行锁(而不是页锁),在Oracle中很少发生死锁。

    避免死锁

    如果访问同样表的事务按照同样的顺序锁定这些表,不管通过显式的还是隐式的,多数死锁都可以避免的。例如,所有的应用开发人员可以遵循下列规则:主表和子表都需要更新时,先锁定主表,再锁定子表。如果这类规则严格指定,所有应用程序都遵守,死锁就很少会发生。

    当你要为一个事务请求一系列锁,考虑首先获得最排他的锁(兼容性最好)。

    锁类型

    Oracle自动使用不同类型的锁来控制数据的并发访问,阻止用户之间的破坏性干扰。Oracle自动为事务锁定一个资源,阻止其他需要同一资源的共享锁的事务获得锁。在某些条件下,事务不再需要资源时,Oracle自动释放锁。

    在这个操作中,Oracle自动根据资源锁定情况和执行的操作在不同的限制级别上获得不同类型的锁。

    Oracle锁通常分为3类。

    描述

    DML锁(数据锁)

    DML锁保护数据。例如,表锁锁定整个表,行锁锁定选择行

    DDL锁(字典锁)

    DDL锁保护模式对象的结构,例如表和视图的定义

    内部锁和闩

    内部锁和闩保护内部数据库结构,比如数据文件。内部锁和闩是完全自动的

    下面开始讨论DML锁、DDL锁和内部锁。

    DML锁

    DML锁(数据锁)的目的是确保多个用户并发访问的数据的完整性。DML锁阻止了同时发生的冲突的DML或者DDL操作造成的破坏性干扰。DML语句自动获得行级锁和表级锁。

    注意:每类锁或锁类型后面括号里的缩写词是在企业管理器的锁监视器使用的缩略词。企业管理器可能用TM显示任何表锁,而不是显示表锁的类型(如RS或SRX)。

    行锁(TX)

    行级锁主要用于阻止两个事务同时修改一行。当事务需要修改一行时,需要获得一个行级锁。

    一个语句或者事务可以持有行锁的数目并没有限制,Oracle并不会将行级锁升级为粗粒度的锁。行锁提供了最好的颗粒度和最好的并发性和吞吐量。

    多版本并发控制和行级别锁定结合意味着只有用户访问同一行的数据时才会产生竞争,具体如下:

    u 读数据不需要等待同一行的写入

    u 写入数据不需要等待同一行数据的读取,除非使用了SELECT .... FOR UPDATE语句,这个语句明确的请求了读锁。

    u 写入只在其他写入试图同时更新同一行数据时才会等待

    注意:读数据可能在某些特殊情况下(分布式事务中)等待同一数据块的写入。

    一个事务通过下列语句修改的每行都会获得一个行级互斥锁:INSERT、UPDATE、DELETE和SELECT ... FOR UPDATE。

    行修改总是排他锁,所以其他的事务无法修改这一行,除非持有锁的事务提交或者回滚了。但是,如果事务由于实例失败而死掉,块级别的恢复会在整个事务恢复之前使行可用。Oracle上述语句的情况下自动获得行级锁。

    如果一个事务在一行上获得一个行锁,事务也在对应表上获得一个表锁。这个表锁阻止了冲突的DDL操作,这个DDL操作可能覆盖当前事务中的数据修改。

    表锁(TM)

    表级锁主要用于并发控制并发的DDL操作,比如阻止在一个DML操作过程中删除相关表。当在一个表上执行DDL或DML语句,就获得一个表锁。表锁不影响DML操作的并行性。对于分区表,表锁在表和子分区级别上都可以获得。

    一个事务在一个表使用下列DML语句:INSERT、UPDATE、DELETE、SELECT....FOR UPDATE和LOCK TABLE时获得一个表锁。这些DML操作获得表锁有两个目的:为事务保留表的DML访问权限和阻止和这个事务冲突的DDL操作。任何表锁都阻止了同一个表上的排他DDL锁的获得,从而阻止了需要这个锁的DDL操作。例如,如果一个没有提交的事务持有一个表的表锁,表不能修改结构和删除。

    表锁可以以多种方式持有:行共享(row share,RS)、行排他(row exclusive,RX)、共享行排他(share row exclusive,SRX)、和排他锁(exclusive,X)。一个表的锁模式的限制确定了同一表上可以获得和持有的其他表锁。

    表 13-3显示了语句获得表锁模式和这表锁允许和禁止的操作。

    表 13-3 表锁的汇总

    SQL语句

    表锁模式

    是否允许下列锁类型

    RS

    RX

    S

    SRX

    X

    SELECT ..... FROM table

    None

    Y

    Y

    Y

    Y

    Y

    INSERT INTO table ....

    RX

    Y

    Y

    N

    N

    N

    UPDATE table ....

    RX

    Y*

    Y*

    N

    N

    N

    DELETE FROM table .....

    RX

    Y*

    Y*

    N

    N

    N

    SELECT .... FROM table FOR UPDATE OF ...

    RS

    Y*

    Y*

    Y*

    Y*

    N

    LOCK TABLE table IN ROW SHARE MODE

    RS

    Y

    Y

    Y

    Y

    N

    LOCK TABLE table in ROW EXCLUSIVE MODE

    RX

    Y

    Y

    N

    N

    N

    LOCK TABLE table in SHARE MODE

    S

    Y

    N

    Y

    N

    N

    LOCK TABLE table in SHARE RWO EXCLUSIVE MODE

    SRX

    Y

    N

    N

    N

    N

    LOCK TABLE table in EXCLUSIVE MODE

    X

    N

    N

    N

    N

    N

    RS:行共享锁

    RX:行排他锁

    S:共享锁

    SRX:共享行排他锁

    X:排他锁

    *:如果没有其他事务持有冲突的行锁就为是,否则发生等待。

    下面开始说明表锁的每个模式,从最少限制到最多限制。还描述了在那个模式下导致事务获得表锁的动作和这种锁模式下其他事务允许和禁止的动作。

    行级共享表锁(RS)

    行级共享表锁(有时候也叫部分共享表锁)指出持有表上锁的事务已经在表上锁定了行,要更新他们。一个行级表共享锁在下列SQL语句运行时自动获得:

    SELECT .... FROM table .... FOR UPDATE OF .....;

    LOCK TABLE table IN ROW SHARE MODE;

    行级共享表锁是最少限制的表锁,为表提供最高水平的并发性。

    允许的操作:持有行共享表锁的事务允许其他事务并发的在同一表上执行查询、插入、更新、删除或者锁定行。因而,其他事务可以在同一个表上同时获得行共享、行排他、共享、共享行排他表锁。

    禁止的操作:持有行共享表锁的事务阻止其他事务使用下面的语句使用排他写访问同一个表:

    LOCK TABLE table IN EXCLUSIVE MODE;

    行排他表锁(RX)

    一个行排他表锁(也叫做部分排他表锁,SX)通常显示持有锁的事务已经在表上更新了一行或多行。一个行排他表锁在使用下列语句修改表时自动获得:

    INSERT INTO table ....;

    UPDATE table ...;

    DELETE FROM table ....;

    LOCK TABLE table in ROW EXCLUSIVE MODE;

    行排他表锁比行共享表锁的限制多一些。

    允许的操作:一个事务持有的行排他表锁允许其他事务在同一个表上并发的查询、插入、更新、删除或锁定行。因而,行排他表锁允许多个事务在一个表上同时拥有表排他锁和表共享锁。

    禁止的操作:一个事务持有的行排他表锁阻止其他事务手工锁定表来执行读取或写入。因而,其他事务不能使用下面的语句并发的锁定表:

    LOCK TABLE table IN SHARE MODE;  

    LOCK TABLE table IN SHARE EXCLUSIVE MODE; 

    LOCK TABLE table IN EXCLUSIVE MODE; 

    共享表锁(S)

    对表执行下面的语句会自动或共享表锁:

    LOCK TABLE table IN SHARE MODE;

    允许的操作:一个事务持有的共享表锁允许其他事务只能查询表、使用SELECT .... FOR UPDATE来锁定特定行或者运行LOCK TABLE ... IN SHARE MODE语句。不允许其他事务进行任何更新。多个事务可以并发的持有同一个表的共享表锁。在这种情况下,没有事务可以更新表(即使一个事务使用SELECT ... FOR UPDATE语句持有行锁也不行)。因而,一个持有共享表锁的事务只有在没有其他事务持有这个表的共享表锁的情况下更新表。

    禁止的操作:一个事务持有的共享表锁阻止其他事务修改这个表和阻止执行下面的语句:

    LOCK TABLE table IN SHARE ROW EXCLUSIVE MODE; 

    LOCK TABLE table IN EXCLUSIVE MODE; 

    LOCK TABLE table IN ROW EXCLUSIVE MODE; 

    共享行排他表锁(SRX)

    共享行排他表锁(也叫共享部分排他表锁,SSX)比共享锁限制多一些。使用下面的语句可以获得一个共享行排他表锁:

    LOCK TABLE table IN SHARE ROW EXCLUSIVE MODE;

    允许的操作:给定一个表同时只有一个事务可以获得共享行排他表锁。一个事务持有的共享行排他表锁允许其他事务查询或者使用SELECT ... FOR UPDATE语句来锁定特定行,但不允许更新表。

    禁止的操作:一个事务持有的共享行排他表锁阻止其他事务获得行排他表锁和修改这个表。还阻止其他事务获得共享锁、共享行排他锁、排他表锁,这些阻止其他事务执行下列语句:

    LOCK TABLE table IN SHARE MODE; 

    LOCK TABLE table IN SHARE ROW EXCLUSIVE MODE; 

    LOCK TABLE table IN ROW EXCLUSIVE MODE; 

    LOCK TABLE table IN EXCLUSIVE MODE; 

    排他表锁(X)

    排他表锁是限制最多的表锁,允许持有锁的事务排他的写入到表中。一个排他表锁使用下列语句可以获得:

    LOCK TABLE table IN EXCLUSIVE MODE;

    允许的操作:只有一个事务可以获得表的排他表锁,一个排他表锁只允许其他事务查询表。

    禁止的操作:一个事务持有的排他表锁禁止其他事务执行任何类型的DML操作或者阻止在表上放置任何类型的锁。

    DML语句自动获得的锁

    前面的部分描述了不同类型的数据锁、可以获得那种锁类型、什么时候可以获得锁、什么时候获得锁和禁止什么操作。下面汇总了Oracle如何为不同的DML操作自动锁定数据。

    在下面的部分,表 13-4汇总了信息。

    表13-4 DML语句获得的锁

    DML 语句

    行锁

    表锁类型

    SELECT ... FROM table

    INSERT INTO table ...

    X

    RX

    UPDATE table ...

    X

    RX

    DELETE FROM table ...

    X

    RX

    SELECT ... FROM table ...    FOR UPDATE OF ...

    X

    RS

    LOCK TABLE table IN ...

    ROW SHARE MODE

    RS

    ROW EXCLUSIVE MODE

    RX

    SHARE MODE

    S

    SHARE EXCLUSIVE MODE

    SRX

    EXCLUSIVE MODE

    X

    X:排他锁

    RX:行排他锁

    RS:行共享锁

    S:共享锁

    SRX:共享行排他锁

    查询的默认锁

    查询是很少干扰其他SQL语句的一种语句,因为它只读取数据。INSERT、UPDATE和DELETE语句可以包含隐式的部分查询语句。查询包括下列类型的语句:

    SELECT

    INSERT .... SELECT ....;

    UPDATE ...;

    DELETE ...;

    查询不包括下列语句

    SELECT ... FOR UPDATE OF ...;

    不使用FOR UPDATE语句的所有查询包含下列特征:

    u 查询没有任何数据锁。因而,其他事务可以查询和修改正在查询的表,包括被查询的特定行。因为查询没有FOR UPDATE子句所以没有获得任何数据锁来阻塞其他操作,这些查询在Oracle中常被称为非阻塞查询。

    u 查询不需要等待任何数据锁释放。它总是执行。(查询在分布式事务的情况下可能需要等待数据锁)

    INSERT、UPDATE、DELETE和SELECT ... FOR UPDATE的默认锁

    INSERT、UPDATE、DELETE和SELECT ... FOR UPDATE语句的锁特征如下:

    u 包含DML语句的事务在语句修改的行上获得排他行锁。其他事务不能更新或删除锁定行,除非锁定事务提交或者回滚。

    u 包含DML语句的事务不需要对子查询和WHERE子句的隐式查询选择的任何行加锁。DML语句的子查询或隐含查询确保查询开始的一致性,而且看不到所属DML语句的影响。

    u 事务中的查询可以看到同一事务中前一个DML语句的改变,但不能看到事务开始之后开始的其他事务所做的修改。

    u 除必须的排他行锁外,包含一个DML语句的事务至少在包含影响行的表上获得一个行排他表锁。如果包含的事务已经持有一个这个表的共享锁、共享行排他锁或者排他表锁,就无法获得行排他表锁。如果包含事务已经持有一个行共享表锁,Oracle自动将锁传唤为行排他表锁。

    DDL锁

    正在执行的DDL操作引用和操作的模式对象使用数据字典锁(DDL)来保护。前面说过,DDL语句隐式的提交当前事务。例如,假定用户创建一个过程。对于用户的单语句进程来说,Oracle自动对过程定义的所有引用的模式对象获得DDL锁。DDL锁阻止了过程编译完成之前其中的引用对象被修改结构或者删除的操作。

    Oracle自动基于任何DDL事务需要的字典锁。用户不能显式的请求DDL锁。只有在DDL操作中引用和修改的模式对象可以获得锁。整个数据字典从来不会锁定。

    DDL锁分为3类:排他DDL锁,共享DDL锁,可中断解析锁。

    排他DDL锁

    大多数DDL操作(除了下面的“共享DDL锁”列出的操作)需要资源的排他DDL锁,阻止其他的可能修改和引用同一个模式对象的DDL操作的破坏性干扰。例如,在使用ALTER TABLE操作来为表增加一行时,不能使用DROP TABLE操作来删除表,反过来也是一样。

    在请求一个排他DDL锁时,如果另外一个操作已经持有了这个模式对象的DDL锁,需要等待老的DDL锁释放才能获得锁。

    DDL操作还在修改的模式对象上获得DML锁(数据锁)。

    共享DDL锁

    某些DDL操作需要请求一个资源的共享DDL锁,阻止冲突DDL操作的破坏性干扰,但允许类似DDL操作的数据并发。例如,当CREATE PROCEDURE语句运行时,包含的事务获得所有引用表的共享DDL锁。其他事务可以并发的创建引用同样表的过程,因而也在同样的表上获得并发的共享DDL锁,但是没有事务可以获得任何引用表的排他DDL锁。所以,持有共享DDL锁的事务确保引用的模式对象的定义在事务执行期间不会改变。

    一个共享DDL锁可以由包含下面的语句的DDL语句在模式对象上获得:

    AUDIT, NOAUDIT, COMMENT, CREATE [OR REPLACE] VIEW/ PROCEDURE/PACKAGE/PACKAGE BODY/FUNCTION/ TRIGGER, CREATE SYNONYM, 和CREATE TABLE (不包括CLUSTER参数)。

    可中断解析锁

    共享池中的SQL语句或PL/SQL程序单位持有每个引用模式对象的解析锁。获得一个引用对象的解析锁,所以如果这个对象被修改或者删除,相关的共享内存区域就无效了。解析锁不禁止任何DDL操作,可以被任何冲突的DDL操作中断,所以叫可中断解析锁。

    解析锁在SQL语句解析执行期间获得,只要共享池中这个语句的共享内存区域存在就一直持有。

    DDL锁的生存周期

    一个聚簇上的DDL操作在其中的聚簇和所有的表和实体化视图上获得排他DDL锁。聚簇中的表或实体化视图的DDL操作在聚簇上获得共享锁,在表或实体化视图上获得共享或排他DDL锁。聚簇上的DDL锁阻止其他操作在第一个操作执行时删除聚簇。

    闩和内部锁

    闩和内部锁保护了内部的数据库和内存结构。对用户来说都是不可访问的,因为用户不需要控制它们的存在和生存周期。下面解释一下企业管理器的LOCKS和LATCHES监视器。

    闩是简单、低级别的串行性机制,用来保护系统全局区(SGA)中的共享数据结构。例如,闩保护了当前访问数据库的用户列表和保护描述高速缓存中的块的数据结构。服务器或后台进程在处理和查看这些结构之一时,只需要获得闩很短的时间。闩的实现是依赖操作系统的,特别是一个进程是否会等待闩和会等待闩多长时间。

    内部锁

    内部锁是比闩高级别的、更复杂的机制,服务于多个不同的目的。

    字典缓存锁

    这些锁是字典缓存的条目在它被修改或者使用时持有时间很短的一种锁。它们确保语句解析时不会看到不一致的对象定义。

    字典锁可以是共享或排他的。共享锁在解析完成时释放,排他锁在DDL操作结束时被释放。

    文件和日志管理锁

    这些锁用来保护不同的文件。例如,一个锁保护控制文件,所以只有一个时间点只能有一个进程修改它。另一个锁调节重做日志文件的使用和归档。数据文件加锁来确保多个实例以一个共享模式挂接数据库,还是一个实例以排他模式挂接数据库。因为日志和文件锁显示文件的状态,这些锁通常会被持有很长时间。

    表空间和回滚段锁

    这些锁保护表空间和回滚段。例如,多个访问数据库的实例必须对于一个表空间在线还是离线达成一致。回滚段加锁所以只有一个实例可以写入回滚段。

    显式(手动)数据锁

    Oracle通常自动执行锁来确保数据并发、数据完整性和语句级别读一致性。但是,你可以覆盖Oracle的默认锁机制。覆盖默认锁机制在下列情况下比较有用:

    u 应用程序需要事务级别读一致性或者重复读。换句话说,它们中的查询需要在事务存续期间必须产生一致性数据,不反映其他事务的改变。你可以通过覆盖默认的锁,或者使用显式锁、只读事务、串行性事务来获得事务级别读一致性。

    u 应用程序需要一个事务拥有一个资源的排他锁,所以事务不需要等待其他事务的完成。

    Oracle自动锁可以在事务级别或者会话级别覆盖。

    在事务级别,包含下列SQL语句的事务覆盖了Oracle默认锁:

    u SET TRANSACTION ISOLATION LEVEL语句

    u LOCK TABLE语句(锁定表或者使用视图时的基表)

    u SELECT ... FOR UPDATE语句

    事务提交或者回滚之后释放这些语句获得的锁。

    在会话级别,会话可以使用ALTER SESSION语句来设置需要的事务隔离级别。

    注意:如果在任何级别上覆盖了Oracle默认锁,数据库管理员或者应用开发人员应该确保覆盖锁的过程正确操作。锁的过程必须满足下列准则:确保数据完整性、接受数据并发性、不存在死锁或者有但可以适当的处理。

    Oracle锁管理服务

    通过Oracle锁管理服务,应用开发人员可以在PL/SQL块中包含下列语句:

    u 请求一个特定类型的锁

    u 给另一个过程的锁一个可辨识的唯一名字,可以在同一个实例或者另一个实例

    u 修改锁类型

    u 释放锁

    因为一个保留的用户锁和一个Oracle锁相同,它也有所有的Oracle锁功能,包括死锁检测。用户锁和Oracle锁从不冲突,因为它们的前缀为UL。

    Oracle锁管理服务通过DBMS_LOCK包中的过程使用。

    Oracle闪回查询概述

    Oracle闪回查询允许你显示和修改历史数据。你可以在数据库中使用特定的挂钟时间或者用户指定的系统修改号(SCN)来执行查询。

    闪回查询使用Oracle的多版本度读一致性来根据需要应用undo来恢复数据。Oracle数据库10g自动调整一个叫撤销持续周期的参数。撤销持续周期指定了旧undo信息(提交事务的undo信息)在覆盖之前必须保留的时间。数据库收集使用统计,基于这些统计信息和undo表空间大小来调整撤销持续周期。

    使用闪回查询可以查询数据库中早上、昨天或者上周存在的数据。这个操作的速度仅仅依赖于查询的数据数量和数据需要回退的修改次数。

    你可以查询一个给定行或事务的历史。使用保存在数据库中的undo数据,你可以显示一行的所有版本和返回这一行以前的版本。闪回查询历史让你可以在事务级别检查数据库的修改。

    你可以审计表的行和获得修改行的事务信息以及行修改时间。通过事务ID,你可以通过LogMiner进行事务挖掘来获得完整的事务信息。

    你可以设置你想显示的日期和时间,那么在那个时间,任何你运行的SQL查询在数据上的操作都存在。如果你是个认证用户,那么你可以修复错误,在不需要管理员介入的情况下回退到要恢复的数据。

    通过SQL语句AS OF,你可以在查询中为每个表选择不同的快照。将表和快照关联就是表修饰。如果不使用快照来修改表,就会使用默认的快照。所有没有指定快照的表获得的都是相同的默认快照。

    例如,假定你想写一个查询来查找过去一个小时创建的新用户帐号。你可以在使用不同的AS OF子句修饰同一个表的两个实例上实施操作。

    DML和DDL操作可以使用表修饰来在子查询中选择快照。例如INSERT TABLE AS SELECT和CREATE TABLE AS SELECT的查询可以在子查询中使用表修饰来修复表中误删除的行。表修改可以是任何表达式:绑定变量、常量、字符串、日期操作等等。你可以打开一个游标,并动态绑定一个快照值(时间戳或SCN)来修饰表。

    闪回查询的优势

    下面列举了使用闪回查询的优势:

    u 应用透明

    打包程序,比如报表生成工具只需要查询,可以使用登陆触发器在闪回查询模式运行。应用程序可以透明运行而不需要修改编码。应用程序需要满足的约束是确保有效,因为截止闪回查询时间只有一个一致性数据库版本。

    u 应用程序性能

    如果应用程序需要恢复动作,它可以通过保存的SCN和闪回到这些SCN就可以了。这比保存数据集然后恢复他们要容易的多和快的多,前者在应用程序要需要回到指定的版本才需要。使用闪回查询,不需要耗费指定版本导致的日志记录成本。

    u 在线操作

    闪回查询是一个在线操作。闪回查询的引用的对象可以被其他会话并发的DML和查询。这些操作的速度不受影响。而且,不同的会话可以在同一个对象上并发的闪回到不同的闪回时间或SCN。闪回查询的速度依赖于需要应用的undo量,这个和查询要回退的时间成正比。

    u 容易管理

    相关用户使用闪回查询不需要额外的管理,除了适当的持续周期、拥有足够的权限等等之外,不需要额外的权限,因为过去的版本按照需要自动组织。

    注意:

    u 闪回查询不撤销任何操作。它只是一个查询机制。在一些环境中,你可以从闪回查询获得输出,自己执行撤销。

    u 闪回查询并不告诉你改变了什么。LogMiner做这个事情。

    u 闪回查询不撤销修改,在你知道行需要回退的具体时间时效率很高。你可以使用它来将这个表回退到一个时间,但是如果表比较大,这是非常昂贵的,因为它包含一个全表拷贝。

    u 闪回查询不支持DDL操作:修改行、删除或截断表。

    u LogMiner对于获得修改历史非常优秀,但是它给你的是基于操作的(插入、更新、删除),而不是基于一行的前后映像。这对于某些应用来说处理起来非常困难。

    闪回查询的默写应用

    这部分列出了使用闪回查询的一些方式:

    u 自我修复

    你可以偶然删除了一些重要的行,想恢复被删除行。你可以回退到删除的时间,来查看丢失的数据,然后将删除的行插入当前表中。

    u 电子邮件或语音邮件应用程序

    你可能已经删除了过去的邮件。使用闪回查询,你可以回退到以前的时间来恢复删除的邮件,将删除的信息重新插入到当前邮箱

    u 帐号余额

    你可以在截止一个月的特定日期显示帐户余额。

    u 打包应用程序

    打包应用程序(比如报表生成工具)可以不需要修改任何应用程序逻辑就可以使用闪回查询。应用程序期望的所有约束都满足,因为用户截止给定时间或SCN可以看到数据库的一致性版本。

    另外,闪回查询可以在审计信息检查后用来查看数据的前映像。在DSS环境中,它可以从OLTP系统中截止一个一致的时间点抽取数据。

  • 相关阅读:
    【URL重写】IIS7配置URL重写
    【IIS7.5】Asp文件上传限制,加载页面大小限制
    msxml3.dll 错误 '800c0005' 系统错误: -2146697211。
    【转】修改3389远程端口的批处理文件.bat
    第一篇:无角牛MVC通用后台数据库设计
    无角牛MVC通用后台
    个人收集资料整理-WebForm
    个人收集资料整理-WinForm
    win7系统中桌面图标显示不正常问题
    ASP.NET MVC 第六回 过滤器Filter
  • 原文地址:https://www.cnblogs.com/xuzhiwei/p/3974198.html
Copyright © 2020-2023  润新知