创建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
相关推荐
酷爱码17 分钟前
css中的 vertical-align与line-height作用详解
前端·css
沐土Arvin31 分钟前
深入理解 requestIdleCallback:浏览器空闲时段的性能优化利器
开发语言·前端·javascript·设计模式·html
专注VB编程开发20年33 分钟前
VB.NET关于接口实现与简化设计的分析,封装其他类
java·前端·数据库
小妖66642 分钟前
css 中 content: “\e6d0“ 怎么变成图标的?
前端·css
L耀早睡1 小时前
mapreduce打包运行
大数据·前端·spark·mapreduce
HouGISer2 小时前
副业小程序YUERGS,从开发到变现
前端·小程序
outstanding木槿2 小时前
react中安装依赖时的问题 【集合】
前端·javascript·react.js·node.js
霸王蟹2 小时前
React中useState中更新是同步的还是异步的?
前端·javascript·笔记·学习·react.js·前端框架
霸王蟹2 小时前
React Hooks 必须在组件最顶层调用的原因解析
前端·javascript·笔记·学习·react.js
专注VB编程开发20年3 小时前
asp.net IHttpHandler 对分块传输编码的支持,IIs web服务器后端技术
服务器·前端·asp.net