一、为什么需要缓存策略
在构建高并发系统时,数据库往往是性能瓶颈。想象一下,每次用户请求都要从数据库读取数据,就像每次去超市都要从仓库搬货一样低效。缓存的作用,就是提前把热门商品摆到货架上,让顾客能快速拿到。
Elixir作为基于Erlang VM的函数式语言,天生适合构建高并发的分布式系统。但官方并没有提供"一刀切"的缓存方案,我们需要根据场景选择合适的技术。
二、ETS:轻量级内存缓存
ETS(Erlang Term Storage)是Erlang/OTP内置的内存键值存储,相当于进程间的共享内存。它的特点是简单直接,适合单节点缓存。
# 创建ETS表(技术栈:Elixir/Erlang)
:ets.new(:user_cache, [
:set, # 表类型为集合(不允许重复键)
:public, # 允许其他进程访问
:named_table, # 使用原子名称引用表
{:read_concurrency, true} # 优化读并发
])
# 写入缓存
:ets.insert(:user_cache, {"user:1001", %{name: "张三", age: 25}})
# 读取缓存
case :ets.lookup(:user_cache, "user:1001") do
[{_key, value}] -> value
[] -> nil
end
# 带TTL的缓存(需要自行实现清理逻辑)
:ets.insert(:user_cache, {"temp_data", "expires_in_10s", System.system_time(:second) + 10})
优点:
- 零外部依赖,性能极高(微秒级读写)
- 支持复杂的模式匹配查询
缺点:
- 数据不持久化,节点重启丢失
- 集群环境下需要额外同步机制
三、Agent + GenServer:状态管理方案
如果需要更精细的控制,可以用Agent封装状态,或者用GenServer实现缓存逻辑。这种方式适合需要业务逻辑处理的场景。
defmodule CacheServer do
use GenServer
# 启动缓存服务
def start_link(opts) do
GenServer.start_link(__MODULE__, %{}, opts)
end
# 初始化空Map
def init(_) do
{:ok, %{ttl: 30}}
end
# 客户端API:写入缓存
def put(pid, key, value) do
GenServer.cast(pid, {:put, key, value})
end
# 客户端API:读取缓存
def get(pid, key) do
GenServer.call(pid, {:get, key})
end
# 回调函数:处理写入
def handle_cast({:put, key, value}, state) do
{:noreply, Map.put(state, key, {value, System.os_time(:second)})}
end
# 回调函数:处理读取(带过期检查)
def handle_call({:get, key}, _from, state) do
case Map.get(state, key) do
{value, timestamp} when System.os_time(:second) - timestamp < state.ttl ->
{:reply, value, state}
_ ->
{:reply, nil, Map.delete(state, key)}
end
end
end
适用场景:
- 需要自定义过期策略
- 缓存数据需要关联业务逻辑
- 单节点中小规模数据
四、分布式缓存:Redis适配
对于需要跨节点共享的缓存,Redis是经典选择。Elixir通过Redix库可以轻松集成:
# 启动Redis连接(技术栈:Elixir + Redis)
{:ok, conn} = Redix.start_link("redis://localhost:6379")
# 设置带TTL的缓存
Redix.command!(conn, ["SET", "user:1001", "{\"name\":\"李四\"}", "EX", "3600"])
# 使用管道批量操作
Redix.pipeline(conn, [
["INCR", "page_views"],
["EXPIRE", "page_views", 60]
])
# 集群支持
pool_opts = [
name: :redis_pool,
pool_size: 10,
host: "cluster.redis.example.com"
]
children = [
{Redix, pool_opts}
]
Supervisor.start_link(children, strategy: :one_for_one)
性能对比:
| 方案 | 读延迟 | 写延迟 | 集群支持 |
|-----------|---------|---------|-------|
| ETS | 1-5μs | 2-8μs | ❌ |
| GenServer | 50-100μs| 70-150μs| ❌ |
| Redis | 0.5-2ms | 1-3ms | ✅ |
五、进阶技巧:缓存雪崩与击穿防护
实际项目中要特别注意缓存失效引发的连锁反应。以下是两种常见问题的解决方案:
1. 雪崩防护 - 给缓存TTL添加随机偏移:
ttl = 3600 + :rand.uniform(300) # 基础1小时+随机5分钟
2. 击穿防护 - 使用进程字典实现简单的互斥锁:
def get_with_lock(key, loader_fn) do
case Process.get({:cache_lock, key}) do
true ->
# 已有其他进程在加载,短暂等待
:timer.sleep(100)
get_with_lock(key, loader_fn)
nil ->
Process.put({:cache_lock, key}, true)
data = loader_fn.() # 执行数据库查询
Cache.put(key, data)
Process.delete({:cache_lock, key})
data
end
end
六、如何选择合适的方案
根据你的业务特点做决策:
- 临时数据:ETS足够(如用户会话)
- 业务敏感数据:GenServer提供更多控制
- 跨服务共享:Redis等外部存储
- 超高并发:考虑多级缓存(ETS+Redis)
记住,没有完美的方案,只有适合当前场景的权衡。建议先用简单实现满足需求,再随着业务增长逐步优化。
评论