第六章 打造一套缓存系统
概要:
- 设计一个简单的缓存服务
- 建立基本的应用结构与监督结构
- 实现缓存的主要功能
6.1 故事背景
由于页面响应速度慢,Erlware项目需要给Web服务器添加本地缓存来提速。查询软件包列表的同时,软件包服务器返回的列表将以URL为键入缓存;当用户访问同一个URL时,可以直接从缓存中取出软件包列表,可迅速完成页面渲染。架构如图6-2
该缓存气筒的基本功能大体包括:
- 缓存的启动和停止;
- 向缓存中添加键/值对;
- 查询与给定的键相对应的值;
- 更新与给定的键相对应的值;
- 删除键/值对;
6.2 缓存的设计
这个简易缓存存储的是键/值对,其中键与键之间不得重复,并且只能映射到一个值。这个设计背后的核心思想是为写入缓存的每一个值都分配一个独立的存储进程,再将对应的键映射至该进程。为每一个值分配一个进程,因为缓存中的值相对独立,各有各的生命周期。同时,Erlang本身对大量轻量级进程提供了良好的支持。
图6-3展示该简易缓存的各个组成部分
一共需要创建5个模块,
6.3 创建OTP应用的基本骨架
应用结构的搭建分为一下几个步骤:
- 创建标准应用目录布局;
- 编写.app文件
- 编写应用行为模式实现模块,即sc_app
- 实现顶层监督者,即sc_sup
6.3.1 应用目录
6.3.2 应用元数据
{application, simple_cache,
[{description, "A simple caching system"},
{vsn, "0.1.0"},
{modules, [
sc_app,
sc_sup
]},
{registered, [sc_sup]},
{applications, [kernel, stdlib]},
{mod, {sc_app, []}}
]}.
6.3.3 实现应用行为模式
-module(sc_app).
-behaviour(application).
-export([start/2, stop/1]).
start(_StartType, _StartArgs) ->
sc_store:init(),
case sc_sup:start_link() of
{ok, Pid} ->
{ok, Pid};
Other ->
{error, Other}
end.
stop(_State) ->
ok.
6.3.4 实现监督者
-module(sc_sup).
-behaviour(supervisor).
-export([start_link/0,
start_child/2
]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
start_child(Value, LeaseTime) ->
supervisor:start_child(?SERVER, [Value, LeaseTime]).
init([]) ->
Element = {sc_element, {sc_element, start_link, []},
temporary, brutal_kill, worker, [sc_element]},
Children = [Element],
RestartStrategy = {simple_one_for_one, 0, 1},
{ok, {RestartStrategy, Children}}.
根监督进程开启的时候不会启动固定的子进程,而是可以动态添加任意多个同类型的子进程;通过调用supervisor:start_child/2开启子进程,启动子进程的函数为sc_element:start_link(Value, LeaseTime)。
每次调用sc_sup:start_child/2,就会新启动一个带有自己的值和淘汰时间的sc_element子进程。
6.4 从骨架到五脏俱全的缓存
6.4.1 sc_element进程
每当新数据插入缓存时,sc_sup派生一个新的进程,用于存储与给定的键相关的数据。需要实现的主要功能有四个:新元素创建、元素值查询、元素值替换、以及元素删除。
-module(sc_element).
-behaviour(gen_server).
-export([
start_link/2,
create/2,
create/1,
fetch/1,
replace/2,
delete/1
]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(DEFAULT_LEASE_TIME, (60 * 60 * 24)).
-record(state, {value, lease_time, start_time}).
start_link(Value, LeaseTime) ->
gen_server:start_link(?MODULE, [Value, LeaseTime], []).
create(Value, LeaseTime) ->
sc_sup:start_child(Value, LeaseTime).
create(Value) ->
create(Value, ?DEFAULT_LEASE_TIME).
fetch(Pid) ->
gen_server:call(Pid, fetch).
replace(Pid, Value) ->
gen_server:cast(Pid, {replace, Value}).
delete(Pid) ->
gen_server:cast(Pid, delete).
init([Value, LeaseTime]) ->
Now = calendar:local_time(),
StartTime = calendar:datetime_to_gregorian_seconds(Now),
{ok,
#state{value = Value,
lease_time = LeaseTime,
start_time = StartTime},
time_left(StartTime, LeaseTime)}.
time_left(_StartTime, infinity) ->
infinity;
time_left(StartTime, LeaseTime) ->
Now = calendar:local_time(),
CurrentTime = calendar:datetime_to_gregorian_seconds(Now),
TimeElapsed = CurrentTime - StartTime,
case LeaseTime - TimeElapsed of
Time when Time =< 0 -> 0;
Time -> Time * 1000
end.
handle_call(fetch, _From, State) ->
#state{value = Value,
lease_time = LeaseTime,
start_time = StartTime} = State,
TimeLeft = time_left(StartTime, LeaseTime),
{reply, {ok, Value}, State, TimeLeft}.
handle_cast({replace, Value}, State) ->
#state{lease_time = LeaseTime,
start_time = StartTime} = State,
TimeLeft = time_left(StartTime, LeaseTime),
{noreply, State#state{value = Value}, TimeLeft};
handle_cast(delete, State) ->
{stop, normal, State}.
handle_info(timeout, State) ->
{stop, normal, State}.
terminate(_Reason, _State) ->
sc_store:delete(self()),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
键/值对 在缓存中存活一段时间后就会被清理出局,这段时间称为淘汰时间。DEFAULT_LEASE_TIME是默认淘汰时间,设置淘汰时间目的在于保证缓存中的内容足够新,缓存就是缓存,不是数据库。
进程的启动:子进程由监督者负责创建,创建新的子进程时,都会回调sc_element:start_link(Value, LeaseTime),然后进行进程初始化。
fetch:获取进程状态中的值;replace:更新进程的值;delete:结束进程
6.4.2 实现sc_store模块
用ETS表实现键与进程标识符之间的映射关系;sc_store模块扮演一个抽象层的角色,屏蔽了实现键与pid间映射关系的存储机制。除了使用ETS,还可以通过存在进程状态中,也可以每次插入删除数据时都讲数据写入某个磁盘文件,甚至是数据库。无论怎样实现,都要将应用本身从存储系统的选型中解耦。
-module(sc_store).
-export([
init/0,
insert/2,
delete/1,
lookup/1
]).
-define(TABLE_ID, ?MODULE).
init() ->
ets:new(?TABLE_ID, [public, named_table]),
ok.
insert(Key, Pid) ->
ets:insert(?TABLE_ID, {Key, Pid}).
lookup(Key) ->
case ets:lookup(?TABLE_ID, Key) of
[{Key, Pid}] -> {ok, Pid};
[] -> {error, not_found}
end.
delete(Pid) ->
ets:match_delete(?TABLE_ID, {'_', Pid}).
6.4.3 API模块
该模块为终端用户提供接口:
insert/2——将键/值对存入缓存;
lookup/1——按键查询值;
delete/1——按键从缓存中删除键/值对
-module(simple_cache).
-export([insert/2, lookup/1, delete/1]).
insert(Key, Value) ->
case sc_store:lookup(Key) of
{ok, Pid} ->
sc_element:replace(Pid, Value);
{error, _} ->
{ok, Pid} = sc_element:create(Value),
sc_store:insert(Key, Pid)
end.
lookup(Key) ->
try
{ok, Pid} = sc_store:lookup(Key),
{ok, Value} = sc_element:fetch(Pid),
{ok, Value}
catch
_Class:_Exception ->
{error, not_found}
end.
delete(Key) ->
case sc_store:lookup(Key) of
{ok, Pid} ->
sc_element:delete(Pid);
{error, _Reason} ->
ok
end.