数据类型:
List
Redis List 的有序指插入顺序有序,是双向链表保存元素位置,不会根据元素值自动排序;ZSet 才是根据 score 自动排序。
LPUSH mylist A
LPUSH mylist B
RPUSH mylist C
LPUSH A → [A]
LPUSH B → [B, A]
RPUSH C → [B, A, C]
ZSet
不管插入先后,永远按 score 排序
# 先插入 score=100 的 zhangsan
ZADD rank 100 zhangsan
# 后插入 score=80 的 lisi
ZADD rank 80 lisi
# 再插入 score=90 wangwu
ZADD rank 90 wangwu
执行 ZRANGE rank 0 -1(从小到大查询)
1) "lisi"
2) "wangwu"
3) "zhangsan"
Hash 哈希
一个 key,包含多个 field:value,适合存对象,类似一张小表一行。
user:100 -> {
name: "zhangsan",
age: "20",
email: "a@test.com"
}
可以单独更新某一个字段,不用序列化整个对象。
GEO
Redis GEO 底层本质就是 ZSet,key 为 GEO 的键名,member 存储地点名称,score 存放经纬度编码后的 geohash 52 位整数。使用 GEOADD 添加坐标时,参数顺序是经度在前、纬度在后,这点很容易踩坑;GEO 没有单独的删除指令,删除元素直接使用 ZREM 操作这个 ZSet 的 member。
# 1. GEOADD 添加坐标(经度 纬度 member)
GEOADD geo:cities 116.403874 39.914885 "北京" 121.473701 31.230416 "上海" 113.264385 23.129110 "广州"
geo:cities(ZSet)
member:"北京" score=geohash(116.403874,39.914885)
member:"上海" score=geohash(121.473701,31.230416)
member:"广州" score=geohash(113.264385,23.129110)
查询城市经纬度
GEOPOS geo:cities 北京 上海
计算两点距离(单位 m /km)
# 北京到上海,单位km
GEODIST geo:cities 北京 上海 km
Redis 的 Zset使用场景?
一般用Zset主要做过排行榜和延迟任务队列。
排行榜是最典型的场景,比如游戏积分榜,把玩家ID作为元素,积分作为score,用ZADD添加或更新积分,ZREVRANGE取Top N排名,ZREVRANK查某个玩家的名次,ZSCORE查具体积分,这些操作都是实时的而且效率很高。
另一个是延迟任务队列,比如电商里订单30分钟未支付自动取消。做法是把任务ID作为元素,执行时间戳作为score,后台线程用ZRANGEBYSCORE查出score小于当前时间戳的任务去执行,执行完用ZREM删掉就行,不需要引入额外的定时任务框架。
当前时间戳为 1759640000 秒,用户下单成功后,需要设置 30 分钟后触发订单自动取消;30 分钟换算为 1800 秒,因此这条延迟任务的执行时间戳 = 1759640000 + 1800 = 1759641800。我们将该执行时间戳作为 zset 的 score,订单 ID 作为 member 存入 Redis 有序集合;
# ZADD key score member
ZADD delay_task_queue 1759641800 order:10086
后台协程轮询读取 score 小于等于当前时间戳的任务,拿到任务后先执行 ZREM 删除,再执行业务逻辑(取消订单操作)。若用户在超时前完成支付,则直接通过 ZREM 移除该任务,避免触发取消订单操作。
# 取出所有到期任务,最多50条
ZRANGEBYSCORE delay_task_queue 0 1759641800 LIMIT 0 50
# 拿到 `order:10086`,执行删除:
ZREM delay_task_queue order:10086
for {
nowTs := time.Now().Unix()
// 查询所有 score <= 当前时间戳 的任务,最多取N条,避免一次捞太多
// ZRANGEBYSCORE 小到大,取0到当前时间戳,limit 0 50,一次最多拿50个任务
tasks, err := redis.ZRangeByScore("delay_task_queue", 0, nowTs, 50)
if err != nil || len(tasks) == 0 {
time.Sleep(200 * time.Millisecond) // 没任务,休息一会减少redis压力
continue
}
// 遍历到期任务
for _, taskMember := range tasks {
orderId := taskMember // order:10086
// 【重点】先删除任务,再执行业务,防止重复消费
ok, _ := redis.ZRem("delay_task_queue", orderId)
if !ok {
continue // 已经被别的进程抢走,跳过
}
// 执行业务:查询订单状态,如果还是未支付 → 关闭订单,释放库存
err := cancelUnpaidOrder(orderId)
if err != nil {
// 执行失败,可选:重新放回zset(重试机制)
}
}
}
还有就是按范围查询的场景,比如查积分在某个区间内的玩家,用ZRANGEBYSCORE就能快速筛选,比数据库里做排序查询高效得多。
Redis 内存淘汰策略
Redis内存淘汰策略一共8种,分两大类:
不淘汰数据的:noeviction,这是Redis 3.0之后的默认策略,内存满了就拒绝写入返回报错,但读和删除还能正常工作。
淘汰数据的又分两类。一类是只在设了过期时间的key里淘汰:volatile-random随机淘汰、volatile-ttl优先淘汰快过期的、volatile-lru淘汰最久没用的、volatile-lfu淘汰使用频率最低的。另一类是在所有key里淘汰:allkeys-random随机淘汰、allkeys-lru淘汰最久没用的、allkeys-lfu淘汰使用频率最低的。
其中LFU相关的策略是Redis 4.0新增的。
Redis 过期删除策略
Redis采用的是惰性删除加定期删除的组合策略。
惰性删除就是不主动删,等到有请求访问某个key时再检查它是否过期,过期了就删掉返回null。这样做的好处是不浪费CPU去主动清理,但缺点是如果一个key过期了一直没人访问,它就会一直占着内存。
定期删除弥补了这个问题。Redis默认每秒做10次过期检查,每次随机抽取20个设了过期时间的key,删掉其中已过期的。如果这一轮过期key超过25%(也就是超过5个),就继续下一轮抽查,直到过期比例低于25%或者达到25ms时间上限才停止。
这两种策略配合使用,在CPU开销和内存浪费之间取得平衡。
使用 Redis考虑因素
两个核心原因:高性能和高并发。
高性能方面,数据库查询通常要走磁盘IO比较慢,把热点数据缓存到Redis里,下次直接从内存读,速度快很多。
高并发方面,Redis单机QPS能轻松到10万,MySQL单机通常也就几千。把部分请求通过缓存消化掉,可以大大减轻数据库的压力。
为什么 Redis 比 MySQL 要快?
三个方面。第一,存储介质不同,Redis基于内存,MySQL基于磁盘,内存的读写速度比磁盘快了几个数量级。第二,数据结构不同,Redis是键值对存储,查找用哈希表O(1)复杂度,MySQL底层是B+树,O(logN)复杂度。第三,线程模型不同,Redis单线程模型省去了多线程竞争和上下文切换的开销。
本地缓存和 Redis 缓存的区别?
本地缓存就是把数据存在应用进程的内存里,比如Java的Caffeine、Guava Cache。优势是访问速度极快没有网络开销,延迟极低。劣势是受单机内存限制、可扩展性差,而且多个应用实例之间的缓存无法共享,存在数据一致性问题。
Redis是分布式缓存,数据存在独立的Redis服务上。优势是可扩展性强、多个应用实例共享同一份缓存、数据一致性好。劣势是有网络开销,访问速度比本地缓存慢。
选择的时候看场景:数据量小、对延迟要要求极高的用本地缓存;数据量大、需要多实例共享、需要支持高并发的用Redis。实际项目中也经常两级缓存结合使用。
高并发场景,Redis 单节点 + MySQL 单节点能有多大的并发量?
如果缓存命中的话,4核8G的配置下Redis可以支撑10万QPS左右。如果缓存没命中请求打到MySQL,同样配置下MySQL大概只能支撑5000左右的QPS。