多服务器并发的操作同一个数据库的同一张表。
有如下一张表:
/* Table: APP_TASK */
TASKNO VARCHAR2(22) not null,
TASK_TYPE VARCHAR2(2) not null,
EXECUTE_TIME DATE,
PARMAS_XML VARCHAR2(2000),
STATUS VARCHAR2(1) default '0'
……
表中的一条数据我们称之为一个task。我们的程序有个方法每次取出50个task,每5秒钟重复执行一次,然后根据每个task的参数去处理task,
处理过程中和处理完成后 会更新task的STATUS等参数。
如果只有一套程序去操作这个表,不会存在问题,但是现在要将这个应用程序部署到多个服务器上,那么每个服务器都操作同一张表,都要取出task,然后再更新。
如果不采取措施,我们无法保证它们取出的task不重复,比如服务器A取出了task1、task2、task3,在同一时间,服务器B也可能取出task2、task3,如此一来,就会乱套。
之前波哥提了一个方法:
"select * from task where flag = 0"; // 取任务
... // 一系列判断
int i = "update task set flag = 1 where id='A' and flag = 0"; // 更新任务A,操作任务设置标志位1
if i = 0 then
task A is used by another server // 如果修改条数为0,则该任务已经被别人处理了,本进程不用处理。
return
也就是说先把task查询出来(只查1条),然后再更新这个task的flag标志,如果更新成功,则表示这条task未被其他server占用,否则,这个task已经被其他server占用了
以前是一次查询50个task出来,但是如果按上面的方法,就要每个task更新一次。50个task就要更新50次,而5秒钟之后又要重复这个操作……
而且实际上,不管你更新1个task还是50个task,都会存在问题,update..where...时,会试图将where查询到的数据加锁,如果该数据已经被其他连接加锁了,
则update会一直等待其他锁释放。
我有个解决方案,用到 select...for update nowait 语句,
select 1个task for update nowait,然后立即 update 这个task set flag=1,最后commit
我也考虑过:
select 50个task for update nowait,但是这样,另外一个服务器select...for update nowait时就会因为数据被加锁而报错。
所以一次只能试探性的取一个出来。
这还是第一个问题。第二个问题:
如果我们取出来数据,设置flag=1,表示这条数据已经被使用了,如果服务器重启或者出现其他意外,导致取出来的这条数据没有被执行,
那么这条数据就被埋没了(因为它的flag已经被设置成1了,而程序查询的时候只查flag=0的数据)。
第三个问题:
下面要讲的这个问题更严重了。
比如,我查询出来1条数据(一个task),这个task从查询出来,到执行结束,假设需要30秒钟。
而如果在这30秒钟之内,有个用户发出指令要修改或者删除这条数据,于是,先前查询出来的task,与数据库中的数据就不同步了。
所以说,我们需要在"从task被查询出来到task执行结束"这个时间段内,对这条数据保持加锁状态,不允许其他程序再更改这条数据,而且,
必须要在同一个事务中完成对这个task的全部操作。
后来,我想到的一个方案:添加一个is_loaded标志和load_time时间。
初始状态IS_LOADED=0,获取任务后,更改IS_LOADED=1以及LOAD_DATE=systime,任务执行完,从taskSet中移除,更新状态IS_LOADED=0。
如果服务器挂了(或者其他原因),那么IS_LOADED=1,但是Now-load_time > N(这个N是我们设置的超时阀值),意思就是执行时间太长,过期了,我们可以认为可能是服务器意外关掉了,这种数据可以被我们监控到。至于处理嘛,没有简单的方法,我头都想大了……
将这种特殊任务交给多服务器去处理,肯定是不行了,交给单一服务器去处理:(我们只在其中一台服务器部署如下的方法)
这台服务器,除了查询IS_LOADED=0的任务,还负责查询Now-load_time>N的任务(就是SQL语句和其他服务器上的稍有不同而已),如果有过期的任务,则将其状态更改成1,使得它可以被重新加载执行。