使用std::unordered_map与std::chrono::steady_clock实现线程安全的TTL DNS缓存,结构含IP列表和过期时间点;异步回调中用mutex保护写入;采用惰性清理策略,查询时检查并剔除过期项。

用 std::unordered_map + std::chrono 实现缓存结构
异步 DNS 请求本身不自带缓存,得自己维护一个带 TTL 的键值对。核心是把域名(std::string)映射到 IP 地址(std::vector<:string></:string> 或 std::string)和过期时间(std::chrono::steady_clock::time_point)。别用 system_clock,它可能因系统时间调整跳变,导致缓存误判过期。
示例结构:
struct DnsCacheEntry {
std::vector<std::string> ips;
std::chrono::steady_clock::time_point expires_at;
};
std::unordered_map<std::string, DnsCacheEntry> cache_;
插入时记录当前时间 + TTL(比如 300s),查询前先比对 expires_at 和当前 steady_clock::now()。
在异步回调里更新缓存要小心线程安全
如果你用的是 getaddrinfo_a、uv_getaddrinfo 或 boost::asio::ip::tcp::resolver 这类异步 DNS 接口,回调会在非主线程执行。直接写入全局 cache_ 会引发数据竞争。
立即学习“C++免费学习笔记(深入)”;
- 最简方案:用
std::mutex保护整个 map,读写都加锁 —— 对低频 DNS 查询(每秒几次)完全够用 - 别用
std::shared_mutex试图优化读多写少场景:C++17 前不可靠,且 DNS 缓存写操作极少,收益几乎为零 - 避免在回调里做耗时操作(如日志格式化、字符串拼接),否则拖慢事件循环
过期清理不能靠定时器轮询
有人想开个后台线程每秒遍历一遍缓存删过期项,这既没必要又容易出错 —— 大量未触发的缓存条目白白占用内存,还增加锁争用。
推荐“惰性清理”策略:
- 每次
lookup()查询前,先检查 key 是否存在且未过期;若已过期,直接 erase 并返回 miss - 写入新结果时,覆盖旧 entry(不用先 erase)
- 如果业务允许,可额外加个轻量级定期清理(比如每 5 分钟调一次
clean_expired()),但仅作为兜底,不是主逻辑
这样既无额外线程,又保证缓存始终“按需保鲜”。
注意 getaddrinfo 返回的地址族和排序问题
异步 DNS 解析返回的地址列表(addrinfo* 链表)默认不保证 IPv4/IPv6 顺序,也不过滤掉不可用地址(如本地回环、链路本地)。缓存前建议做两件事:
- 只提取
AI_PASSIVE以外的、sin_addr或sin6_addr可用的地址,转成字符串存入ips - 如果业务明确只需要 IPv4,解析时传
hints.ai_family = AF_INET,避免后续过滤开销 - 别缓存空结果(
getaddrinfo返回EAI_NODATA或EAI_NONAME)—— 这类失败不等于“永久不存在”,重试更合理
缓存设计上,一个域名对应多个 IP 是常态,但不要把 localhost 或 127.0.0.1 当作有效结果存进去,除非你明确需要它们。
真正麻烦的不是怎么存,而是 TTL 判断和并发更新那几行代码的边界条件 —— 比如刚好在 expires_at 那一纳秒发生查询,或两个线程同时发现过期并尝试删除同一 key。这些地方必须用原子比较或锁包裹,不能靠“大概率不会撞上”来赌。


















