题目
设计线程安全的可重入缓存系统
信息
- 类型:问答
- 难度:⭐⭐⭐
考点
并发编程, 元编程, 缓存策略, 线程安全, 异常处理
快速回答
实现线程安全的可重入缓存需解决以下核心问题:
- 使用
Concurrent::Map或Mutex保证基础存储的线程安全 - 通过
Thread.current跟踪递归调用避免死锁 - 实现缓存击穿保护(如锁优化或占位符)
- 添加 TTL 过期机制和缓存清理策略
- 处理异常和缓存雪崩(如随机过期时间)
核心挑战与设计目标
在并发环境下实现缓存系统需解决:1) 多线程竞争导致数据不一致;2) 递归调用触发死锁;3) 缓存失效时的雪崩/击穿问题。
完整实现代码
require 'concurrent'
class ReentrantCache
def initialize
@store = Concurrent::Map.new
@mutex = Mutex.new
end
def fetch(key, ttl: nil, &block)
# 检查当前线程递归深度
thread_data = Thread.current[:cache_recursion] ||= {}
if thread_data[key]
return yield # 直接执行块避免死锁
end
entry = @store[key]
# 缓存有效检查
if entry_valid?(entry, ttl)
entry[:value]
else
# 标记递归状态
thread_data[key] = true
@mutex.synchronize do
# 双重检查锁定模式
entry = @store[key]
if entry_valid?(entry, ttl)
thread_data.delete(key)
return entry[:value]
end
begin
result = yield
@store[key] = { value: result, created_at: Time.now }
result
ensure
thread_data.delete(key)
end
end
end
end
def clear
@store.clear
end
private
def entry_valid?(entry, ttl)
entry && (!ttl || Time.now - entry[:created_at] < ttl)
end
end
# 使用示例
cache = ReentrantCache.new
cache.fetch(:api_data, ttl: 60) { expensive_operation }关键技术解析
- 线程安全存储:使用
Concurrent::Map替代Hash,或配合Mutex同步关键操作 - 可重入设计:通过
Thread.current[:cache_recursion]记录当前线程的调用栈,遇到递归时直接执行块代码 - 缓存击穿防护:双重检查锁定(Double-Checked Locking)确保只有一个线程执行耗时操作
- TTL 实现:存储创建时间戳,在读取时校验时间差
最佳实践
- 对高频热点数据使用
Concurrent::Map#compute_if_absent原子操作 - 添加随机抖动到 TTL(如
ttl * (0.8 + rand(0.4)))避免批量失效 - 大型系统可结合
ActiveSupport::Cache::Store扩展
常见错误
- 死锁风险:未处理可重入时,同一线程二次获取锁导致死锁
- 脏读问题:非原子操作下可能返回过期数据(如先读后校验 TTL)
- 内存泄漏:未清理无效缓存或递归标记数据
扩展知识
- 缓存雪崩对策:分层缓存 + 熔断机制
- 高级优化:
- 使用Concurrent::LazyRegister延迟加载
- 通过ObjectSpace::WeakMap实现弱引用缓存
- 结合 Ractor 隔离(Ruby 3+) - 性能监控:添加
hit_rate统计和慢查询日志