应使用 std::unordered_map 实现 dns 缓存,因其平均 o(1) 查找性能优于 std::map 的 o(log n);键必须为 std::string,值应封装为含 ip 和 steady_clock 绝对过期时间的结构体;查询时须实时校验过期并立即擦除,多线程需用 mutex 或 shared_mutex 保护。

缓存结构用 std::unordered_map 而不是 std::map
查 DNS 本质是字符串到 IP 地址的映射,键为域名(如 "github.com"),值为解析结果(如 "140.82.121.3")。std::unordered_map 平均 O(1) 查找,比 std::map 的 O(log n) 更适合高频查询。但要注意:键必须是 std::string(不能用 const char* 直接当 key,否则比较的是指针地址);值建议封装成结构体,方便后续扩展 TTL 和记录时间。
常见错误是直接存裸 IP 字符串,导致无法判断是否过期。正确做法是定义:
struct DnsRecord {
std::string ip;
std::chrono::steady_clock::time_point expires_at;
};
然后声明缓存:std::unordered_map<:string dnsrecord> cache_;</:string>
插入缓存时必须校验 TTL 并统一用 steady_clock
DNS 响应里带的 TTL 是秒数,但本地缓存需要转换为绝对过期时间点,避免反复计算或受系统时间跳变影响。用 std::chrono::steady_clock(单调时钟)而非 system_clock,否则 NTP 同步或手动调时会导致缓存提前失效或永久不淘汰。
插入逻辑示例:
void insert(const std::string& domain, const std::string& ip, int ttl_seconds) {
auto now = std::chrono::steady_clock::now();
auto expires = now + std::chrono::seconds(ttl_seconds);
cache_[domain] = {ip, expires};
}
注意点:
-
ttl_seconds可能为 0,表示禁止缓存,此时不应写入 - 某些 DNS 响应 TTL 为负数(协议允许),应视为 0
- 不要在插入时做清理——清理留到查询前或后台线程
查询时先检查过期再返回,别依赖“懒删除”
很多实现只在插入时清理旧项,但缓存可能长期不更新,导致查到过期记录。必须在 lookup() 中实时判断:
std::optional<:string> lookup(const std::string& domain) {
auto it = cache_.find(domain);
if (it == cache_.end()) return std::nullopt;
auto now = std::chrono::steady_clock::now();
if (now >= it->second.expires_at) {
cache_.erase(it); // 立即删,避免下次再查
return std::nullopt;
}
return it->second.ip;
}</:string>
关键细节:
- 用
std::optional表达“没找到或已过期”,比返回空字符串更明确 - 擦除后直接 return,不要继续用
it->second.ip—— 迭代器已失效 - 不要用
time_t或秒级时间戳做比较,浮点误差和精度丢失会导致误判
多线程访问必须加锁,且粒度不能只锁整个 map
如果 DNS 缓存在网络库中被多个 worker 线程并发调用,std::unordered_map 本身不保证线程安全。最简方案是用 std::shared_mutex(C++17)实现读写分离:
读操作(lookup)用共享锁,写操作(insert、erase)用独占锁。但注意:即使只读,find() + erase() 组合仍是写操作,不能拆成两个锁区间——中间可能被其他线程修改。
更稳妥的做法是把过期检查和擦除合并进一次独占锁:
std::optional<:string> lookup(const std::string& domain) {
std::shared_lock<:shared_mutex> lock(mutex_);
auto it = cache_.find(domain);
if (it == cache_.end()) return std::nullopt;
// ⚠️ 不能在这里 unlock!因为下面要判断 expires_at
// 所以干脆升级为独占锁,或改用读锁 + 原子时间戳预判(复杂且易错)
// 实际项目中,更推荐直接用独占锁保护整个 lookup+clean 流程
}</:shared_mutex></:string>
真实场景中,简单起见建议用 std::mutex 全局锁——除非缓存 QPS 超过 10k,否则性能差异可忽略。真正容易被忽略的是:析构函数里必须加锁清空,否则可能 crash。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











