oracle非阻塞读需要重视
?
?
来自oracle资料 写道
Oracle 的无阻塞读方法有一个副作用,如果确实想保证一次最多只有一个用户访问一行数据,开发人员就得自己做些工作。
考虑下面这个例子。一位开发人员向我展示了他刚开发的一个资源调度程序(可以用来调度会议室、
投影仪等资源),这个程序正在部署当中。应用中实现了这样一个业务规则:在给定的任何时间段都不能
将一种资源分配给多个人。也就是说,应用中包含了实现这个业务规则的代码,它会明确地检查此前这个
时间片没有分配给其他用户(至少,这个开发人员认为是这样)。这段代码先查询SCHEDULES 表,如果不
存在与该时间片重叠的记录行(该时间片尚未分配),则插入新行。所以,开发人员主要考虑两个表:
create table resources ( resource_name varchar2(25) primary key, ... );
create table schedules
( resource_name references resources,
start_time date not null,
end_time date not null,
check (start_time < end_time ),
primary key(resource_name,start_time)
);
在分配资源(如预订房间)之前,应用将查询:
select count(*)
from schedules
where resource_name = :room_name
and (start_time <= :new_end_time)
and (end_time >= :new_start_time)
看上去很简单,也很安全(在开发人员看来):如果得到的计数为0,这个房间就是你的了。如果返回的数非0,那在此期间你就不能预订这个房间。了解他的逻辑后,我建立了一个非常简单的测试,来展示这个应用运行时可能出现的一个错误,这个错误极难跟踪,而且事后也很难诊断。有人甚至以为这必定是一个数据库bug。
我所做的其实很简单,就是让另外一个人使用这个开发人员旁边的一台机器,两个人都浏览同一个屏幕,然后一起数到3 时,两人都单击Go 按钮,尽量同时预订同一个房间,一个人想预订下午3:00 到下午4:00 这个时段,另一个人要预订下午3:30 到下午4:00 这个时段。结果两个人都预订成功。这个逻辑独立执行时原本能很好地工作,但到多用户环境中就不行了。为什么会出现这个问题?部分原因就在于Oracle的非阻塞读。这两个会话都不会阻塞对方,它们只是运行查询,然后完成调度房间的逻辑。两个会话都通过运行查询来查找是否已经有预订,尽管另一个会话可能已经开始修改SCHEDULES 表,但查询看不到这些修改(所做的修改在提交之前对其他会话来说是不可见的,而等到提交时已为时过晚)。由于这两个会话并没有试图修改SCHEDULES 表中的同一行,所以它们不会相互阻塞。由此说来,这个应用不能像预期的那样保证前面提到的业务规则。
开发人员需要一种方法使得这个业务规则在多用户环境下也能得到保证,也就是要确保一次只有一个人预订一种给定的资源。在这种情况下,解决方案就是加入他自己的一些串行化机制。他的做法是,在对SCHEDULES 表进行修改之前,先对RESOURCES 表中的父行锁定。这样一来, SCHEDULES 表中针对给定RESOURCE_NAME 值的所有修改都必须依次按顺序进行,一次只能执行一个修改。
这样一来, SCHEDULES 表中针对给定RESOURCE_NAME 值的所有修改都必须依次按顺序进行,一次只能执行一个修改。也就是说,要预订资源X一段时间,就要锁定RESOURCES 表中对应X 的那一行,然后修改SCHEDULES 表。所以,除了前面的count(*)外,开发人员首先需要完成以下查询:
select * from resources where resource_name = :room_name FOR UPDATE;
这里,他在调度资源之前先锁定了资源(这里指房间),换句话说,就是在SCHEDULES 表中查询该资源的预订情况之前先锁定资源。通过锁定所要调度的资源,开发人员可以确保别人不会同时修改对这个资源的调度。其他人都必须等待,直到他提交了事务为止,此时就能看到他所做的调度。这样就杜绝了调度重叠的可能性。
开发人员必须了解到,在多用户环境中,他们必须不时地采用多线程编程中使用的一些技术。在这里,FOR UPDATE 子句的作用就像是一个信号量semaphore),只允许串行访问RESOURCES 表中特定的行,这样就能确保不会出现两个人同时调度的情况。我建议把这个逻辑实现为一个事务API,也就是说,所有逻辑都打包进一个存储过程中,只允许应用通过这个API 修改数据。代码如下:
create or replace procedure schedule_resource
( p_resource_name in varchar2,
p_start_time in date,
p_end_time in date
)
as
l_resource_name resources.resource_name%type;
l_cnt number;
begin
首先在RESOURCES 表中锁定我们想调度的那个资源的相应行。如果别人已经锁定了这一行,我们就会阻塞并等待:
select resource_name into l_resource_name
from resources
where resource_name = p_resource_name
FOR UPDATE;
既然我们已经有了锁,那么只有我们能在这个SCHEDULES 表中插入对应此资源名的调度,所以如下查看这个表是安全的:
select count(*)
into l_cnt
from schedules
where resource_name = p_resource_name
and (start_time <= p_end_time)
and (end_time >= p_start_time);
if ( l_cnt <> 0 )
then
raise_application_error
(-20001, 'Room is already booked!' );
end if;
如果能运行到这里而没有发生错误,就可以安全地在SCHEDULES 表中插入预订资源的相应记录行,而不用担心出现重叠:
insert into schedules
( resource_name, start_time, end_time )
values
( p_resource_name, p_start_time, p_end_time );
end schedule_resources;
这个解决方案仍是高度并发的,因为可能有数以千计要预订的资源。这里的做法是,确保任何时刻只能有一个人修改资源。这是一种很少见的情况,在此要对并不会真正更新的数据手动锁定。
我们要知道哪些情况下需要这样做,还要知道哪些情况下不需要这样做(稍后会给出这样一个例子),这同样很重要。另外,如果别人只是读取数据,就不会锁定资源不让他们读(但在其他数据库中可能不是这样),所以这种解决方案可以很好地扩缩。
以上代码有一个前提,即假设事务隔离级别是READ COMMITTED。如果事务隔离级别是SERIALIZABLE,这个逻辑将无法正常工作。
?
?