正文

串行事务调度初窥2005-07-12 18:55:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/jzyray/2758.html

分享到:

“事务(Transaction)”应该可以认定是一个数据库术语了,但在本文中它可被宽泛地认为是一个可供调度的信息集合,被交通员调度的车辆便是生活中的例子。
“串行”是对目前大多数人进行开发的串行机而言的,即在每一个时间步只执行一个指令,操作一个数据。也就是说,本文不涉及并行算法。
本文旨在介绍事务调度的最基本的几种管理方案。此方案没经过特殊优化,模型很可能效率不高,但足以描述几种调度思想。
下面,我们将从简易的队列开始模拟事务的调度。


一.队列
队列(Queue)是基本的算法结构之一,采用FIFO(First In First Out)机制,体现传统的“先到者先得”的思想,要求事务执行的优先级以事务加入的时间为线索。
调度模块通常有两部分组成:监听和事务处理。以后各节均以此为模版讲解。
首先,应当阐明本例中可用到的队列方法:Empty()返回一布尔值,标明队列是否为空;Front()返回队列的队头事务;EnQueue(Code c)将事务t插入队尾;DeQueue()将队头删除。
然后,假定当前用于缓存事务的队列为TransQ,那么监听代码可写作:
public void Listen(){
    Code c;//c用于储存属于某个事务的指令
    while(AppRun){
        //AppRun是一个布尔变量,用于终止程序

        //得到要加入的事务
        AcceptNewTransaction(out c);

        //加入事务
        TransQ.EnQueue(c);
    }
}
再写出处理代码:
public void Handle(){
    while(AppRun){
        if(!TransQ.Empty()){

            //执行队头事务
            Execute(TransQ.Front());

            //出队
            TransQ.DeQueue();
        }

    }
}
注意:监听与处理是在两个不同线程中运行。(显而易见 :-P)
这种队列的运行效果便如公路收费站单车道一样,先到的车辆先通过收费站。


二.优先队列
在做饭时,要煮米,但却发现还没淘米,因此若按上一节所示的队列进行便行不通了:首先想到煮米,所以按时间顺序煮米就会发生在淘米前。这时,我们需要一个自定义优先级的组合——优先队列。
先介绍一下优先队列的方法:Empty()返回一个布尔值,指示队列是否为空;Insert(Code c,ThreadPriority p)将t插入到优先极为p的位置;Pop()用于返回并删除当前优先级最高的事务。
假定优先队列为TransP,可写出监听代码:
public void Listen(){
    Code c;
    ThreadPriority p;
    while(AppRun){
        AcceptNewTransaction(out c,out p);

        //加入事务
        TransP.Insert(c,p);
    }
}
再写出处理代码:
public void Handle(){
    //处理事务
    while(AppRun)
        if(!TransP.Empty())
            Execute(TransP.Pop());
}
其中,优先队列优先级的设定方案可以说在一定程度上影响着整体效率。例如,采用序号便牌,那么,序号的维护便成了重点,若每次执行Pop()时,都要将所有序号重新计算,就是个很低效的方法。
注意:没有人可以容忍自己的事务长时间在调度区挂起,应该使最终的调度时间相对于事务运行时间忽略不计。


三.多队列
平时坐车时可以发现,多车道的公路上常标有快车道、重型车道等特殊车道用来满足不同需求。
事务调度时也可根据需求进行这种变化,如等级划分。我们现在将以等级为依据实现多队列。
所谓“等级”,实际上就是优先级,只不过是队列级的优先级。比如说,管理员的事务优先级总比用户的要高,所以分开处理便可。
我们现在将第一节的代码变形,以达到代码复用的目的:
public void Listen(ref Queue q){
    Code c;
    while(AppRun){
        AcceptNewTransaction(out c);
        q.EnQueue(c);
    }
}

public void Handle(ref Queue q){
    while(AppRun){
        if(!q.Empty()){
            Execute(q.Front());
            q.DeQueue();
        }
    }
}
再写出包装函数:
public void Listen1(){
    Listen(ref q[1]);
}
…//类似的
public void Listenn(){
    Listen(ref q[n]);
}


public void Handle1(){
    Handle(ref q[1]);
}
…//类似的
public void Handlen(){
    Handle(ref q[n]);
}
再写一个线程启动函数:
public void NewThread(ThreadStart ts1,ThreadStart ts2,ThreadPriority p){
    Thread t1=new Thread(ts1);
    Thread t2=new Thread(ts2);
    t1.Priority=p;
    t2.Priority=p;
    t1.Start();
    t2.Start();
}
这样,便可以利用线程的优先级作用为队列的等级。
也就是说,调度模块运行后,只需调用以下代码:
NewThread(new ThreadStart(Listen1),new ThreadStart(Handle1),p1);

NewThread(new ThreadStart(Listenn),new ThreadStart(Handlen),pn);
手动或动态加入队列,即可实现多队列。
注意:多线程应用时要注意细节,犹应考虑资源的利用。


四.多优先队列
同样,优先队列也可以分等级,来完成更为复杂的运用。
实现多优先队列请参见二、三节内容。


五.逻辑调度
使是上,数据库中的调度应当是最复杂的了,因为这其中牵扯狠要命的环节——锁机制(时间戳协议等与此相仿)。我们依靠锁来完成对共享数据的并发操作。本节将分块对此调度的串行基本思想进行初步探讨。
1.为什么要调度
数据库要同时承载多到成百上千的连接,甚至是负责管理成百上千个指向同一块资源的请求。N多个请求怎能同时串行处理,所以,坦然面对生活,想想如何调度吧。再者,自己发出的消息一直没有回音一定使你很无奈吧。所以,采用调度模块,及时对死亡的请求进行回滚才是明智之举。
2.为什么要利用锁
既然同一块数据可能被多个线程操作,那么同时进行的指令极有可能发生冲突。而基于锁的协议就可以有效的杜绝这种冲突的发生。此外,锁还可以用于权限的实行。
调度模块在执行事务时会检测该事务所申请的锁型及操作与目标数据所标配的锁型的兼容性,必要时进行回滚。
3.有哪些功能型锁
锁的类型经过4个方面去定义:可读性及其共享性、可写性及其共享性。注:只有在可读或可写的条件下,相应的共享性才有意义。如共享可读排他可写、排他可读不可写便是两种锁型。
但是,我们只讨论两种最常用的锁:共享锁(共享可读不可写)和排他锁(共享可读排他可写)。对于他俩而言,一个数据可只拥有任意个共享锁或只拥有一个排他锁。
4.锁的实例
假令共享锁为S1,排他锁为S2,事务T1、T2同时对某一数据项进行操作,则
    T1    T2
1    grant S1    
2        grant S1
3    read    
4        read
可以正常运行,而
    T1    T2
1    grant S1    
2        grant S1
3    read    
4    grant S2    
5    write    
在运行到4时出现错误。因为T1申请排他锁,但T2还持有共享锁,所以申请被拒绝。
当然,
    T
1    grant S1
2    write
运行到2时出错,因为S1是共享锁。
5.逻辑顺序
下面,我们将讨论insert,delete,read和write的冲突问题。
insert:
若数据不存在,则应先执行insert,然后执行其它命令;反之,在数据存在时执行insert将出错。
delete:
read,write在delete后执行将出错;若数据不存在而执行delete将出错。
read,write:
数据不存在时执行read和write将出错。
根据上述规则,我们可以轻易对下述事务的正确性做出判断。
    T
1    grant S2
2    write
3    delete
4    read
第4行出错,同样
    T
1    insert
2    read
3    delete
4    delete
第4行出错。
6.代码实现
本块代码将用队列实现,由于本文只是简要概述,所以代码中将不考虑原子性及回滚等内容。
Public void Listen(){
    Code c;
    while(AppRun){
        AcceptNewTransaction(out c);
        TransQ.EnQueue(c);
    }
}

public void Handle(){
    CodeLockInfo cl;//事务当前锁型
    DataLockInfo dl;//数据当前锁型
    Code c;
    while(AppRun){
        if(!TransQ.Empty()){
            c=TransQ.Front();
            TransQ.DeQueue();
            cl=GetCodeLockInfo(c);
            dl=GetDataLockInfo(c.Target);
                //Code.Target指向待操作数据
            ConfirmCompatibility(cl,dl);
                //若锁不相容则抛出异常,CI
            ConfirmCode(c.CodeName,Exists(c.Target));
                /*根据逻辑顺序,判断当前指令与目标数据是否相容,不相容则抛出异常,CII*/

            //全部无异常,则运行
            Execute(c);
        }
    }
}
其中,CI、CII语句处要设置错误处理,若发生错误,可根据事务日志进行回滚等恢复操作。
当然,可以根据需求对代码进行变形,使用优先队列、多队列、多优先队列等。但要注意:若采用多监听、多处理模式,势必增加单个事务的运行时间,要求更多的系统资源,但却提高了调度模块的负载能力,减少了等待时间。所以,同时运行几个模块应当慎重考虑。
7.有必要的补充
刚才的代码在实际运行时还会遇到一个很严重的问题。我们知道,每个事务都应当相对独立,所以当N个事务同时操作一个数据项时,只要有一个事务使用了write指令,那么所有事务均因此会出现冲突。
为了更为简单地分析这个问题,我们只考虑read和write语句。
假设T1和T2同时操作某一数据项,C1和C2分别为T1和T2的语句。
I.C1=read,C2=read:
    不发生冲突。
II.C1=read,C2=write:
    若C1在C2前运行,则不冲突;否则C1返回的是C2设定的值,并非T1原先想要的(因为事务相互独立)。
III.C1=write,C2=write:
    铁定冲突,最终值由较后运行的决定。
当然,所有分析均在锁兼容的情况下进行,否则申请共享锁却调用write在本块没有意义。
那么,I是最放心的,III是最痛心的,看看实例吧。
    T1    T2
1    read    
2    write    
3        read
4        write
5    read    
在5处冲突,可这样解决:
    T1    T2
1    read    
2    write    
3    read    
4        read
5        write
当然,这是最极端的情况。因为解决方案中T1和T2相当于单独连续运行。不过,单独连续运行绝对可以避免此类冲突,但并发的优势却不再存在。所以,只有到实在无法调换时再采用此法。


经过一番探讨,相信大家已经清楚事务调度的意义和启蒙性的有关实现方法了吧。数据库及其思想是十分诱人的,而其中的调度思想的重要性也完全可以与I/O设计思想比肩。愿此文可以对大家有所启示。

阅读(4668) | 评论(2)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

loading...
您需要登录后才能评论,请 登录 或者 注册