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 性能特征
| 操作 | 复杂度 | 说明 |
|---|
| insert | O(1) | set 中替换已存在的键 |
| lookup | O(1) | 哈希查找 |
| delete | O(1) | 哈希删除 |
| match | O(n) | 需要扫描 |
| select | O(n) | 需要扫描(有索引时更快) |
| first/next | O(log n) | ordered_set |
| tab2list | O(n) | 复制所有数据 |
13.7.2 优化建议
| 建议 | 说明 |
|---|
使用 read_concurrency | 读多写少时提升读性能 |
使用 write_concurrency | 写多时提升并发写性能 |
使用 compressed | 节省内存 |
避免 tab2list | 大表用 select 或 foldl |
避免频繁 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 注意事项
⚠️ 常见陷阱
- ETS 表在拥有者进程退出后被销毁(除非设置 heir)
- ETS 不参与 GC,数据不会被自动清理
match 和 select 是全表扫描,大表性能差- public 表无并发保护,需要自行保证数据一致性
- ETS 数据存储在独立内存空间,不计入进程 heap
💡 最佳实践
- 缓存场景使用
set + public + named_table - 读多写少加上
read_concurrency - 统计计数器使用
update_counter - TTL 缓存定期清理过期数据
- 大数据量考虑
ordered_set 或 Mnesia
13.9 扩展阅读
上一章:12 - 应用详解
下一章:14 - Mnesia 数据库