Skip to content

Redis Redis-AI整理版 ​

基于 Redis 重新整理。

目标不是“把原文再说一遍”,而是把内容改成更适合学习、复习、回看的笔记结构。

这份笔记怎么读 ​

原文把整段官方文档塞在表格单元格里,本整理版把每个数据类型重排为“速查表 + 关键命令详解 + 应用场景”。建议先过五大数据类型,再看分布式锁与淘汰策略,最后手写一遍 LRU。

学习路线图 ​

阶段重点说明
入门五大数据类型会用常用命令,知道各自的应用场景
进阶分布式锁setnx 的原子性与看门狗续期
进阶过期与淘汰策略“2 个维度、4 个方面”和三大过期删除策略
手写LRU 算法用 Map + 双向链表实现一遍

一、基本数据结构总览 ​

参考:Redis 数据结构 - 简书

redis基本数据结构

redis数据结构简单图1

跳表图


二、string ​

1. 速查表 ​

命令作用
SET key value [EX seconds] [PX milliseconds] [NX|XX]将字符串值 value 关联到 key
GET key返回与键 key 相关联的字符串值
STRLEN key返回键 key 储存的字符串值的长度
INCR key为键 key 储存的数字值加上一
DECR key为键 key 储存的数字值减去一

2. 关键命令详解 ​

SET:

  • key 已持有其他值时直接覆写旧值,无视类型;对带有 TTL 的键设置后,原有的 TTL 被清除
  • 可选参数(Redis 2.6.12 起):EX seconds 秒级过期(等同 SETEX)、PX milliseconds 毫秒级过期(等同 PSETEX)、NX 只在键不存在时设置(等同 SETNX)、XX 只在键已存在时设置
  • 返回值:设置成功返回 OK;使用了 NX/XX 但条件不满足时返回空批量回复(NULL Bulk Reply)
  • 因为 SET 的参数可以覆盖 SETNX、SETEX、PSETEX 的效果,将来的版本可能会废弃这三个命令

GET:键不存在返回 nil;值并非字符串类型返回错误。STRLEN:键不存在返回 0;不是字符串值返回错误。

INCR / DECR:

  • 键不存在时先初始化为 0 再操作;值不能解释为数字时返回错误;限制在 64 位有符号数字之内
  • INCR 是针对字符串的操作:Redis 没有专用的整数类型,执行时值会被解释为十进制 64 位有符号整数
  • 返回执行操作之后的值

3. 应用场景 ​

  1. 分布式锁(setnx)
  2. 递增、递减(点赞数,只是统计被点赞总数)

三、hash ​

对应 Java 的 Map<String, Map<K, V>> 结构。

1. 速查表 ​

命令作用
HSET hash field value将哈希表 hash 中域 field 的值设置为 value
HGET hash field返回哈希表中给定域的值
HMSET key field value [field value …]同时将多个域-值对设置到哈希表 key 中
HMGET key field [field …]返回哈希表 key 中一个或多个给定域的值
HGETALL key返回哈希表 key 中所有的域和值
HLEN key返回哈希表 key 中域的数量
HDEL key field [field …]删除哈希表 key 中一个或多个指定域
HEXISTS hash field检查给定域 field 是否存在于哈希表 hash 当中

2. 关键命令详解 ​

  • HSET:哈希表不存在时先创建再操作;域 field 已存在时旧值被新值覆盖
  • HMSET:同时设置多个域-值对,会覆盖已存在的域;key 不存在时先创建空哈希表
  • HMGET:不存在的域返回 nil;不存在的 key 被当作空哈希表处理
  • HGETALL:返回值里每个域名之后紧跟域的值,所以返回值长度是哈希表大小的两倍
  • HDEL:不存在的域将被忽略;HEXISTS:存在返回 1,不存在返回 0

3. 应用场景 ​

购物车场景,存储用户基础信息。

用户基础信息


四、list ​

list 是有序的,且可以重复。

1. 速查表 ​

命令作用
LLEN key返回列表 key 的长度
LPOP key移除并返回列表 key 的头元素
LREM key count value根据参数 count 移除列表中与 value 相等的元素
LPUSH key value [value …]将一个或多个值 value 插入到列表 key 的表头
RPUSH key value [value …]将一个或多个值 value 插入到列表 key 的表尾(最右边)
LPUSHX key value仅当 key 存在且是列表时,将 value 插入表头
RPUSHX key value仅当 key 存在且是列表时,将 value 插入表尾

2. 关键命令详解 ​

  • LPUSH / RPUSH:多个 value 从左到右依次插入表头/表尾。对空列表执行 LPUSH mylist a b c 结果是 c b a,RPUSH mylist a b c 结果是 a b c。key 不存在时先创建空列表再操作;key 不是列表类型时返回错误
  • LPUSHX / RPUSHX:与 LPUSH / RPUSH 相反,当 key 不存在时什么也不做
  • LREM 的 count:count > 0 从表头向表尾移除 count 个;count < 0 从表尾向表头移除 |count| 个;count = 0 移除所有与 value 相等的值
  • LLEN:key 不存在时按空列表处理返回 0;不是列表类型返回错误

3. 应用场景 ​

微信文章订阅公众号。

指定用户订阅文章


五、set ​

集合,不可重复。

1. 速查表 ​

命令作用
SADD key member [member …]将一个或多个 member 元素加入到集合 key 当中
SREM key member [member …]移除集合 key 中的一个或多个 member 元素
SMEMBERS key返回集合 key 中的所有成员
SISMEMBER key member判断 member 元素是否是集合 key 的成员
SCARD key返回集合 key 的基数(元素数量)
SRANDMEMBER key [count]返回集合中的一个或多个随机元素(不移除)
SPOP key移除并返回集合中的一个随机元素
SDIFF key [key …]返回所有给定集合之间的差集

2. 关键命令详解 ​

  • SADD:已存在的 member 会被忽略;key 不存在时先创建集合;不是集合类型返回错误。SREM:不存在的 member 会被忽略
  • SMEMBERS:不存在的 key 被视为空集合。SISMEMBER:是成员返回 1,不是或 key 不存在返回 0
  • SRANDMEMBER:只给 key 时返回一个随机元素;count 为正且小于基数时返回 count 个互不相同的元素,大于等于基数时返回整个集合,为负时返回可能重复、长度为 |count| 的数组。它只返回不移除
  • SPOP:移除并返回一个随机元素,key 不存在或空集时返回 nil。SDIFF:返回差集成员列表,不存在的 key 视为空集

3. 应用场景 ​

  1. 随机抽奖小程序
  2. 微信朋友圈点赞
  3. 微博好友关注的社交关系(使用 SINTER key 获取交集)
  4. QQ 推荐可能认识的人(使用 SDIFF key 获取差集)

随机抽奖

QQ 推荐可能认识的人(注意:这里把好友本身算进去了,实际需要去除):

可能认识的人


六、zset ​

有序集合,不可重复。在有序集合中加入了一个元素和该元素的分数。object encoding key 是压缩列表(ziplist,O(N))和跳表(skiplist,O(logN))。

1. 速查表 ​

命令作用
ZADD key score member [[score member] …]将一个或多个 member 元素及其 score 值加入到有序集 key 当中
ZRANGE key start stop [WITHSCORES]返回有序集 key 中指定区间内的成员(按 score 递增)
ZSCORE key member返回有序集 key 中成员 member 的 score 值
ZREM key member [member …]移除有序集 key 中的一个或多个成员
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]返回 score 值介于 min 和 max 之间的成员
ZINCRBY key increment member为成员 member 的 score 值加上增量 increment
ZCARD key返回有序集 key 的基数
ZCOUNT key min max返回 score 值在 min 和 max 之间的成员数量

2. 关键命令详解 ​

  • ZADD:member 已是有序集成员时更新 score,并通过重新插入保证它在正确的位置上;score 可以是整数或双精度浮点数;key 不存在时先创建空的有序集
  • ZRANGE:按 score 递增排列,相同 score 按字典序;下标以 0 为底,可用负数(-1 表示最后一个成员);超出范围的下标不报错;需要递减时用 ZREVRANGE;WITHSCORES 让成员和 score 一并返回(value1, score1, ..., valueN, scoreN)
  • ZRANGEBYSCORE:返回 score 介于 min 和 max 之间(默认闭区间)的成员;给参数前加 ( 可用开区间;min/max 可以是 -inf 和 +inf;LIMIT 参数类似 SQL 的 SELECT LIMIT offset, count,offset 很大时定位可能要遍历整个有序集,最坏 O(N)
  • ZINCRBY:传负数 increment 即做减法;key 不存在或 member 不是成员时等同于 ZADD;返回新 score 值(字符串形式)

3. 应用场景 ​

  1. 根据商品的销售进行排序
  2. 点赞排行榜的排序
  3. 战力榜排序

战力榜


七、Redis 分布式锁 ​

1. 什么是分布式锁 ​

分布式微服务架构拆分后,各个微服务之间为了避免冲突和数据故障而加入的一种锁。可以用 mysql、zookeeper、redis(setnx)实现,一般常用的是 redis 分布式锁。

2. redis 做分布式锁要注意的问题 ​

  1. redis.setnx 的时候需要注意原子性
  2. redis.del 的时候需要注意原子性,确保删除的是该线程持有的锁,一般采用 lua 脚本实现(官网推荐)
  3. setnx 时设置了过期时间,可能存在过期时间结束了线程还未执行结束的情况,这时其他线程可以获得锁。可以在获取锁的线程后台启动一个守护线程,每次快到过期时间时重置过期时间

3. 常见追问 ​

4. redis 分布式锁如何续期(看门狗) ​

通过守护线程“看门狗”,每隔 10s(internalLockLeaseTime / 3)检查业务是否完成,未结束就续期,每次续命值默认为 30s(internalLockLeaseTime)。

看门狗是个守护线程,必须有线程守护:服务宕机时守护线程关闭、不再续期,key 直到过期失效。

不用 lua 脚本的话,还可以使用事务。


八、缓存过期与淘汰策略 ​

1. maxmemory 相关 ​

  • maxmemory 设置为 0 或未设置时:64 位系统下不限制内存大小,32 位系统下最多使用 3GB;默认配置为 0
  • redis 内存设置一般推荐为最大物理内存的四分之三
  • 常用命令:config get maxmemory 查看最大限制内存、config set maxmemory 1 设置、info memory 查看内存信息
  • 修改方式:改配置文件的 maxmemory 属性,或 config set maxmemory value

内存满了再执行 set k1 v1 会抛出:OOM command not allowed when used memory > 'maxmemory'.

2. redis 缓存淘汰策略(8 种) ​

策略说明
volatile-lru对所有设置了过期时间的 key 使用 LRU 算法进行删除
allkeys-lru对所有 key 使用 LRU 算法进行删除
volatile-lfu对所有设置了过期时间的 key 使用 LFU 算法进行删除
allkeys-lfu对所有 key 使用 LFU 算法进行删除
volatile-random对所有设置了过期时间的 key 使用随机删除
allkeys-random对所有的 key 使用随机删除
volatile-ttl删除马上要过期的 key(TTL 最小的)
noeviction不驱逐任何 key,写操作直接返回错误
  • LRU means Least Recently Used(最近最少使用);LFU means Least Frequently Used(最常用)
  • 记忆框架:2 个维度(过期键中筛选 / 所有键中筛选)x 4 个方面(lru / lfu / 随机 / ttl)
  • 实际最常用 allkeys-lru;没有配置 MAXMEMORY POLICY 时默认为 noeviction

3. 三大过期删除策略 ​

一个键到了过期时间之后,不会马上从内存中删除,redis 采用的是:

  1. 定时删除:对 CPU 不友好,用处理器性能换取存储空间(时间换空间)
  2. 惰性删除:数据达到过期时间后,下次调用再进行删除,会造成大量无效数据,除非 FLUSHDB(空间换时间)
  3. 定期删除:每隔一段时间执行一次删除过期键操作,并限制删除操作的时长和频率来减少对 CPU 时间的影响

定期删除的特点

即使这样还是会有“漏网之鱼”,所以出现了淘汰策略。


九、LRU 算法的手写实现 ​

思路:1. 参考 LinkedHashMap 实现;2. 使用 Map 加 Node(双向链表)实现。

第二种思路(Map 负责 O(1) 查找,双向链表维护新旧顺序:新节点放头部,满了淘汰尾部):

java
package com.blacktea.redisson;

import lombok.extern.slf4j.Slf4j;

import java.util.HashMap;
import java.util.Map;

@Slf4j
public class MyLruDemo {

    /**
     * Node节点,作为数据的载体
     */
    static class Node<K, V> {
        Node<K, V> prev;
        Node<K, V> next;
        V value;
        K key;

        public Node(K key, V value) {
            this.prev = this.next = null;
            this.value = value;
            this.key = key;
        }

        public Node() {
            this.prev = this.next = null;
        }
    }

    /**
     * 双向链表,里面的数据就是我们的Node
     */
    class DoubleLinkedList<K, V> {
        Node<K, V> head;
        Node<K, V> tail;

        public DoubleLinkedList() {
            head = new Node<>();
            tail = new Node<>();
            head.next = tail;
            tail.prev = head;
        }

        public void addHead(Node<K, V> node) {
            node.next = head.next;
            node.prev = head;
            head.next.prev = node;
            head.next = node;
        }

        public void removeNode(Node<K, V> node) {
            node.next.prev = node.prev;
            node.prev.next = node.next;
            node.prev = null;
            node.next = null;
        }

        public Node<K, V> getLast() {
            return tail.prev; // 尾结点是虚拟节点
        }
    }

    private int cacheSize;
    Map<Integer, Node<Integer, Integer>> map;
    DoubleLinkedList<Integer, Integer> doubleLinkedList;

    public MyLruDemo(int cacheSize) {
        this.cacheSize = cacheSize;                       // 缓存长度
        this.map = new HashMap<>();                       // 查找 hash
        this.doubleLinkedList = new DoubleLinkedList<>(); // 双向链表
    }

    public int get(int key) {
        if (!map.containsKey(key)) {
            return -1;
        }
        // 将Node放到队列头部
        Node<Integer, Integer> node = map.get(key);
        doubleLinkedList.removeNode(node);
        doubleLinkedList.addHead(node);
        return node.value;
    }

    public void put(int key, int value) {
        Node<Integer, Integer> node;
        if (map.containsKey(key)) {
            node = map.get(key);
            doubleLinkedList.removeNode(node);
            node.value = value;
        } else {
            node = new Node<>(key, value);
            if (map.size() == cacheSize) {
                // 缓存长度达到,淘汰链表尾部
                Node<Integer, Integer> lastNode = doubleLinkedList.getLast();
                map.remove(lastNode.key);
                doubleLinkedList.removeNode(lastNode);
            }
        }
        map.put(key, node);
        doubleLinkedList.addHead(node); // 添加到队列头
    }

    public static void main(String[] args) {
        MyLruDemo lru = new MyLruDemo(3);
        lru.put(1, 1);
        lru.put(2, 2);
        lru.put(3, 3);
        lru.put(4, 4); // 淘汰最久未使用的 1
        lru.put(3, 3); // 3 移到头部
        lru.put(5, 1); // 淘汰 2
    }
}

十、分布式锁 lua 脚本 ​

加锁脚本(hash 结构 + 可重入计数的写法):

lua
if (redis.call('exists', KEYS[1]) == 0)
    then redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
    then redis.call('hincrby', KEYS[1], ARGV[2], 1);
    redis.call('pexpire', KEYS[1], ARGV[1]);
    return nil;
end;
return redis.call('pttl', KEYS[1]);

十一、一页总结 ​

  • string:SET 的 EX/PX/NX/XX 参数一个命令覆盖 SETEX/PSETEX/SETNX,适合计数和分布式锁
  • hash:对应 Map<String, Map<K, V>>,适合存对象(购物车)
  • list:有序可重复,LPUSH/RPUSH 两头进出,适合订阅消息流
  • set:不可重复,SINTER 交集做共同关注、SDIFF 差集做可能认识的人
  • zset:带分数的有序集合,ziplist/skiplist 两种编码,做各种排行榜
  • 分布式锁:setnx 原子性 + lua 删锁 + 看门狗续期(10s 检查、30s 续命)
  • 淘汰策略:2 维度 x 4 方面共 8 种,最常用 allkeys-lru,默认 noeviction
  • 过期删除:定时、惰性、定期组合使用,仍有漏网之鱼所以需要淘汰策略
  • LRU 手写:Map + 双向链表,访问即移到头部,满了淘汰尾部