Oracle 锁
Posted Jane Chiu
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Oracle 锁相关的知识,希望对你有一定的参考价值。
锁的概述
锁简述
锁(lock)机制是所有数据库都具有的一个关键特性,用于管理对共享资源的并发访问,数据库使用锁是为了支持对共享资源进行并发访问,同时提供数据完整性和一致性。
Oracle可以通过行级锁对表数据进行锁定,也在其他级别上使用锁,从而实现对多种不同的资源提供并发访问。比如,执行一个存储过程时,过程本身会以某种模式锁定,以允许其他用户执行这个过程,但是不允许另外的用户以任何方式修改这个过程。这也解释了一开始所述的请求运行时package重编译失败的原因。
丢失更新
丢失更新是一个经典的数据库问题,可以用来很好的阐述某些情况不使用锁会带来的不好后果。
(1) 会话session1中的 一个事务获取了一行数据,并显示给用户user1;
(2) 会话session2中的另一个事务也获取这一行,显示给用户user2;
(3) User1修改了这一行并提交,成功;
(4) User2 紧随其后也修改了这一行并提交,成功。
这个过程就是丢失更新,因为第(3)步做的所有修改都会丢失。这就是没有考虑锁定的原因。
因此,在实际的编码过程中,但凡涉及到update表内容,都需要对数据进行锁定,防止丢失更新的情况。
数据的锁定分为两种策略:悲观锁定和乐观锁定。
悲观锁定(pessimistic locking)
所谓的悲观锁定即是我们常用的句式:select … for update (nowait)~在试图更新之前,查找到数据的那一刻,因为我们很悲观,认为一定会有其他用户对数据进行更改,因此我们提前把数据先锁住,不允许其他会话更新。查询时主动加锁。
常用句式:游标锁表
PROCEDURE aa IS
CURSOR csr_lock(p_org_id NUMBER) IS
SELECT '1' FROM cux.cux_2_fin_text_messages_all ftm WHERE ftm.org_id = p_org_id
FOR UPDATE NOWAIT;
BEGIN
OPEN csr_lock(rec_data.org_id);
CLOSE csr_lock;
...
EXCEPTION WHEN app_exception.record_lock_exception THEN
dbms_output.put_line('资源已被加锁,请稍后再试');
END aa;
乐观锁定(optimistic locking)
乐观锁定,即把所有锁定都延迟到即将执行更新之前才做,换句话说,我们会修改信息而不使用锁。我们乐观的认为在我们修改信息之前,数据没有被其他用户修改。只会在修改的前一刻去确认一下记录是否有被其他人所更改,如果发现数据已经被修改过,则返回提示消息给用户,用户重新决定如何操作。
乐观锁定多使用版本戳的方式,通过对数据版本的比较,判断其他用户是否对数据做出更改。通过版本戳的方式,可以在应用中同时保留旧值(查询出来的值)和新值(数据库中的当前值),更新时使用如下语句:
Update table
Set column1 = :new_column1, column2 = :new_column2, ….
Where primary_key = :primary_key
And decode (column1, :old_column1, 1) = 1
And decode (column2, :old_column2, 1) = 1
…
如果更新了0行,说明该记录已被其他用户更新。相对完整的过程如下:
(使用SQL%NOTFOUND可以捕获到更新失败(即更新0行)的记录)
DECLARE
no_result EXCEPTION;
BEGIN
UPDATE TABLE t
SET column1 = :new_column1, column2 = :new_column2
WHERE primary_key = :primary_key
AND decode(column1, :old_column1, 1) = 1
AND decode(column2, :old_column2, 1) = 1;
IF SQL%NOTFOUND THEN
RAISE no_result;
END IF;
EXCEPTION
WHEN no_result THEN
dbms_output.put_line('你的数据更新语句失败了!');
WHEN OTHERS THEN
dbms_output.put_line(SQLCODE || '---' || SQLERRM);
END;
常用的实现乐观锁定的方法:
使用真实列充当版本列,使用虚拟列记录散列值或校验和;
使用版本列的乐观锁定
可以对每个要保护的表增加单独一列,一般是number或者DATE/TIMESTAMP列,每次记录修改的时候,同时修改该列存储的值;
在实现乐观并发控制的时候,应用只需要验证更新的那一刻,数据库中这一列的值与最初读出的值是否匹配,如果两个值相等,就说明这一行未被更新过。
create table dept
(deptno number(5),
loc varchar2(10),
last_mod timestamp with time zone default systimestamp not null,
constraint dept_pk primary key(deptno));
数据类型:TIMESTAMP WITH TIME ZONE(Oracle 9i及以上版本才有这个数据 类型),默认当前系统时间。TIMESTAMP数据类型在oracle中精度是最高的,通常可以精确到微秒(百万分之一秒);
该字段的维护方法:使用触发器或者在应用中进行维护。使用触发器会引入大量开销,因此考虑在应用中进行维护。为了避免应用程序在更新表内容时有所遗漏,可以将更新的过程封装到一个存储过程中,而不要让应用直接更新表。
使用校验和的乐观锁定
校验和是通过使用基数据本身来计算一个“虚拟的”版本列,通过使用散列函数获取一个散列值(hash value),散列值充当输入数据的一个唯一标示符,可以使用散列值来验证数据是否被修改。 可使用虚拟列来存储对应的散列值。
注意:散列值或校验和并非真正唯一,只能说通过适当的设计,能使出现冲突的可能性相当小,两个随机的串有相同校验和或散列值的可能性极小,可足以忽略不计。
下面介绍几种计算散列和校验和的方法,均是oracle提供的数据库包;
OWA_OPT_LOCK.CHECKSUM: 在Oracle 8i 8.1.5及以上版本中提供。给定一个串,其中一个函数会返回一个16位的校验和。给定ROWID时,另一个函数会计算该行的16位校验和,而且同时将这一行锁定。出现冲突的可能性是65536分之一。
DBMS_OBFUSCATION_TOOLKIT.MD5:在Oracle 8i 8.1.7 及以上版本提供,它会计算一个128位的消息摘要,冲突的可能性是3.4028E+38分之一(非常小);
DBMS_CRYPTO.HASH: 10g及以上版本提供,它会计算一个SHA-1(安全散列算法1)或MD4/MD5消息摘要。
ORA_HASH: 也是10g及以上版本才有,这是一个内置的 SQL算法,取一个varchar2值作为输入,还可以有另外一对输入(可选)来控制返回值,返回的是一个数值,默认为0到4294967295间的一个数。
以ORA_HASH举例:
第一步:创建测试表emp,其中sal_hash是虚拟列,以ora_hash计算两列的散列值;同时向emp表中插入一组测试数据;
SQL> CREATE TABLE EMP
2 (
3 EMPNO NUMBER(6),
4 SAL NUMBER(8),
5 COMM NUMBER(8),
6 SAL_HASH AS (ora_hash(sal||'/'||comm))
7 );
Table created
SQL> insert into emp(empno,sal,comm) values (10,100,100);
1 row inserted
SQL> select * from emp;
EMPNO SAL COMM SAL_HASH
------- --------- --------- ----------
10 100 100 2898671969
第二步:声明一个变量hash,存储当前列的散列值;该hash值可作为当前行状态的一个标签。
SQL> variable hash number;
SQL> begin select sal_hash into :hash from emp where empno = 10; end;
2 /
PL/SQL procedure successfully completed
hash
---------
2898671969
第三步:更新该行数据。重新查询,计算虚拟列sal_hash的值;
SQL> update emp set sal= 200 where empno=10 and sal_hash = :hash;
1 row updated
SQL> select * from emp ;
EMPNO SAL COMM SAL_HASH
------- --------- --------- ----------
10 200 100 86852855
可以发现,因为更新了sal列导致sal_hash的散列值发生了变化。
如果ora_hash中的参数发生了变化,其对应的散列值就会发生改变。通过对比前后散列值的变化,我们可以知道该行记录是否有被其他人所修改。
第四步:再次更新该行数据,会发现,因为hash变量中存储的散列值与数据库中当前的数据不一致,更新失败;
SQL> update emp set sal= 100 where empno=10 and sal_hash = :hash;
0 rows updated
要让基于散列的方法正常工作,就必须确保每个应用计算散列值使用相同的方法,以你可以为表增加一个虚拟列(11g及以上)。因为是虚拟列,不会带来任何存储开销,这一列的值不会计算并存在磁盘上,只有从数据库中获取数据时才会计算。
关于虚拟列的介绍请看后面大节。
要注意的是:计算散列或校验和是一个CPU密集型操作(相当占用CPU),计算代价昂贵。如果系统上CPU是稀有资源,需要充分考虑到这一点。
悲观锁定还是乐观锁定
悲观锁定在Oracle中工作得非常好,与乐观锁定相比,有很多优点。不过,它需要与数据库有一条有状态的连接,如客户/服务器连接,因为无法跨连接持有锁。对于具有庞大用户量的系统,要在整个事务期间都保持连接,代价略大。
乐观锁适用于写比较少的情况,即冲突很少发生,这样可以省去锁的开销,加大系统的整个吞吐量。
两种锁各有优缺点,在实际中根据情况决定。使用乐观锁定可考虑使用版本列+时间戳列的方式,这种方法与散列或校验和的方法相比,代价不那么昂贵。
目前,我所在的雪花项目多采用悲观锁定的方式。
阻塞(blocking)
如果一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞;
5条常见的DML语句可能会阻塞,具体是:INSERT,UPDATE,DELETE,MERGE和SELECT FOR UPDATE.
(1) INSERT阻塞:比如表的主键约束,如果有两个会话试图用同样的主键值插入一行。数据库会报错:违反唯一性约束条件。
(2) 阻塞的Merge、Update和delete:
一个交互式应用中,可以从数据库查询某个数据,允许最终用户去处理这个数据,再把它“放回”到数据库中,此时如果出现了Update或者Delete阻塞,说明可能存在丢失更新的问题,如果是在过程代码中,那么该段代码可能会出现bug。
为防止出现丢失更新的情况,我们在UPDATE或者DELETE数据之前,可使用SELECT FOR UPDATE NOWAIT,进行数据的手工锁定;该条查询查询的作用有二:
a、验证自从你查询数据之后数据未被更改,可防止丢失更新;
b、锁住行,防止UPDATE或者DELETE被阻塞;
不论是悲观锁定还是乐观锁定都可以利用select for update nowait查询来验证行未被修改。悲观锁定会在用户有意修改数据的那一刻使用这条语句,乐观锁定则在即将在数据库中更新数据时使用这条语句。
MERGE是在9i及更高级版本中采用的语法,是将INSERT和UPDATE合并的用法。
通过MERGE语句,根据一张表或者子查询的连接条件对另一张表进行查询,匹配上的进行update,无法匹配的则执行insert;
这个语法仅需要一次全表扫描就完成了全表工作,执行效率要高于insert+update;
merge语法:
MERGE INTO table_name
USING TABLE | view | subquery
ON (condition)
WHEN MATCHED THEN merge_update_clause
WHEN NOT MATCHED THEN merge_insert_clause;
merge实例,根据doctor表去更新表users的内容。
MERGE INTO users
USING doctor
ON (users.user_id = doctor.doctorid)
WHEN MATCHED THEN
UPDATE SET users.user_name = doctor.doctorname
WHEN NOT MATCHED THEN
INSERT VALUES (doctor.doctorid, doctor.doctorid, doctor.doctorname, SYSDATE, 'T');
在10g中,merge有增加一些新特性:
(1) Update或insert子句是可选的;
(2) Update和insert子句可以加where子句,只对满足where条件的数据加以处理;
(3) 在ON条件中使用常量过滤谓词来insert所有的行到目标表中,不需要连接源表和目标表;
MERGE INTO users
USING doctor
ON (1 = 0)
WHEN NOT MATCHED THEN
INSERT VALUES (doctor.doctorid, doctor.doctorid, doctor.doctorname, SYSDATE, 'T');
它的作用是将doctor表内容全部插入到users表中,这种用法等同于直接使用insert into;
(4) Update子句后面可以跟DELETE子句来去除一些不需要的行;
delete只能和update配合,从而达到删除满足where条件的子句的纪录 ;
MERGE INTO users
USING doctor
ON (users.user_id = doctor.doctorid)
WHEN MATCHED THEN
UPDATE SET users.user_name = doctor.doctorname
DELETE WHERE users.user_name like 'OL%'
WHEN NOT MATCHED THEN
INSERT VALUES (doctor.doctorid, doctor.doctorid, doctor.doctorname, SYSDATE, 'T');
delete 子句(有其自己的where子句)可以删除目标表中被merge更新的行。delete…where子句可以计算更新值,而不是目标表中的初始值。如 果目标表中的行符合delete…where条件但不在merge所作用(就如on条件所定义的)的行集范围内,就不会删除。
死锁(deadlock)
有两个会话,每个会话都持有另一个会话想要的资源,此时就会出现死锁。
一个比较简单的例子:表A 和表B,打开两个会话
(1) 会话1 更新表A第一行A1并且会话2更新表B第一行B1;更新后不提交。
(2) 会话1更新B表第一行B1,此时会话1被阻塞;要注意的是,现在还不是死锁,因为会话2还有机会提交或者回滚,这样会话1就能继续执行下去;
(3) 回到会话2,更新A表第一行A1,导致死锁。
常见的导致死锁的原因是外键没有加索引。
倘若没有对外键加索引,Oracle会使用表锁来保证外键关系。
外键不加索引的情况下,如果更新父表的主键或者删除了父表中的一行,整个子表都会被锁住。
锁类型
Oracle中主要有3类锁:
DML锁(DML lock):DML代表数据操纵语言,比如SELECT,INSERT,UPDATE,MERGE和DELETE语句。DML锁机制允许并发执行数据修改。
DDL锁:DDL代表数据定义语言,比如CREATE和ALTER语句等,DDL锁可以保护对象结构定义;
内部锁和闩:
DML锁
TX锁:事务发起第一个修改时会得到TX锁(事务锁),而且会一直持有这个锁,直至事务提交(COMMIT)或者回滚(ROLLBACK)。TX锁用作一种排队机制,使得其他会话可以等待这个事务执行。
TM锁:TM锁用于确保在修改表内容时,表的结构不会改变。
如果已经更新了一个表,会得到该表的一个TM锁,这会防止其他用户对该表执行DROP或者ALTER命令。它会保护表的数据结构不受其他用户更改。
Update数据时,会获得TX锁,保证该条记录数据不被其他会话修改。同时获得TM锁,保证该表的数据结构不会被其他会话修改。
可参考网址:http://blog.csdn.net/crazylaa/article/details/4966787
DDL锁
在DDL操作中,会自动为对象加DDL锁,保护这些对象不被其他会话修改。比如执行一个DDL操作 ALTER TABLE T,表T上会加一个排他DDL锁,防止其他会话得到该对象的DDL锁和TM锁。
在DDL语句执行期间会一直持有DDL锁,一旦操作执行就立即释放DDL锁,在ORACLE中,DDL一定会提交,用伪代码表示执行过程如下:
BEGIN
COMMIT;
ddl - STATEMENT;
COMMIT;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
END;
DDL总会提交,因为DDL在一开始就提交,因此如果需要回滚,它不会回滚你的事务。如果执行了DDL操作,它会使你所执行的所有未执行的工作成为永久性的,即使DDL不成功也会如此。
如果在程序中需要执行DDL,但是不想让它提交现有的事务,可以使用一个自治事务做DDL操作。
DDL锁一共有3种类型:
排他DDL锁:可防止其他会话得到它们自己的DDL锁或者TM(DML)锁。可以理解为,在DDL操作期间,可以查询一个表,但是无法以任何方式修改这个表。
示例:大多数DDL都带有一个排他DDL锁,比如Alter语句。
共享DDL锁:这些锁会保护被引用对象的结构,使之不会被其他会话修改,但是允许修改数据。
示例:在创建存储的编译对象,比如过程和视图时,会对依赖的对象加共享DDL锁,比如:
create view myview
as
select emp.empno,emp.name,dept.deptno,dept.dname
from emp,dept
where emp.deptno = dept.deptno
表emp和dept都会加上共享DDL锁,而Create view命令依然在处理。可以修改这些表的内容,但是不能修改它们的结构。
可中断解析锁:该锁允许一个对象(比如共享池中缓存的一个查询计划)向另外某个对象注册其依赖性,如果在被依赖的对象上执行DDL,该对象就会被失效掉。因此,这些锁是“可中断的”,不能防止DDL出现。
你的会话解析一条语句时,对于该语句引用的每一个对象都会加一个解析锁,加锁的目的是:如果以某种方式删除或者修改了一个被引用的对象,可以将共享池中已解析的缓存语句置为无效(刷新输出)。
使用DBA_DDL_LOCKS视图:
这个视图对开发人员很有用,发现测试或者开发系统中某段代码无法编译时,将会挂起并最终超时,这说明有人正在使用这段代码(实际上是正在运行这段代码),可以使用这个视图来查看这个人是谁。
SELECT s.sid,
s.serial#,
s.blocking_session block_id,
s.serial#,
s.user#,
s.machine,
s.terminal,
substr(s.module, instr(s.module, 'frm:') + 4),
s.username,
s.client_identifier,
s.blocking_session_status,
s.sql_exec_start,
a.name
FROM dba_ddl_locks a, v$session s
WHERE a.session_id = s.sid
--AND a.name LIKE 'CUX_2%'
AND a.name = 'CUX_2_OM_ERP_GPS_S_PUB';
闩
(不是很明白,待定~)
手动锁定和用户定义锁
手动锁定就是我们常用的SELECT语句:
SELECT … FOR UPDATE (NOWAIT);
用户定义锁:
使用包:DBMS_LOCK包;目前项目上多是在请求中用来控制并发。
以上是关于Oracle 锁的主要内容,如果未能解决你的问题,请参考以下文章
JUC并发编程 共享模式之工具 JUC CountdownLatch(倒计时锁) -- CountdownLatch应用(等待多个线程准备完毕( 可以覆盖上次的打印内)等待多个远程调用结束)(代码片段
Client / Server Interoperability Support Matrix for Different Oracle Versions (Doc ID 207303.1)(代码片段