$redis->setex(“train:{$trainId}:seats“, 30, $available);的庖丁解牛
·
$redis->setex("train:{$trainId}:seats", 30, $available); 是 高并发票务系统中缓存余票的核心操作,通过 原子性写入 + 自动过期 实现高性能与最终一致性。
一、核心原理:为什么用 SETEX?
▶ 1. 命令本质
SETEX key seconds value=SET key value+EXPIRE key seconds的原子操作- 原子性保障:
- 避免
SET后未及时EXPIRE导致缓存永不过期
- 避免
▶ 2. 业务场景
- 余票缓存:
- 高频查询(如 12306 放票瞬间)
- 数据时效性要求高(30 秒内有效)
- 优势:
- 减轻 DB 压力:99% 请求命中 Redis
- 自动过期:避免脏数据长期滞留
💡 核心认知:
SETEX= 原子写入 × 自动过期 × 高性能
二、底层机制:Redis 如何执行?
▶ 1. 执行流程
▶ 2. 内存结构
redisDb结构:typedef struct redisDb { dict *dict; // 键空间(key → value) dict *expires; // 过期字典(key → expire_time) } redisDb;- 过期策略:
- 惰性删除:访问时检查过期
- 定期删除:随机抽样清理过期键
▶ 3. 性能数据
| 操作 | 耗时(RTT) | QPS(单实例) |
|---|---|---|
SETEX |
0.2ms | 100,000+ |
📌 关键点:
SETEX是 O(1) 操作,无锁设计,线程安全
三、工程实践:票务系统中的正确用法
▶ 1. 缓存更新策略
- Cache-Aside 模式:
// 查询余票 public function getAvailableSeats($trainId) { $key = "train:{$trainId}:seats"; $available = $redis->get($key); if ($available === false) { // 缓存未命中 → 查 DB $available = $db->query("SELECT available FROM inventory WHERE train_id = ?", [$trainId]); // 回填缓存(30秒过期) $redis->setex($key, 30, $available); } return $available; }
▶ 2. 缓存击穿防护
- 问题:
- 余票为 0 时,大量请求穿透到 DB
- 解决方案:
// 空值缓存(防击穿) if ($available == 0) { $redis->setex($key, 60, 0); // 延长空值 TTL }
▶ 3. 缓存与 DB 一致性
- 最终一致性方案:
- 扣减库存:先更新 DB,再删除缓存(Cache-Delete)
// 扣减余票 $db->exec("UPDATE inventory SET available = available - 1 WHERE train_id = ? AND available > 0", [$trainId]); if ($db->affectedRows() > 0) { $redis->del("train:{$trainId}:seats"); // 删除缓存 } - 优势:
- 避免并发更新导致缓存与 DB 不一致
四、避坑指南
| 陷阱 | 破局方案 |
|---|---|
| 盲目设置短 TTL | 根据业务调整(余票变化频率) |
| 忽略空值缓存 | 余票=0 时缓存空值,防击穿 |
| 先更新缓存后 DB | 必须先 DB 后缓存,否则可能写入旧数据 |
五、终极心法
**“SETEX 不是命令,
而是缓存的呼吸——
- 当你 原子写入,
你在校准时效;- 当你 自动过期,
你在铸造纯净;- 当你 删除缓存,
你在守护一致。真正的高并发,
始于对缓存的敬畏,
成于对细节的精控。”
结语
从今天起:
- 高频读数据必用
SETEX - 空值场景缓存 0 并延长 TTL
- 更新时先 DB 后删缓存
因为最好的缓存设计,
不是盲目存储,
而是精准控制每一比特的流动。
更多推荐

所有评论(0)