Redis 可以用来做什么?

Redis 是互联网技术领域使用最广泛的中间件,它的英语全称是 Remote Dictionary Service,意思是远程字典服务。Redis 以其超高性能、完美的文档、简洁易懂的源码和丰富的客户端支付在开源中间件领域广受好评。

  1. 记录帖子的点赞数、评论数和点击数(hash)。
  2. 记录用户的帖子 ID 列表(排序),便于快速显示用户的帖子列表(zset)。
  3. 记录帖子的标题、摘要、作者和封面信息,用于列表页展示(hash)。
  4. 记录帖子的点赞用户 ID 列表、评论 ID 列表,用户显示和去重计数(zset)。
  5. 缓存近期热帖内容(帖子内容空间占用比较大),减少数据库压力(hash)。
  6. 记录帖子的相关内容 ID,根据内容推荐相关帖子(list)。
  7. 如果帖子 ID 是整数自增的,可以用 Redis 来分配帖子 ID(计数器)。
  8. 收藏集和帖子之间的关系(zset)。
  9. 记录热榜帖子 ID 列表,总热榜和分类热榜(zset)。
  10. 缓存用户历史行为,进行恶意行为过滤(zset,hash)。

    Redis 基础数据结构

    Redis 有5种基础数据结构,分别为:string(字符串)、list(列表)、set(集合)、hash(哈希)、zset(有序集合)。

    string(字符串)

    string 是 Redis 最简单的数据结构。Redis 所有的数据结构都是以唯一的 key 字符串作为名称,然后通过这个唯一的字符串 key 值来获取对应的 value 值。不同类型的数组结构的差异就在于 value 的结构不一样。
    Redis 的字符串的动态字符串,是可以修改的,它的结构类似于 Java 中的 ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,最大长度为 512M。

    键值对

    1. 127.0.0.1:6379> set name codehole
    2. OK
    3. 127.0.0.1:6379> get name
    4. "codehole"
    5. 127.0.0.1:6379> exists name
    6. (integer) 1
    7. 127.0.0.1:6379> del name
    8. (integer) 1
    9. 127.0.0.1:6379> get name
    10. (nil)
    11. 127.0.0.1:6379>

    批量键值对

    可以批量对多个键值对进行读写,减少网络开销。
    1. 127.0.0.1:6379> set name1 codehole
    2. OK
    3. 127.0.0.1:6379> set name2 holycoder
    4. OK
    5. 127.0.0.1:6379> mget name1 name2 # 返回一个列表
    6. 1) "codehole"
    7. 2) "holycoder"
    8. 127.0.0.1:6379> mset name1 boy name2 girl name3 hello # 配置设置键值对
    9. OK
    10. 127.0.0.1:6379> mget name1 name2 name3 # 返回一个列表
    11. 1) "boy"
    12. 2) "girl"
    13. 3) "hello"
    14. 127.0.0.1:6379>

    过期和 set 命令扩展

    可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。不过这个自动删除的机制是比较复杂的,后绪会学习它的原理。
    1. 127.0.0.1:6379> set name codehole
    2. OK
    3. 127.0.0.1:6379> get name
    4. "codehole"
    5. 127.0.0.1:6379> expire name 2 # 设置 2 秒后过期
    6. (integer) 1
    7. 127.0.0.1:6379> get name
    8. "codehole"
    9. 127.0.0.1:6379> get name # 2 秒后获取到 null
    10. (nil)
    11. 127.0.0.1:6379> setex name 2 hello # 设置过期时间的简便写法
    12. OK
    13. 127.0.0.1:6379> get name
    14. "hello"
    15. 127.0.0.1:6379> get name # 2 秒后获取到 null
    16. (nil)
    17. 127.0.0.1:6379> setnx name hello # 如果 name 不存在则设置
    18. (integer) 1
    19. 127.0.0.1:6379> get name
    20. "hello"
    21. 127.0.0.1:6379> setnx name world # 因为 name 已经存在了,所以设置失败
    22. (integer) 0
    23. 127.0.0.1:6379> get name # 获取到的 name 没有改变
    24. "hello"
    25. 127.0.0.1:6379>

    计数

    如果 value 的值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是 signed long 的最大最小值,超过了这个值 Redis 会报错。
    1. 127.0.0.1:6379> set age 30
    2. OK
    3. 127.0.0.1:6379> incr age # 自增 1
    4. (integer) 31
    5. 127.0.0.1:6379> incrby age 5 # 可以指定自增步长
    6. (integer) 36
    7. 127.0.0.1:6379> incrby age -10
    8. (integer) 26
    9. 127.0.0.1:6379> set price 9223372036854775807
    10. OK
    11. 127.0.0.1:6379> incr price # 超出范围,报错
    12. (error) ERR increment or decrement would overflow
    13. 127.0.0.1:6379>

    list(列表)

    Redis 的列表相当于 Java 的 LinkedList,注意它是链表而不是数组。这就意味着它插入和删除操作非常快,而索引定位很慢。
    当列表弹出最后一个元素后,该数组结构将自动被删除,内存被回收。
    Redis 的列表结构常用来做异步队列使用。将需要异步处理的任务塞进 Redis 的列表,另一个线程从这个列表中轮询获取数据进行处理。

    右进左出:队列

    1. 127.0.0.1:6379> rpush books python java go c++ javascript
    2. (integer) 5
    3. 127.0.0.1:6379> llen books
    4. (integer) 5
    5. 127.0.0.1:6379> lpop books
    6. "python"
    7. 127.0.0.1:6379> lpop books
    8. "java"
    9. 127.0.0.1:6379> lpop books
    10. "go"
    11. 127.0.0.1:6379> llen books
    12. (integer) 2
    13. 127.0.0.1:6379>

    右进右出:栈

    1. 127.0.0.1:6379> rpush books1 python java go c++ javascript
    2. (integer) 5
    3. 127.0.0.1:6379> rpop books1
    4. "javascript"
    5. 127.0.0.1:6379> rpop books1
    6. "c++"
    7. 127.0.0.1:6379> llen books1
    8. (integer) 3
    9. 127.0.0.1:6379>

    慢操作

    lindex 相当于 Java 链表的 get(int index) 方法,它需要对整个链表进行遍历,性能随着 index 的增大而变差。
    ltrim 有两个参数 start_indexend_index,这两个参数定义了一个区间,在区间内的元素会被保留,区间外的元素会被 删除。可以用 ltrim 实现一个定长链表,这一点非常有用。index 可以为负数,index = -1 表示倒数第一个元素。
    1. 127.0.0.1:6379> rpush books python java go c++ javascript
    2. (integer) 5
    3. 127.0.0.1:6379> lindex books 1 # 获取索引为 1 的元素
    4. "java"
    5. 127.0.0.1:6379> lrange books 0 -1 # 获取从索引 0 至倒数第 1 个元素,相当于获取所有
    6. 1) "python"
    7. 2) "java"
    8. 3) "go"
    9. 4) "c++"
    10. 5) "javascript"
    11. 127.0.0.1:6379> ltrim books 1 -1 # 保留索引 1 至倒数第 1 个元素
    12. OK
    13. 127.0.0.1:6379> lrange books 0 -1
    14. 1) "java"
    15. 2) "go"
    16. 3) "c++"
    17. 4) "javascript"
    18. 127.0.0.1:6379> ltrim books 1 0 # 保留元素长度为负数,相当于清空列表
    19. OK
    20. 127.0.0.1:6379> llen books
    21. (integer) 0
    22. 127.0.0.1:6379>

    快速列表

    如果再深入一点,会发现 Redis 底层存储的还不是一个简单的 LinkedList,而是称之为快速链表 QuickList 的一个结构。
    在元素较少的情况下会使用一块连续的内存存储,这个结构是 ZipList,即是压缩列表。它将所有的元素紧挨着一起存储,分配的是一块连续的内存空间。当数据量较多的时候才会改成 QuickList。因为普通的链表需要附加指针空间,会比较浪费空间,而且会加重内容的碎片化。比如一个列表里存的只是 int 类型的数组,结构上还需要两个额外的指针 prevnext。所以 Redis 将链表和 ZipList 结合起来组成了 QuickList。也就是将多个 ZipList 使用双向指针串起来使用。这样即满足了快速插入删除的性能,又不会出现太大的空间冗余。

    hash(字典)

    Redis 中的字典相当于 Java 中的 HashMap,同样是数组 + 链表结构,当第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。
    不同的是 Redis 的字典只能是字符串,另外它们的 rehash 方式不一样,Java 中的 HashMap 在字典很大时,rehash 是个耗时操作,HashMap 会一次性 rehash。Redis 为是提高性能采用了渐进式 rehash 策略。
    渐近式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进的将旧 hash 的内容一点点迁移到新的 hash 结构中。
    当 hash 移除了最后一个元素后,整个数据结构将被删除,内存被回收。
    hash 结构也可以用来存储用户信息,不同于字符串需要一次性序列化整个对象,hash 可以对对象结构中的每个字段单独存取。
    hash 也有缺点,hash 结构的存储消耗要高于单个字符串。
    1. 127.0.0.1:6379> hset books java 'think in java'
    2. (integer) 1
    3. 127.0.0.1:6379> hset books golang 'concurrency in go'
    4. (integer) 1
    5. 127.0.0.1:6379> hset books pythin 'python cookbook'
    6. (integer) 1
    7. 127.0.0.1:6379> hgetall books
    8. 1) "java"
    9. 2) "think in java"
    10. 3) "golang"
    11. 4) "concurrency in go"
    12. 5) "pythin"
    13. 6) "python cookbook"
    14. 127.0.0.1:6379> hlen books
    15. (integer) 3
    16. 127.0.0.1:6379> hget books java
    17. "think in java"
    18. 127.0.0.1:6379> hset books golang 'learning go progoramming' # 更新操作所以返回 0
    19. (integer) 0
    20. 127.0.0.1:6379> hget books golang
    21. "learning go progoramming"
    22. 127.0.0.1:6379> hmset books java 'effective java' python 'learning python' # 批量设置操作
    23. OK
    24. 127.0.0.1:6379> hget books java
    25. "effective java"
    26. 127.0.0.1:6379>
    和字符串一样,hash 结构中的单个 key 也可以进行计数
    1. 127.0.0.1:6379> hset user age 18
    2. (integer) 1
    3. 127.0.0.1:6379> hincrby user age 1
    4. (integer) 19
    5. 127.0.0.1:6379> hincrby user age -10
    6. (integer) 9
    7. 127.0.0.1:6379> hget user age
    8. "9"
    9. 127.0.0.1:6379>

    set(集合)

    Redis 的集合相当于 Java 中的 HashSet,它内部的键值是无序且唯一的,它相当于一个特殊的 hash 字典结构,字典中所有的 value 都是 null。
    当 hash 移除了最后一个元素后,整个数据结构将被删除,内存被回收。
    set 结构可以用来存储活动中奖的用户 ID,因为它有去重功能,可以保证同一个用户不会多次中奖。
    1. 127.0.0.1:6379> sadd books3 java
    2. (integer) 1
    3. 127.0.0.1:6379> sadd books3 python
    4. (integer) 1
    5. 127.0.0.1:6379> sadd books3 golang
    6. (integer) 1
    7. 127.0.0.1:6379> smembers books3 # 获取所有,注意跟插入顺序不一致
    8. 1) "java"
    9. 2) "golang"
    10. 3) "python"
    11. 127.0.0.1:6379> sismember books3 java # 查询某个元素是否存在
    12. (integer) 1
    13. 127.0.0.1:6379> sismember books3 js
    14. (integer) 0
    15. 127.0.0.1:6379> scard books3 # 获取总长度
    16. (integer) 3
    17. 127.0.0.1:6379> spop books3 # 弹出一个
    18. "java"
    19. 127.0.0.1:6379>

    zset(有序列表)

    zset 可能是 Redis 提供的最为特色的数据结构,它类似于 Java 中的 SortSet 和 HashMap 的结合体,一方面它是一个set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。zset 的内部实现是跳跃列表数据结构。
    zset 中最后一个 value 被删除后,数组结构将自动删除,内存被回收。zset 可以用来存粉丝关注列表,value 存的是粉丝的 ID,score 是关注时间,可以实现按照关注时间排序显示关注列表的功能。还可以用来存学生的成绩,value 值是学生的 ID,score 是学生的成绩,可以实现按照成绩排序显示学生列表。
    1. 127.0.0.1:6379> zadd books4 9.0 'think in java'
    2. (integer) 1
    3. 127.0.0.1:6379> zadd books4 8.9 'java concurrency'
    4. (integer) 1
    5. 127.0.0.1:6379> zadd books4 8.6 'java cookbook'
    6. (integer) 1
    7. 127.0.0.1:6379> zrange books4 0 -1 # 按 score 排序获取所有
    8. 1) "java cookbook"
    9. 2) "java concurrency"
    10. 3) "think in java"
    11. 127.0.0.1:6379> zrevrange books4 0 -1 # 按 score 逆序获取所有
    12. 1) "think in java"
    13. 2) "java concurrency"
    14. 3) "java cookbook"
    15. 127.0.0.1:6379> zcard books4 # 获取长度
    16. (integer) 3
    17. 127.0.0.1:6379> zscore books4 'java concurrency' # 获取指定 value 的 score
    18. "8.9000000000000004" # 内部 score 使用 double 进行存储,存在小数精度问题
    19. 127.0.0.1:6379> zrank books4 'java concurrency' # 获取指定 value 的排名
    20. (integer) 1
    21. 127.0.0.1:6379> zrangebyscore books4 0 8.91 # 根据分值区间遍历
    22. 1) "java cookbook"
    23. 2) "java concurrency"
    24. 127.0.0.1:6379> zrangebyscore books4 -inf 8.91 withscores # 根据分值区间遍历,同时返回分值。inf 代表 infinite,无穷大的意思
    25. 1) "java cookbook"
    26. 2) "8.5999999999999996"
    27. 3) "java concurrency"
    28. 4) "8.9000000000000004"
    29. 127.0.0.1:6379> zrem books4 'java concurrency' # 删除 value
    30. (integer) 1
    31. 127.0.0.1:6379> zrange books4 0 -1
    32. 1) "java cookbook"
    33. 2) "think in java"
    34. 127.0.0.1:6379>

    容器型数据结构的通用规则

    list、set、hash、zset 这四种数据结构都是容器型数据结构,它们共享下面两条通用规则。

    create if not exists

    如果容器不存在,那就创建一个,再进行操作。

    drop if no element

    如果容器里面没有元素了,那么立即删除容器,释放内存。

    过期时间细节

    Redis 所有的数据结构都可以设置过期时间,过期时间是以对象为单位,比如 hash 结构的过期时间是整个 hash 对象,而不是其中的某个 key
    还需要注意的是,如果一个 string 已经设置了过期时间,然后再调用了一个 set 方法修改了它,那么它的过期时间将失效。
    1. 127.0.0.1:6379> set str1 hello
    2. OK
    3. 127.0.0.1:6379> expire str1 30
    4. (integer) 1
    5. 127.0.0.1:6379> ttl str1
    6. (integer) 25
    7. 127.0.0.1:6379> set str1 world # 重新调用 set
    8. OK
    9. 127.0.0.1:6379> ttl str1 # 过期时间失效
    10. (integer) -1
    11. 127.0.0.1:6379>