Redis-笔记

数据类型:

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。