vault backup: 2026-05-21 23:50:11

This commit is contained in:
hhs
2026-05-21 23:50:11 +08:00
parent c3779073e9
commit b2b947f9f4
3 changed files with 363 additions and 212 deletions
+117 -3
View File
@@ -198,14 +198,16 @@ students := rdb.ZRangeByScore(ctx, "score:classA", redis.ZRangeBy{
| `FLUSHDB` | OK | O(N) | 清空当前库 |
| `FLUSHALL` | OK | O(N) | 清空所有库 |
| `TYPE key` | string/hash/list... | O(1) | 查看数据类型 |
| `EXISTS key [key ...]` | existing count | O(N) | 判断 key 是否存在,支持批量 |
| `OBJECT ENCODING key` | encoding name | O(1) | 查看底层编码(排查性能利器) |
| `UNLINK key [key ...]` | deleted count | O(N) | 异步删除,不阻塞主线程 ✅ |
| `MOVE key db` | 1 or 0 | O(1) | 移动到其他数据库(集群不支持) |
| `RENAME key newkey` | OK | O(1) | 重命名(不管类型) |
| `RANDOMKEY` | key / nil | O(1) | 随机返回一个 key(集群不支持) |
```go
// 优雅删除大 key——避免 DEL 阻塞主线程
keys := []string{"large:key1", "large:key2"}
rdb.Del(ctx, keys...).Err() // 小批量 DEL
// 优雅删除大 key——首选 UNLINK(异步删除,不阻塞主线程)
rdb.Unlink(ctx, "large:key1", "large:key2")
// 或者用 SSCAN/HSCAN 分批删除字段
iter := rdb.HScan(ctx, "big:hash", 0, "", 100).Iterator()
@@ -214,6 +216,9 @@ for iter.Next(ctx) {
}
```
> [!TIP] DEL vs UNLINK
> `DEL` 是同步删除,大 key(如百万元素的 Set)会阻塞主线程。`UNLINK`(4.0+)在后台线程异步释放内存,**生产环境优先用 UNLINK**。
> [!WARNING] TTL 的陷阱
> - `TTL` 返回 `-1` 表示 key 存在但无过期时间,`-2` 表示 key 不存在
> - Redis 的过期键删除是**惰性删除 + 定期删除**混合策略,不能保证设置 EXPIRE 后立即清理
@@ -297,6 +302,115 @@ EXEC # 原子提交
> [!WARNING] Redis 事务不是"数据库事务"
> Redis 事务只保证**不中断**(没有回滚),中间执行的命令如果出错,后续命令仍会继续。如果需要"出错就回滚"的行为,请使用 Lua 脚本。
## Pub/Sub — 发布订阅
Pub/Sub 是 Redis 内置的消息广播模型:发布者向 channel 发消息,所有订阅者**实时**收到。注意它是 **fire-and-forget**——不持久化、不确认、离线消息会丢失。
| 命令 | 返回值 | 复杂度 | 说明 |
|------|--------|--------|------|
| `SUBSCRIBE channel [channel ...]` | messages | O(N) | 订阅一个或多个 channel |
| `UNSUBSCRIBE channel [channel ...]` | OK | O(N) | 取消订阅 |
| `PUBLISH channel message` | receivers count | O(N+M) | 发布消息,N=channel 订阅者数,M=pattern 匹配数 |
| `PSUBSCRIBE pattern [pattern ...]` | messages | O(N) | 按 glob 模式订阅(如 `news.*`) |
| `PUNSUBSCRIBE pattern [pattern ...]` | OK | O(N) | 取消模式订阅 |
| `PUBSUB CHANNELS [pattern]` | channels | O(N) | 列出活跃 channel |
| `PUBSUB NUMSUB [channel ...]` | channel-count pairs | O(N) | 查询各 channel 订阅者数量 |
```go
// 订阅端——监听消息
sub := rdb.Subscribe(ctx, "order:events")
ch := sub.Channel()
for msg := range ch {
fmt.Printf("channel=%s payload=%s\n", msg.Channel, msg.Payload)
}
// 发布端——广播事件
receivers, _ := rdb.Publish(ctx, "order:events", `{"orderId":"1001","status":"paid"}`).Result()
fmt.Printf("delivered to %d subscribers\n", receivers)
```
> [!QUESTION] Pub/Sub vs Stream,怎么选?
> - **Pub/Sub**:实时广播,不关心历史,用完即丢(适合通知、缓存失效广播)
> - **Stream**:持久化消息队列,支持消费组、ACK、回溯(适合业务事件、任务队列)
> - 简单规则:**需要"至少一次"保证就用 Stream,否则 Pub/Sub 足够**
## Geo — 地理位置
Redis 3.2+ 引入的地理位置类型,底层用 **ZSet** 实现(GeoHash 编码为 score),支持范围查询和距离计算。
| 命令 | 返回值 | 复杂度 | 说明 |
|------|--------|--------|------|
| `GEOADD key longitude latitude member [lon lat member ...]` | added count | O(log N) | 添加地理位置 |
| `GEOPOS key member [member ...]` | coordinates | O(N) | 获取坐标 |
| `GEODIST key member1 member2 [m\|km\|ft\|mi]` | distance | O(1) | 两点间距离 |
| `GEORADIUS key lon lat radius m\|km\|ft\|mi [WITHCOORD] [WITHDIST] [ASC\|DESC] [COUNT n]` | members | O(N+log N) | 以坐标为中心搜索 ⚠️ 6.2+ 已废弃,用 GEOSEARCH |
| `GEORADIUSBYMEMBER key member radius m\|km\|ft\|mi` | members | O(N+log N) | 以成员为中心搜索 ⚠️ 6.2+ 已废弃 |
| `GEOSEARCH key FROMLONLAT lon lat BYRADIUS radius m\|km [ASC\|DESC] [COUNT n] [WITHCOORD] [WITHDIST]` | members | O(N+log N) | ✅ 6.2+ 推荐,统一搜索命令 |
| `GEOSEARCH key FROMMEMBER member BYRADIUS radius m\|km` | members | O(N+log N) | 以成员为中心搜索 |
| `GEOSEARCHSTORE dst src ...` | stored count | O(N+log N) | 搜索结果存入新 key(6.2+) |
| `GEOHASH key member [member ...]` | geohash strings | O(N) | 返回 GeoHash 编码字符串 |
```go
// 门店入库——添加地理位置
rdb.GeoAdd(ctx, "stores:shanghai",
&redis.GeoLocation{Name: "store-001", Longitude: 121.4737, Latitude: 31.2304},
&redis.GeoLocation{Name: "store-002", Longitude: 121.4897, Latitude: 31.2397},
)
// 附近 3km 的门店——GEOSEARCH
stores, _ := rdb.GeoSearchLocation(ctx, "stores:shanghai", &redis.GeoSearchLocationQuery{
GeoSearchQuery: redis.GeoSearchQuery{
Longitude: 121.4800,
Latitude: 31.2350,
Radius: 3,
RadiusUnit: "km",
Sort: "ASC",
Count: 10,
},
WithCoord: true,
WithDist: true,
}).Result()
```
> [!TIP] GEO 底层就是 ZSet
> 可以用 `ZRANGE`、`ZREM` 等 ZSet 命令操作 Geo key。`GEODIST` 计算的是球面距离(Haversine 公式),精度在 ~0.5% 以内。
## Lua 脚本速查
Redis 2.6+ 内置 Lua 解释器,脚本在服务端**原子执行**,是实现 CAS、限流器、排行榜原子操作的利器。
| 命令 | 返回值 | 说明 |
|------|--------|------|
| `EVAL "script" numkeys key [key ...] arg [arg ...]` | script result | 执行 Lua 脚本 |
| `EVALSHA sha1 numkeys key [key ...] arg [arg ...]` | script result | 按 SHA1 执行(脚本已缓存) |
| `SCRIPT LOAD "script"` | SHA1 | 缓存脚本,返回 SHA1 |
| `SCRIPT EXISTS sha1 [sha1 ...]` | 1 or 0 array | 检查脚本是否已缓存 |
| `SCRIPT FLUSH` | OK | 清空脚本缓存 |
| `SCRIPT KILL` | OK | 终止正在执行的脚本(只限无写操作时) |
```go
// 原子扣减库存——Lua 实现 check-and-set
script := redis.NewScript(`
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock and stock >= tonumber(ARGV[1]) then
redis.call('DECRBY', KEYS[1], ARGV[1])
return 1
end
return 0
`)
result, _ := script.Run(ctx, rdb, []string{"stock:sku:1001"}, 1).Int()
// result == 1 表示扣减成功,0 表示库存不足
```
> [!WARNING] Lua 脚本的限制
> - 脚本执行期间**阻塞整个 Redis**,严禁写耗时逻辑(循环上限建议 < 5000 次迭代)
> - 脚本中不能访问外部网络或文件系统
> - 集群模式下所有 KEYS 必须在同一个 slot(用 `{hash_tag}` 保证)
> - `SCRIPT KILL` 只能终止**未执行写操作**的脚本;已写数据的脚本只能等它跑完或重启 Redis
> [!NOTE] EVAL vs EVALSHA
> `EVAL` 每次发送完整脚本源码,`EVALSHA` 只发送 40 字节的 SHA1 摘要。高并发场景**务必用 EVALSHA + SCRIPT LOAD**,减少网络开销。go-redis 的 `NewScript` 会自动处理 EVALSHA 失败后回退 EVAL。
## 关联笔记
- [[hhs/Redis/02-核心数据类型]] — 每种类型的底层编码原理