(注:分析代码基于RabbitMQ 2.8.2)
当消息需要持久化(相应队列首先必须是durable)或者因为内存吃紧,需要把消息转移到磁盘的时候就会触发持久化操作。Rabbit中两部分信息涉及到持久化操作:一个是消息本身,由msg_store模块负责([$RABBIT_SRC/src/rabbit_msg_store.erl]),另一个是消息在队列中的位置,由queue_index模块负责([$RABBIT_SRC/src/rabbit_queue_index.erl])。
client_write(MsgId, Msg, Flow, CState = #client_msstate { cur_file_cache_ets = CurFileCacheEts, client_ref = CRef }) -> %% 更新flying_ets表中对应{MsgId, CRef}的Diff,因为是写操作,所以加1 ok = client_update_flying(+1, MsgId, CState), %% 更新cur_file_cache_ets表中对应MsgId的记录,CacheRefCount加1 ok = update_msg_cache(CurFileCacheEts, MsgId, Msg), %% 调用服务端write操作 ok = server_cast(CState, {write, CRef, MsgId, Flow}).
handle_cast({write, CRef, MsgId, Flow}, State = #msstate { cur_file_cache_ets = CurFileCacheEts, clients = Clients }) -> case Flow of %% 流控相关处理 flow -> {CPid, _, _} = dict:fetch(CRef, Clients), credit_flow:ack(CPid, ?CREDIT_DISC_BOUND); noflow -> ok end, %% cur_file_cache_ets对应MsgId记录,CacheRefCount减1,一般情况下,前面的client_write/4会对CachedRefCount加1,这里减1 后,CacheRefCount为0,在下面write_message/4操作完成后,如果要创建新的持久化文件,CacheRefCount为0的所有记录都会被清除(cur_file_cache_ets一般只保存当前文件里的消息) true = 0 =< ets:update_counter(CurFileCacheEts, MsgId, {3, -1}), %% 根据flying_ets表中Diff的值来确定是否要执行实际的写入操作,如果update_flying/4返回ignore,则表示不需要写入 case update_flying(-1, MsgId, CRef, State) of process -> [{MsgId, Msg, _PWC}] = ets:lookup(CurFileCacheEts, MsgId), noreply(write_message(MsgId, Msg, CRef, State)); ignore -> %% A 'remove' has already been issued and eliminated the %% 'write'. State1 = blind_confirm(CRef, gb_sets:singleton(MsgId), ignored, State), %% 如果要处理的消息不在当前文件,则清除cur_file_cache_ets中关于该消息的记录 case index_lookup(MsgId, State1) of [#msg_location { file = File }] when File == State1 #msstate.current_file -> ok; _ -> true = ets:match_delete(CurFileCacheEts, {MsgId, '_', 0}) end, noreply(State1) end.
write_message(MsgId, Msg, State = #msstate { current_file_handle = CurHdl, current_file = CurFile, sum_valid_data = SumValid, sum_file_size = SumFileSize, file_summary_ets = FileSummaryEts }) -> %% 定位到当前文件的最后 {ok, CurOffset} = file_handle_cache:current_virtual_offset(CurHdl), %% 将消息内容添加到当前文件后面 {ok, TotalSize} = rabbit_msg_file:append(CurHdl, MsgId, Msg), %% 更新索引信息 ok = index_insert( #msg_location { msg_id = MsgId, ref_count = 1, file = CurFile, offset = CurOffset, total_size = TotalSize }, State), [#file_summary { right = undefined, locked = false }] = ets:lookup(FileSummaryEts, CurFile), %% 更新当前文件大小的统计信息 [_,_] = ets:update_counter(FileSummaryEts, CurFile, [{#file_summary.valid_total_size, TotalSize}, {#file_summary.file_size, TotalSize}]), %% 更新所有文件的统计信息,并判断是否需要创建一个新的持久化文件 maybe_roll_to_new_file(CurOffset + TotalSize, State #msstate { sum_valid_data = SumValid + TotalSize, sum_file_size = SumFileSize + TotalSize }).
read(MsgId, CState = #client_msstate { cur_file_cache_ets = CurFileCacheEts }) -> %% 从当前文件缓存表中查找,如果找到则返回 case ets:lookup(CurFileCacheEts, MsgId) of [] -> Defer = fun() -> {server_call(CState, {read, MsgId}), CState} end, %% 从Index中查找,如果找到一个引用数大于0的消息,则调用client_read1/3读取实际的消息;否则认为消息没找到,通过上面的Defer函数,调用服务端read操作(消息引用数为0时,消息有可能会处于GC处理状态) case index_lookup_positive_ref_count(MsgId, CState) of not_found -> Defer(); MsgLocation -> client_read1(MsgLocation, Defer, CState) end; [{MsgId, Msg, _CacheRefCount}] -> {{ok, Msg}, CState} end.
remove(MsgIds, CState = #client_msstate { client_ref = CRef }) -> %% 删除操作,fly_ets表中的Diff减1 [client_update_flying(-1, MsgId, CState) || MsgId <- MsgIds], server_cast(CState, {remove, CRef, MsgIds}). 服务端remove操作: handle_cast({remove, CRef, MsgIds}, State) -> {RemovedMsgIds, State1} = lists:foldl( fun (MsgId, {Removed, State2}) -> %% 根据flying_ets中的Diff值决定是否需要执行删除操作 case update_flying(+1, MsgId, CRef, State2) of process -> {[MsgId | Removed], remove_message(MsgId, CRef, State2)}; ignore -> {Removed, State2} end end, {[], State}, MsgIds), %% maybe_compact检查是否需要执行文件合并 noreply(maybe_compact(client_confirm(CRef, gb_sets:from_list(RemovedMsgIds), ignored, State1)));remove_message/3的主要逻辑就是更新对应消息的引用计数,更新要删除消息所在文件的统计数据,如果消息所在文件已经没有有效数据,则删除该文件。
%% 计算两个文件中总的有效数据大小,后续合并后的文件会扩展到这个大小 TotalValidData = SourceValid + DestinationValid, {DestinationWorkList, DestinationValid} = load_and_vacuum_message_file(Destination, State), %% 从目标文件读取有效消息数据 {DestinationContiguousTop, DestinationWorkListTail} = drop_contiguous_block_prefix(DestinationWorkList), %% 检查从目标文件开始的连续有效消息区间 case DestinationWorkListTail of %% 如果目标文件中的所有有效消息数据都是连续,只要把目标文件扩展到合并后的大小,并将写入位置定位到目标文件的最后 [] -> ok = truncate_and_extend_file( DestinationHdl, DestinationContiguousTop, TotalValidData); %% 如果目标文件中有空洞,则:1)将除了第一个连续区间内的消息以外的所有有效消息先读到一个临时文件;2)把目标文件扩展到合并后的大小;3)把临时文件中的有效数据拷贝到目标文件 _ -> Tmp = filename:rootname(DestinationName) ++ ?FILE_EXTENSION_TMP, {ok, TmpHdl} = open_file(Dir, Tmp, ?READ_AHEAD_MODE++?WRITE_MODE), ok = copy_messages( DestinationWorkListTail, DestinationContiguousTop, DestinationValid, DestinationHdl, TmpHdl, Destination, State), TmpSize = DestinationValid - DestinationContiguousTop, %% so now Tmp contains everything we need to salvage %% from Destination, and index_state has been updated to %% reflect the compaction of Destination so truncate %% Destination and copy from Tmp back to the end {ok, 0} = file_handle_cache:position(TmpHdl, 0), ok = truncate_and_extend_file( DestinationHdl, DestinationContiguousTop, TotalValidData), {ok, TmpSize} = file_handle_cache:copy(TmpHdl, DestinationHdl, TmpSize), %% position in DestinationHdl should now be DestinationValid ok = file_handle_cache:sync(DestinationHdl), ok = file_handle_cache:delete(TmpHdl) end, %% 从源文件中加载所有有效消息数据 {SourceWorkList, SourceValid} = load_and_vacuum_message_file(Source, State), %% 将源文件中的所有有效数据拷贝到目标文件 ok = copy_messages(SourceWorkList, DestinationValid, TotalValidData, SourceHdl, DestinationHdl, Destination, State).