Redis 练习网站

Redis 为什么那么快

1. 高速的存储介质

redis 使用内存作为存储介质,内存的随机访问速度比硬盘快

2. 优良的底层数据结构

3. 高效的线程模型

Redis 使用单线程处理网络所有请求

4. 高效的网络IO模型

Redis 数据结构

SDS (simple dynamic string)

主要解决

  1. C语言以 \0 空字符串的结束符,不用遍历就可以获得字符串长度
  2. 减少String类型频繁修改时,内存需要重新不断分配的问题
  3. 内存溢出的问题
    1. struct sdshdr {
    2. //记录数组中所使用的字节数,相当于字符串的长度
    3. int len ;
    4. //记录数组中未使用的字节数量
    5. int free;
    6. //字节数组,保存字符串
    7. char buf[];
    8. }
    SDS示例

image.png

SDS 在执行拼接操作时,先会去检查给定的SDS 的空间是否足够,如果不够,先去扩展SDS空间,再进行拼接操作。

减少修改字符串时带来内存冲分配次数
因为C的字符串长度和底层数据长度存在相关联。增长或缩短一个C 字符串都会进行一次内存重分配。

hash 表

ziplist 压缩列表

skipList 跳表

跳跃表的实现比平衡树更简单,跳表主要解决单链表遍历效率慢的问题,同一个跳跃表中,各个节点保存的成员对象必须是唯一的,当时多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小进行排序,成员对象大小进行排序

quicklist 快速列表
Redis 有5种基础数据类型,string (字符串),list(列表),set(集合),hash(哈希),zset(有序集合)
同一种数据类型,可以有多种不同编码作为底层实现,同一种编码也可以作为多种数据类型上层实现
image.png

5种基本数据类型

string

string 是Redis 最简单的数据结构,Redis 所有的数据结构都以唯一字符串Key作为名称,然后通过唯一的Key 来获取相应的value 数据, value 可以是json 数据

  1. > set name hello
  2. OK
  3. > sadd name helloset
  4. (error) WRONGTYPE Operation against a key holding the wrong kind of value

string 数据类型使用非常广泛,一个常见的用途就是缓存用户信息,我们将用户信息结构体使用JSON 序列化成字符串,然后将序列化后的字符串塞进Redis来缓存,同样,去用户信息会经过一次反序列化的过程。

键值对

  1. > set name dapeng
  2. OK
  3. > get name
  4. "dapeng"
  5. > del name
  6. (integer) 1
  7. > get name
  8. (nil)

批量键值对
可以批量对字符串进行读写,可以节省网络开销

  1. > set name1 one
  2. OK
  3. > set name2 tow
  4. OK
  5. > set name2 two
  6. OK
  7. > mget name1
  8. 1) "one"
  9. > mget name1 name2
  10. 1) "one"
  11. 2) "two"
  12. > mset name1 one name2 #必须给每一个key 配有对应的value值
  13. (error) ERR wrong number of arguments for MSET
  14. > mset name1 one name2 two
  15. OK

过期和 set 命令扩展
可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。不过这个「自动删除」的机制是比较复杂的,

  1. > set name dapeng
  2. OK
  3. > get name
  4. "dapeng"
  5. > expire name 5 # 5s后过期
  6. (integer) 1
  7. > get name
  8. (nil)
  9. > setex name 5 dapeng # 相当于 set + expire
  10. OK
  11. > get name
  12. "dapeng"
  13. > get name
  14. (nil)
  15. > setnx name dapeng
  16. (integer) 1
  17. > get name
  18. "dapeng"
  19. > setnx name dapeng
  20. (integer) 0
  21. > get name
  22. "dapeng"
  23. > setnx name dalin # 因为name已经存在,所有set创建不成功
  24. (integer) 0

计数
如果value 值是一个整数,还可以对他进行自增操作,自增是有范围的,它的范围 signed long 的最大最小值,超过了这个值,Redis 会报错

  1. > set age30
  2. OK
  3. > incr age # 对age +1
  4. (integer) 1
  5. > incrby age 5
  6. (integer) 6
  7. > incrby age -5
  8. (integer) 1
  9. > decrby age -5
  10. (integer) 6
  11. > decrby age 2
  12. (integer) 4
  13. > incr number #对未设置值的number 进行incr操作时 number默认为0
  14. (integer) 1
  15. > get number1
  16. (nil)

list(列表)

Redis 的list 相当于Java 中LikedList,这意味着 list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为 O(n),这点让人非常意外。 list 能够存储大量的数据。
当列表弹出最后一个元素之后,该数据结构自动被删除,内存被回收

缺点: 由于list 底层使用的是双向链表,所以他的查询效率比较慢。

Redis 的列表结构常用来做异步队列使用,将需要延后处理的任务结构体序列化成字符串塞进 Reids 的列表,另一个线程从这个列表中轮询数据进行处理

  1. > rpush books python java golang
  2. (integer) 3
  3. > llen books # 队列的长度
  4. (integer) 3
  5. > lpop books #左进右出
  6. "python"

快速列表
Redis - 图3
如果再深入一点,你会发现 Redis 底层存储的还不是一个简单的 linkedlist,而是称之为快速链表 quicklist 的一个结构。

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

hash (字典)

Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同 Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

hash value的值只能去String

  1. > hset books java "think in java" # 命令行的字符串如果包含空格,要用引号括起来
  2. (integer) 1
  3. > hset books golang "concurrency in go"
  4. (integer) 1
  5. > hset books python "python cookbook"
  6. (integer) 1
  7. > hgetall books # entries(),key 和 value 间隔出现
  8. 1) "java"
  9. 2) "think in java"
  10. 3) "golang"
  11. 4) "concurrency in go"
  12. 5) "python"
  13. 6) "python cookbook"
  14. > hlen books
  15. (integer) 3
  16. > hget books java
  17. "think in java"
  18. > hset books golang "learning go programming" # 因为是更新操作,所以返回 0
  19. (integer) 0
  20. > hget books golang
  21. "learning go programming"
  22. > hmset books java "effective java" python "learning python" golang "modern golang programming" # 批量 set
  23. OK

set(集合)

Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值NULL(类似于 HashSet 的底层结构)。

  1. > sadd lag java
  2. (integer) 1
  3. > sadd lag java pathon go
  4. (integer) 2
  5. > smembers lag # 与插入顺序并不一致,因为set是无序的
  6. 1) "pathon"
  7. 2) "java"
  8. 3) "go"
  9. > sismember lag java # 查询某个 value 是否存在,相当于 contains(o)
  10. (integer) 1
  11. > scard lag #获取长度 相当于count()
  12. 3
  13. > spop lag # 弹出一个
  14. "pathon"

Zset (有序集合)

zset 可能是 Redis 提供的最为特色的数据结构,它也是在面试中面试官最爱问的数据结构。它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫做「跳跃列表」的数据结构。
zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

  1. > zadd books 9.0 "think in java"
  2. (integer) 1
  3. > zadd books 8.9 "java concurrency"
  4. (integer) 1
  5. > zadd books 8.6 "java cookbook"
  6. (integer) 1
  7. > zrange books 0 -1 # 按 score 排序列出,参数区间为排名范围
  8. 1) "java cookbook"
  9. 2) "java concurrency"
  10. 3) "think in java"
  11. > zrevrange books 0 -1 # 按 score 逆序列出,参数区间为排名范围
  12. 1) "think in java"
  13. 2) "java concurrency"
  14. 3) "java cookbook"
  15. > zcard books # 相当于 count()
  16. (integer) 3
  17. > zscore books "java concurrency" # 获取指定 value 的 score
  18. "8.9000000000000004" # 内部 score 使用 double 类型进行存储,所以存在小数点精度问题
  19. > zrank books "java concurrency" # 排名
  20. (integer) 1
  21. > zrangebyscore books 0 8.91 # 根据分值区间遍历 zset
  22. 1) "java cookbook"
  23. 2) "java concurrency"
  24. > zrangebyscore books -inf 8.91 withscores # 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
  25. 1) "java cookbook"
  26. 2) "8.5999999999999996"
  27. 3) "java concurrency"
  28. 4) "8.9000000000000004"
  29. > zrem books "java concurrency" # 删除 value
  30. (integer) 1
  31. > zrange books 0 -1
  32. 1) "java cookbook"
  33. 2) "think in java"
  34. > zadd list 9.0 js #如果scor相同,那么按照value值进行排序
  35. (integer) 1
  36. > zrange list 0 -1
  37. 1) "javas"
  38. 2) "java"
  39. 3) "js"

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

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

  1. create if not exists
    如果容器不存在,那就创建一个,再进行操作。比如 rpush 操作刚开始是没有列表的,Redis 就会自动创建一个,然后再 rpush 进去新元素。
  2. drop if no elements
    如果容器里元素没有了,那么立即删除元素,释放内存。这意味着 lpop 操作到最后一个元素,列表就消失了。

过期时间

一个需要特别注意的地方是如果一个字符串已经设置了过期时间,然后你调用了 set 方法修改了它,它的过期时间会消失。

  1. 127.0.0.1:6379> set codehole yoyo
  2. OK
  3. 127.0.0.1:6379> expire codehole 600
  4. (integer) 1
  5. 127.0.0.1:6379> ttl codehole
  6. (integer) 597
  7. 127.0.0.1:6379> set codehole yoyo
  8. OK
  9. 127.0.0.1:6379> ttl codehole
  10. (integer) -1

Redis持久化

如果服务器突然碰到断电的情况,由于Redis 的数据都在内存中,防止数据丢失,确保数据安全性,
RDB 是一个紧凑压缩的二进制文件,存储效率较高。
RBD 恢复数据要比AOF 快的多
RDB 无法做到实时持久化,具有较大的可能性丢失数据

RDB

Redis 以快照的方式,将当前数据状态进行保存。

save

手动执行一次保存操作,默认文件为 dump.rdb
save 指令的执行会阻塞当前Redis 服务器,直到当前RDB 过程完成为止,可以会造成长时间的阻塞,

bgsave

手动启动后台保存操作,但不是立即执行,会启动一个子线程进行持久化操作,针对save 阻塞问题进行优化。

image.png

RDB自动保存

save second changes
满足限定时间范围内key 的变化数量达到指定数量即进行持久化。

缺点

  • 存储数据量较大,效率较低,基于快照思想,每次读写都是全部数据,当数据量巨大,效率非常低。
  • 大数据量下IO 效率较低
  • 基于fork 创建子线程,内存产生额外消耗。
  • 宕机会带来数据丢失风险

AOF

AOF (append only file) 持久化,以独立的日志的方式记录每次写命令,重启时再重新执行AOF文件中命令,达到恢复数据的目的。主要作用是解决了数据持久化的实时性。AOF将写命令刷新到缓冲区,将命令同步到AOF 文件中,

AOF 写数据的三种策略(appendfsync)
always(每次)
每次写入操作均同步到AOF 文件中,数量零误差,性能较差。
everysec(每秒)默认
no (系统控制)

AOF 重写

随着命令不断写入AOF ,文件会越来越大,为了解决这个问题,Redis 引入了AOF 重写机制压缩文件体积,

  • 降低磁盘占用率,提交磁盘利用率
  • 提高持久化效率,降低持久化写时间,提交IO 性能
  • 降低数据恢复用时,提交数据恢复效率

事务

Redis 在执行的过程中,多条连续执行的指令被干扰,打断,插入,Redis 事务就是一个命令执行的队列,将一系列预定义包装成一个整体,当执行时,一次性按照添加顺序依次执行,中间不会被打断或者干扰。
multi 开启事务
exec 执行事务
discard 取消事务
如果语法错误,所有命令都不会执行,如果出现命令错误,运行错误的命令不会被执行,如对list 进行 incr操作

watch

对key 添加监视锁,在执行exec 前如果key 发生了变化,终止事务执行,
watch key1 [key2]
取消对所有key 的监视
uwatch

删除策略

过期数据不是马上删除,数据删除策略在内存占用和CPU占用之间寻找一种平衡,

惰性删除

数据到达过期时间,不做处理,等下次访问该数据时,再去判断数据是否过期,
这样操作会导致内存压力较大,

定时删除

逐出算法

当新数据进入redis ,内存不足,就要逐出一段内存

Redis 删除大key

所谓大key 就是某个key的value 比较大,本质上是value比较大的问题,
key 可以有程序自行设置的,value往往不受程序控制
因为Redis 的核心工作线程是单线程,删除大key 是一个比较耗时的操作,会阻塞线程,并发量下降,
在大于redis 4 版本中,redis可以使用unlike 会计算删除key 的成本,从而做出同步或异步删除的选择,实现异步删除
del 命令是同步

来源
https://zhuanlan.zhihu.com/p/473930220
https://www.redis.com.cn/commands/unlink.html

Redis 分布式锁

Redis 哨兵模式

缓存一致性问题

Redis主从复制

Redis集群