mnesia之锁

在《mnesia之transaction》中提到,mnesia提供的数据读写接口中包含了获取锁请求的操作。

实际上是调用mnesia_locker提供的接口,向mnesia_locker进程发送获取锁的请求。 

例如:

mnesia.erl:

write(Tid, Ts, Tab, Val, LockKind)
when is_atom(Tab), Tab /= schema, is_tuple(Val), tuple_size(Val) > 2 ->
    ...
    case LockKind of
    write ->
        mnesia_locker:wlock(Tid, Store, Oid);
    sticky_write ->
        mnesia_locker:sticky_wlock(Tid, Store, Oid);
    ...

mnesia_locker.erl:

wlock(Tid, Store, Oid, CheckMajority) ->
    ...
    get_wlocks_on_nodes(Ns, Ns, Store, Op, Oid),
    ...

get_wlocks_on_nodes([Node|Tail], Orig, Store, Request, Oid) ->
    {?MODULE, Node} ! Request,
    ...
    get_wlocks_on_nodes(Tail, Store, Request)
 mnesia_locker进程收到请求后,进行相应的处理。
loop(State) ->
    receive
    {From, {write, Tid, Oid}} ->
        try_sticky_lock(Tid, write, From, Oid),
        loop(State);
    ...

try_sticky_lock(Tid, Op, Pid, {Tab, _}=Oid) ->
    case ?ets_lookup(mnesia_sticky_locks, Tab) of
	[] ->
	    try_lock(Tid, Op, Pid, Oid);
	[{_,N}] when N == node() ->
	    try_lock(Tid, Op, Pid, Oid);
	[{_,N}] ->
	    Req = {Pid, {Op, Tid, Oid}},
	    Pid ! {?MODULE, node(), {switch, N, Req}}
    end.

try_lock(Tid, Op, SimpleOp, Lock, Pid, Oid) ->
   case can_lock(Tid, Lock, Oid, {no, bad_luck}) of
	{yes, Default} ->
	    Reply = grant_lock(Tid, SimpleOp, Lock, Oid, Default),
	    reply(Pid, Reply);
	{{no, Lucky},_} ->
	    C = #cyclic{op = SimpleOp, lock = Lock, oid = Oid, lucky = Lucky},
	    reply(Pid, {not_granted, C});
	{{queue, Lucky},_} ->
	    ?ets_insert(mnesia_lock_queue,
			#queue{oid = Oid, tid = Tid, op = Op,
			       pid = Pid, lucky = Lucky}),
	    ?ets_insert(mnesia_tid_locks, {Tid, Oid, {queued, Op}})
    end.

=====================================================================

mnesia有5种不同类型的锁,读锁、写锁、读表锁、 写表锁和粘锁(sticky lock)。读锁和读表锁都是共享锁,而写锁则是排他的。也就是说当一个事务持有某数据项的读锁时,另外一个事务可以继续获取到该数据项的读锁,但是无法获取到该数据项的写锁,写锁请求必须等待直到读锁释放。当一个事务持有某数据项的写锁时,其他事务无法获取到该数据项的读锁,也无法获取到该数据项的写锁。 

这里有个小细节需要注意:mnesia_locker在处理读锁请求的时候会增加判断避免出现饿死现象。

check_lock(Tid, Oid = {Tab, Key}, [], [], X, AlreadyQ, Type) ->
    if
	Type == write ->
	    check_queue(Tid, Tab, X, AlreadyQ);
	Key == ?ALL ->
	    check_queue(Tid, Tab, X, AlreadyQ);
	true ->
	    %% If there is a queue on that object, read_lock shouldn't be granted
	    ObjLocks = ets:lookup(mnesia_lock_queue, Oid),
	    case max(ObjLocks) of
		empty ->
		    check_queue(Tid, Tab, X, AlreadyQ);
		ObjL ->
		    case allowed_to_be_queued(ObjL,Tid) of
			false ->
			    %% Starvation Preemption (write waits for read)
			    {no, ObjL};
			true ->
			    check_queue(Tid, Tab, {queue, ObjL}, AlreadyQ)
		    end
	    end
    end;

补充:假设事务T1在一数据项上持有读锁,另一个事务T2申请在该数据项的写锁,显然,事务T2必须等待T1释放读锁。同时事务T3可能申请该事务的读锁,前面说到读锁是共享的,因此T3被授予读锁的权利。此时T1可能已经释放了数据项上的读锁,T2事务还必须等待T3事务释放读锁。按照如此规律,每个事务在释放读锁前有新的事务请求读锁,于是可能出现事务T2总是不能在该数据项上获取到写锁,事务也就可能永远得不到执行。这个现象被称之为饿死(starved)。

另外,读锁只需在一个节点上锁住就可以了,写锁则需要在拥有副本的所有节点上进行锁的操作。

mnesia对于死锁的处理是采用"wait-die"机制。 在《数据库系统概念》书中是这么解释"wait-die"的:

wait-die机制基于非抢占技术。当事务Ti申请的数据项当前被Tj持有,仅当Ti的时间戳小于Tj的时间戳(即Ti比Tj老)时,允许Ti等待。否则,Ti回滚(死亡)。

在mnesia_locker进程中,维护了几张ets表,其中mnesia_tid_locks记录所有事务当前占用的锁的情况,mnesia_held_locks记录当前数据项(表)上锁的情况,mnesia_lock_queue记录某个数据项(表)上等待锁的事务信息,mnesia_sticky_locks记录当前粘锁的情况。mnesia_locker进程在处理锁请求时根据数据项上锁的情况与等待锁的情况决定是否授予事务锁或者是将请求添加到等待队列中。

当mnesia_locker进程收到释放锁的通知时,除了在mnesia_tid_locks和mnesia_held_locks表中删除相应记录外,还会对mnesia_lock_queue中等待的事务进行激活。 

loop(State) ->
    receive
    ...
    {release_tid, Tid} ->
        do_release_tid(Tid),
        loop(State);
    ...

do_release_tid(Tid) ->
    Locks = ?ets_lookup(mnesia_tid_locks, Tid),
    ?ets_delete(mnesia_tid_locks, Tid),
    release_locks(Locks),
    UniqueLocks = keyunique(lists:sort(Locks),[]),
    rearrange_queue(UniqueLocks).

rearrange_queue([{_Tid,{Tab,Key},_} | Locks]) ->
   if
	Key /= ?ALL->
	    Queue =
		ets:lookup(mnesia_lock_queue, {Tab, ?ALL}) ++
		ets:lookup(mnesia_lock_queue, {Tab, Key}),
	    case Queue of
		[] ->
		    ok;
		_ ->
		    Sorted = sort_queue(Queue),
		    try_waiters_obj(Sorted)
	    end;
	true ->
	    Pat = ?match_oid_lock_queue({Tab, '_'}),
	    Queue = ?ets_match_object(mnesia_lock_queue, Pat),
	    Sorted = sort_queue(Queue),
	    try_waiters_tab(Sorted)
    end,
    ?dbg("RearrQ ~p~n", [Queue]),
    rearrange_queue(Locks);
rearrange_queue([]) ->
    ok.

try_waiters_obj([W | Waiters]) ->
    case try_waiter(W) of
	queued ->
	    no;
	_ ->
	    try_waiters_obj(Waiters)
    end;
try_waiters_obj([]) ->
    ok.

try_waiter(Oid, Op, SimpleOp, Lock, ReplyTo, Tid) ->
    case can_lock(Tid, Lock, Oid, {queue, bad_luck}) of
    {yes, Default} ->
        ...
    {{queue, _Why}, _} ->
        ...
    {{no, Lucky}, _} ->
        ...

你可能感兴趣的:(mnesia之锁)