创建OTP应用的基本骨架到实现缓存

应用结构的搭建分为一下几个步骤:

(1)创建标准应用目录布局;

(2)编写**.app**文件

(3)编写应用行为模式实现模块,即sc_app

(4)实现顶层监督者,即sc_sup

一:应用目录结构的布局

首先新建一个名为simple cache的顶层应用目录。在该目录下,新建docebinincludeprivsrc等子目录。最终的目录树应该是这样的:

Erlang 复制代码
simple_cache
            |
            | -doc
            | -ebin
            | -include
            | -priv
            | -src

虽然这个例子还用不上docincludepriv 目录,不过,反正多建几个目录没有什么坏处,以备

不时之需吧。目录布局完毕后,下一步就是布置**.app**文件。

二:创建应用元数据

在启动应用或在执行运行时代码热升级时,OTP需要了解一些用于描述应用自身的元数据。存放元数据的**.app** 文件的文件名应该与应用名相匹配(但无须采用特定模块的名字),在这个例子中,该文件就是ebin/simple_cache.app.app文件当前内容如下:

Erlang 复制代码
%% ebin/simple_cache.app
{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, []}}
 ]}.

sc_appsc_sup这两个模块肯定是少不了的,已经罗列在内;其余模块则将在后续逐步加人该列表。另外,很明显根监督者应该以sc_sp为注册名进行注册。骨架已经搭建完毕,接下来就要给应用行为模式的实现模块添砖加瓦了。

三:实现应用行为模式

应用行为模式的实现位于文件src/sc_app.erl内,特别需要注意的是,.app文件中的mod元组给出了应用行为模式模块的模块名,系统就是从这里得知应该从何处启动和停止应用的。代码如下:

Erlang 复制代码
%% src/sc_app.erl
-module(sc_app).
 
%% 行为模式声明
-behaviour(application).
 
%% 导出行为模式回调函数
-export([start/2, stop/1]).

%% 启动根监督者 
start(_StartType, _StartArgs) ->
    case sc_sup:start_link() of
        {ok, Pid} ->
            {ok, Pid};
        Other ->
            {error, Other}
    end.
 
stop(_State) ->
    ok.

sc_app模块唯一的任务就是在应用启动时启动根监督者 ,在应用停止时则什么也不用做

四:实现监督者

根监督者在文件src/sc_sup.erl 中实现,但是这个监督者我们没有给这个监督者静态指派 任何永久子进程 ,但却可以给它动态添加任意多个同类型的临时子进程,代码如下:

Erlang 复制代码
-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) ->
    %% sc_element:start_link/2 的参数
    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}}.

1.简易的一对一

该监督者的监督策略被设定为simple_one_for_one (简易一对一监督)。simple_one_for_one型监督者只能启动一种子进程,但却可以启动任意多个。它所有的子进程都是运行时动态添加的,监督者本身在启动时不会启动任何子进程。如下图所示是一个简易的一对一模块:

2.监督者模块

根监督进程开启的时候不会启动固定的子进程,而是可以动态添加任意多个同类型的子进程;通过调用supervisor:start_child/2 开启子进程,启动子进程的函数为sc_element:start_link(Value, LeaseTime) 。每次调用sc_sup:start_child/2 ,就会新启动一个带有自己的值和淘汰时间的sc_element子进程。

五:编写sc_element进程

sc_element 模块实现了sc_sup 的子进程,每当有新数据插人缓存时,sc_sup 就会派生出一个新

sc_element 进程,用于存储与给定的键相关联的数据。我们打算以gen_server 行为模式为基

础来实现这类进程,数据将被保存在gen_server进程的进程状态中。

1.模块首部

每当新数据插入缓存时,sc_sup派生一个新的进程,用于存储与给定的键相关的数据。需要实现的主要功能有四个:新元素创建、元素值查询、元素值替换、以及元素删除。下面代码就是一个首部模块:

Erlang 复制代码
%% src/sc_element.erl
-module(sc_element).
 
-behaviour(gen_server).

%% 导出的API函数
-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}).

键/值对在缓存中存活一段时间之后便会被清理出局,这段时间称作淘汰时间。DEFAULTLEASE_TIME 便是默认的淘汰时间 (以秒为单位)。设定淘汰时间的目的在于保证缓存中的内容足够新,缓存就是缓存,不是数据库。创建sc_element 进程时,你可以通过API自行调整这个值。模块首部的最后一项定义了用于表示gen_serverj进程状态的记录。它由3个字段组成:进程持有的值淘汰时间 ,以及进程启动时的时间戳

2.API段和进程的启动

模块中的下一部分就是API的实现,如下述代码所示:

Erlang 复制代码
%% src/sc_element.erl
start_link(Value, LeaseTime) ->
    gen_server:start_link(?MODULE, [Value, LeaseTime], []).
 
create(Value, LeaseTime) ->
    %% 将启动委托给sc_sup
    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).

子进程由监督者负责创建;个别细节则由监督者的API函数sc_sup:start_child/2 负责屏蔽。然而,监督者的存在属于实现细节,sc_element 的用户并不关心。为此,我们创建了API函数create/2 ,用于将创建子进程的任务委托给sc_sup 。此外,如果直接采用默认淘汰时间,还可以选用更为简化的create/1。这下即便把底层实现改个底儿朝天,也不用动接口层了。整体的调用流程如下图所示:

存储新存储时的调用流程。其中sc_element API向simple_cache屏蔽了sc_sup的存在。同时,sc sup也不关心sc_element的功能细节

3.gen_server回调段

sc_elementi进程启动之后的第一件事就是通过gen server回调函数init/l完成进程初始化,该函数应返回一个初始化完毕的进程状态记录。gen_server:start_link/3 调用将一直阻塞到init/1 返回为止。src/sc_element.erl的各个回调如下面代码所示:

Erlang 复制代码
%% src/sc_element.erl
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}.

注:记得设置服务器超时,如果忘了在回调函数的返回值中设置新的超时,超时将被重置为1n1f11ty。因此旦用上服务器超时,切记在每个回调函数的每个子句中都设置好超时。

六:实现sc_store模块

至此,你已经实现了基本的应用结构和缓存的后端存储系统,其中还包括淘汰时间管理功能。现在,你将以此为基础构建出一整套完整的存储系统。目前,所有缺失环节中最关键的一环就是键与进程标识符之间的映射关系,有了它你才能根据给定的键查找到相应的值。下面展示了src/sc_store.erl 的代码。这一次,这个模块没有采用任何OTP行为模式,也没有与任何进程相关联一它只包含一组供其他进程调用的库函数。

Erlang 复制代码
%% src/sc_store.erl
-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}).

API由init/1 (负责存储系统的初始化)和处理基本CRUD操作(创建读取更新和删除 )的3个函数组成,其中insert/2同时负责创建新表项和更新现存表项。这几个函数的实现都很简洁。具体实现如下:

1.存储的初始化

在init/1中,首先需要创建用于存放映射关系的ETS表 。第一种方法是直接调用ets:new/2 即可,第二种方法是利用表名。ETS接口要求每张表都有一个名字;但必须先设置named_table 才能用表名来访问表,此外,多张表可以共用一个表名。在此采用具名表的原因在于我们不希望库的用户去追踪表句柄,一旦这样做,你就必须将句柄传递给所有会用到sc_storel 的进程,而且sc_storel的每个API调用都必须包含该句柄。因此我们可以对src/app.erl 中的start/2函数进行如下改动:

Erlang 复制代码
%% src/sc_app.erl
start(_StartType, _StartArgs) ->
    %% 加到这个位置
    sc_store:init(),
    case sc_sup:start_link() of
        {ok, Pid} ->
            {ok, Pid};
        Other ->
            {error, Other}
    end.

只要加上这么一句,sc_store便可以在应用启动后的第一时间完成初始化。如果再延迟初始化的时机(例如放在顶层监督者启动之后),就有可能在某些地方出现试图访问某张尚不存在的ETS表的风险。

2.表项的创建和更新

默认情况下,表中所有元组的第一个元素被视作键,其余元素被视作载荷(个数任意)。按键进行查找时,与该键对应的整个元组都将被返回。ETS默认表现为一个集合(set):同一时刻一个键只能与一个表项相对应,如果表中现有的某个表项与插入的新元组具有相同的键 ,那么旧表项将被新元组覆盖一这恰恰是你所需要的功能。

七:打造应用层API模块

应用层API模块通常与应用同名。在此,你将为simple_cache 应用建立一个名为simple_cache的API模块。该模块为缓存服务的终端用户提供了以下接口函数:

|----------|--------------|
| insert/2 | 将键/值对存入缓存 |
| lookup/1 | 按键查询值 |
| delete/1 | 按键从缓存中删除键/值对 |

这套API并未包含应用的启动、停止功能,相关功能将交由application:start/1等系统函数来处理。具体代码如下所示:

Erlang 复制代码
%% src/simple_cache.erl
-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
        %% 获取键对应的Pid
        {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.

这样,我们的简易缓存就可以运作了。下面我们来尝试一下(强烈建议你亲自动手)。试运行之前,编译src目录下的所有模块,并将生成的**.beam** 文件悉数放到ebin目录下。然后请按下述指示(在应用根目录下)运行Erlang,启动应用:

Erlang 复制代码
$ erl -pa ebin
Eshell V5.5.5 (abort with ^G)
1> application:start(simple_cache).
ok
相关推荐
轻口味9 分钟前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王44 分钟前
React Hooks
前端·javascript·react.js
迷途小码农零零发1 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀1 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪2 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word
ekskef_sef3 小时前
32岁前端干了8年,是继续做前端开发,还是转其它工作
前端
sunshine6414 小时前
【CSS】实现tag选中对钩样式
前端·css·css3
真滴book理喻4 小时前
Vue(四)
前端·javascript·vue.js
蜜獾云4 小时前
npm淘宝镜像
前端·npm·node.js