强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

Erlang/OTP 完全指南 / 13 - ETS 表

第 13 章:ETS — Erlang Term Storage

ETS(Erlang Term Storage)是 Erlang 内置的高性能内存表,支持并发读写,常用于缓存和共享状态。


13.1 ETS 基础

13.1.1 创建表

%% 创建一个 set 类型的 ETS 表
Tab = ets:new(my_table, [set, public, named_table]).
%% 返回表的引用(如果用 named_table,则用名字访问)

%% 或者使用名字
ets:new(my_table, [set, public, named_table]).
%% 现在可以用 my_table 访问

13.1.2 表类型

类型说明键重复
set默认,键唯一不允许
ordered_set有序,键唯一不允许
bag允许多个相同键允许
duplicate_bag允许完全相同的行允许

13.1.3 访问权限

权限说明
public任何进程可读写
protected只有拥有者可写,任何进程可读(默认)
private只有拥有者可读写

13.1.4 表选项

选项说明
named_table可以用名字访问
{keypos, N}指定键的位置(默认 1)
{heir, Pid}拥有者退出后,表转移给指定进程
{heir, none}拥有者退出后,表被销毁
compressed压缩存储(节省内存,访问稍慢)

13.2 基本操作

13.2.1 插入和查询

%% 创建表
ets:new(user_cache, [set, public, named_table]).

%% 插入:ets:insert(Tab, Object)
%% Object 是元组,第一个元素(默认)是键
ets:insert(user_cache, {alice, 25, "Beijing"}).
ets:insert(user_cache, {bob, 30, "Shanghai"}).

%% 插入多个
ets:insert(user_cache, [{charlie, 22, "Guangzhou"}, {dave, 28, "Shenzhen"}]).

%% 查询:ets:lookup(Tab, Key)
ets:lookup(user_cache, alice).   %% [{alice, 25, "Beijing"}]
ets:lookup(user_cache, nobody).  %% []

%% 获取所有键
ets:keys(user_cache).            %% [alice, bob, charlie, dave]

%% 获取所有数据
ets:tab2list(user_cache).        %% [{alice,25,"Beijing"}, ...]

13.2.2 更新和删除

%% 更新(替换已存在的键)
ets:insert(user_cache, {alice, 26, "Shanghai"}).  %% 替换旧值
ets:lookup(user_cache, alice).  %% [{alice, 26, "Shanghai"}]

%% 更新计数器
ets:update_counter(user_cache, visit_count, 1).    %% 增加 1
ets:update_counter(my_table, key, {2, 10}).        %% 第2个元素增加10

%% 删除
ets:delete(user_cache, alice).        %% 删除指定键
ets:delete_all_objects(user_cache).   %% 清空表
ets:delete_table(user_cache).         %% 删除整个表

13.2.3 匹配和选择

%% match:模式匹配返回指定字段
ets:match(user_cache, {'$1', '$2', '_'}).
%% [[alice,25],[bob,30],...]

%% match_object:返回完整对象
ets:match_object(user_cache, {'$1', '$2', '_'}).
%% [{alice,25,"Beijing"},{bob,30,"Shanghai"},...]

%% select:使用 Match Spec(强大的查询)
%% 查找年龄 > 25 的用户
ets:select(user_cache, [
    {{'$1', '$2', '$3'}, [{'>', '$2', 25}], ['$1']}
]).
%% [bob, dave]

%% select_delete:删除匹配的记录
ets:select_delete(user_cache, [
    {{'$1', '$2', '_'}, [{'<', '$2', 20}], [true]}
]).
%% 删除年龄 < 20 的记录

13.2.4 Match Spec 速查

%% Match Spec 格式:[Head, Guards, Body]

%% Head: 模式,用 '$N' 表示变量
%% Guards: 条件列表
%% Body: 返回值列表

%% 示例:查找价格在 10-50 之间的商品名称
ets:select(products, [
    {{'_', '$1', '$2'}, [{'>=', '$2', 10}, {'=<', '$2', 50}], ['$1']}
]).

%% Guard 操作符
%% {'andalso', G1, G2}   与
%% {'orelse', G1, G2}    或
%% {'not', G}            非
%% {'==', '$1', Val}     等于
%% {'/=','$1', Val}      不等于
%% {'>', '$1', Val}      大于
%% {'<', '$1', Val}      小于
%% {'>=', '$1', Val}     大于等于
%% {'=<', '$1', Val}     小于等于

13.3 遍历

%% first/next 遍历(set 或 ordered_set)
walk(Tab) ->
    walk(Tab, ets:first(Tab)).

walk(_Tab, '$end_of_table') ->
    ok;
walk(Tab, Key) ->
    [Record] = ets:lookup(Tab, Key),
    io:format("~p~n", [Record]),
    walk(Tab, ets:next(Tab, Key)).

%% safe_fixtable:固定遍历(允许并发修改)
ets:safe_fixtable(Tab, true),
%% ... 遍历 ...
ets:safe_fixtable(Tab, false).

%% foldl
ets:foldl(fun({K, V}, Acc) ->
    Acc#{K => V}
end, #{}, my_table).

13.4 表信息

ets:info(my_table).
%% [{owner,<0.123.0>},
%%  {heir,none},
%%  {name,my_table},
%%  {size,1000},
%%  {node,'nonode@nohost'},
%%  {named_table,true},
%%  {type,set},
%%  {keypos,1},
%%  {protection,public}]

ets:info(my_table, size).        %% 1000(记录数)
ets:info(my_table, memory).      %% 内存使用(words)
ets:info(my_table, owner).       %% 拥有者 PID

%% 所有 ETS 表
ets:all().

13.5 实战:缓存系统

%% cache.erl
-module(cache).
-export([new/1, put/3, put/4, get/2, delete/2, flush/1, size/1]).

new(Name) ->
    ets:new(Name, [set, public, named_table]).

put(Name, Key, Value) ->
    put(Name, Key, Value, infinity).

put(Name, Key, Value, TTL) ->
    Expires = case TTL of
        infinity -> infinity;
        Seconds -> erlang:system_time(second) + Seconds
    end,
    ets:insert(Name, {Key, Value, Expires}).

get(Name, Key) ->
    case ets:lookup(Name, Key) of
        [{Key, Value, infinity}] ->
            {ok, Value};
        [{Key, Value, Expires}] ->
            Now = erlang:system_time(second),
            if
                Now < Expires -> {ok, Value};
                true ->
                    ets:delete(Name, Key),
                    not_found
            end;
        [] ->
            not_found
    end.

delete(Name, Key) ->
    ets:delete(Name, Key).

flush(Name) ->
    ets:delete_all_objects(Name).

size(Name) ->
    ets:info(Name, size).
$ erl
1> cache:new(my_cache).
my_cache
2> cache:put(my_cache, user_1, #{name => "Alice"}, 300).
ok
3> cache:get(my_cache, user_1).
{ok,#{name => "Alice"}}

13.6 实战:读写锁(ETS 实现)

%% rw_counter.erl
%% 使用 ETS 实现无锁并发计数器
-module(rw_counter).
-export([new/1, increment/1, decrement/1, get/1]).

new(Name) ->
    ets:new(Name, [set, public, named_table]),
    ets:insert(Name, {counter, 0}).

increment(Name) ->
    ets:update_counter(Name, counter, 1).

decrement(Name) ->
    ets:update_counter(Name, counter, -1).

get(Name) ->
    [{counter, Val}] = ets:lookup(Name, counter),
    Val.

13.7 性能优化

13.7.1 ETS 性能特征

操作复杂度说明
insertO(1)set 中替换已存在的键
lookupO(1)哈希查找
deleteO(1)哈希删除
matchO(n)需要扫描
selectO(n)需要扫描(有索引时更快)
first/nextO(log n)ordered_set
tab2listO(n)复制所有数据

13.7.2 优化建议

建议说明
使用 read_concurrency读多写少时提升读性能
使用 write_concurrency写多时提升并发写性能
使用 compressed节省内存
避免 tab2list大表用 selectfoldl
避免频繁 match使用 lookup 或建立索引表
%% 高读并发配置
ets:new(cache, [set, public, named_table,
    {read_concurrency, true}]).

%% 高写并发配置
ets:new(counters, [set, public, named_table,
    {write_concurrency, true}]).

%% 读写都高并发
ets:new(session, [set, public, named_table,
    {read_concurrency, true},
    {write_concurrency, true}]).

13.8 注意事项

⚠️ 常见陷阱

  1. ETS 表在拥有者进程退出后被销毁(除非设置 heir)
  2. ETS 不参与 GC,数据不会被自动清理
  3. matchselect 是全表扫描,大表性能差
  4. public 表无并发保护,需要自行保证数据一致性
  5. ETS 数据存储在独立内存空间,不计入进程 heap

💡 最佳实践

  1. 缓存场景使用 set + public + named_table
  2. 读多写少加上 read_concurrency
  3. 统计计数器使用 update_counter
  4. TTL 缓存定期清理过期数据
  5. 大数据量考虑 ordered_setMnesia

13.9 扩展阅读


上一章:12 - 应用详解 下一章:14 - Mnesia 数据库