《Redis设计与实现》:笔记

指令

Redis指令参考:Redis Order

结构

对象9

  • type:对象的类型
    • REDIS_STRING,字符串对象
    • REDIS_LIST,列表对象
    • REDIS_HASH,哈希对象
    • REDIS_SET,集合对象
    • REDIS_ZSET,有序集合对象

数据类型

汇总一个类型对应的多种编码

  • REDIS_STRING,字符串对象
    • REDIS_ENCODING_INT
      • 要求字符串对象保存的是整数值, 并且这个整数值可以用 long 类型来表示
      • ptr属性里面(将 void* 转换成 long ), 并将字符串对象的编码设置为 int
      • 通过 APPEND 命令, 向一个保存整数值的字符串对象追加了一个字符串值,对象的编码就会从int变成raw
    • REDIS_ENCODING_RAW
      • 字符串对象保存的是一个字符串值,且长度大于 39 字节
      • ptr指针指向一个SDS对象(sdshdr)
    • REDIS_ENCODING_EMBSTR
      • 字符串对象保存的是一个字符串值,且长度小于等于 39 字节
      • 和raw一样,ptr指针指向一个SDS对象,但它的redisObject和sdshdr在空间上是连续的,所以内存分配和释放都只需要一次就可以完成(raw因为两者空间不连续,所以需要分别分配)
      • Redis 没有为 embstr 编码的字符串对象编写任何相应的修改程序,所以对embstr的任意修改,都会导致对象的编码从embstr转为raw

    long double类型的浮点数也是作为字符串值来保存的,按长度分为raw和embstr保存

  • REDIS_LIST,列表对象
    • REDIS_ENCODING_ZIPLIST
      • 使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素(可以是字符数组或者int)
      • 需要满足列表对象保存的所有字符串元素的长度都小于 64 字节,并且列表对象保存的元素数量小于 512 个;否则转换为linkedlist
    • REDIS_ENCODING_LINKEDLIST
      • 使用双端链表作为底层实现, 每个双端链表节点(node)都保存了一个字符串对象, 而每个字符串对象都保存了一个列表元素。
    • Redis3.2版本引入快速列表QuickList,它是ZipList和LinkedList的结合体,将LinkedList按段切分,每一段使用ZipList紧凑存储来节省空间

  • REDIS_HASH,哈希对象
    • REDIS_ENCODING_ZIPLIST
      • 使用压缩列表作为底层实现, 每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾
        • 保存了同一键值对的两个节点总是紧挨在一起, 保存键的节点在前, 保存值的节点在后;
        • 先添加到哈希对象中的键值对会被放在压缩列表的表头方向, 而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。
      • 需要满足哈希对象保存的所有键值对的键和值的字符串长度都小于 64 字节,并且哈希对象保存的键值对数量小于 512 个;否则转换为hashtable来实现
    • REDIS_ENCODING_HT
      • 使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存
  • REDIS_SET,集合对象
    • REDIS_ENCODING_INTSET
      • 使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面
      • 需要满足集合对象保存的所有元素都是整数值,并且集合对象保存的元素数量不超过 512 个;否则转换为hashtable实现
    • REDIS_ENCODING_HT
      • 使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 NULL
  • REDIS_ZSET,有序集合对象(每个节点有成员member和分值score)
    • REDIS_ENCODING_ZIPLIST
      • 使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score),压缩列表的集合元素按照分值从小到大排序

      文中似乎没有说这里的排序方式,如何保持有序性呢?压缩列表中的元素是按照插入顺序存储的,所以新添加的元素会按照其分数值的大小插入到合适的位置,以保持有序性。这样,即使有新元素的加入,Redis 仍然能够在压缩列表中维护有序集合的顺序。

      • 需要满足有序集合保存的元素数量小于 128 个,并且有序集合保存的所有元素成员的长度都小于 64 字节;否则转换为skiplist实现
    • REDIS_ENCODING_SKIPLIST
      • 使用 zset 结构作为底层实现, 一个 zset 结构同时包含一个字典dict和一个跳跃表zskiplist
        • 跳跃表按分值从小到大保存了所有集合元素, 每个跳跃表节点都保存了一个集合元素。通过这个跳跃表, 程序可以对有序集合进行范围型操作, 比如 ZRANK 、ZRANGE 等命令就是基于跳跃表 API 来实现的。
        • 字典为有序集合创建了一个从成员到分值的映射, 字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。 通过这个字典, 程序可以用 O(1) 复杂度查找给定成员的分值

持久化

Redis4之后支持AOF+RDB混合持久化的方式,先使用RDB存储快照,然后用AOF持久化记录所有的写操作

当满足重写策略或手动触发重写时,将最新的数据存储为新的RDB记录

当Reids重启时,先加载RDB的部分,再加载剩余的AOF部分

Lua脚本

限流脚本

1
2
3
4
5
6
7
8
local times = redis.call('incr', KEYS[1])
if times == 1 then
redis.call('expire', KEYS[1], ARGV[1])
end
if times > tonumber(ARGV[2]) then
return 0
end
return 1

执行命令:redis-cli --eval limit.lua rate.limit:127.0.0.1 , 5 6

6秒内执行5次以上命令,则会返回0,达到限流目的

秒杀抢购

利用Lua脚本的原子性和安全性来实现订单数量和库存数量的统一(避免超卖)

1
2
3
4
5
6
7
8
local count = redis.call('get', KEYS[1])
if (tonumber(count) >= 0)
then
redis.call('incr', ARGV[1])
return redis.call('decr', KEYS[1])
else
return -1
end
1
2
3
4
> redis-cli
> set stocknumber 10
> exit
> redis-cli --eval seckill.lua stocknumber , ordernumber

主从复制

1
2
3
docker run -d -p 6379:6379 --name redis1 redis
docker run -d -p 6380:6379 --name redis2 redis
docker run -d -p 6381:6379 --name redis3 redis
1
2
3
4
# redis2
docker exec -it redis2 bash
redis-cli
slaveof <local ip> 6379
1
2
3
4
# redis3
docker exec -it redis3 bash
redis-cli
slaveof <local ip> 6379
1
2
3
4
5
# redis1
docker exec -it redis1 bash
redis-cli
set name clay
info replication
1
2
3
# redis2 | redis3
get name
info replication

集群

集群重新分片

1
2
3
4
redis-cli --cluster reshard 127.0.0.1:7000
# 输入需要转移的slot数量
# 输入recv节点id
# 输入src节点,填入all表示从每个主节点取一些哈希槽

扩容(添加7006节点)

1
2
3
redis-cli --cluster add-node 127.0.0.1:7006 127.0.0.1:7000  # 添加7006节点
redis-cli --cluster reshard 127.0.0.1:7000 # 按照上面的分片命令,给这个新节点分配哈希槽
... # recv节点:7006

缩容(删除7006节点)

1
2
3
redis-cli --cluster reshard 127.0.0.1:7000  # 转移7006节点的哈希槽
... # src节点:7006
redis-cli --cluster del-node 127.0.0.1:7006 # 删除7006节点

拓展

Redis6新功能

  • 多线程读写:将主线程的IO读写任务拆分出来给一组独立的线程去执行
  • ACL(Access Control List):访问控制列表,允许连接与执行用户关联,且可以限制该用户的部分使用功能(原本只有一个“默认”用户)

布隆过滤器

Redis可以安装布隆过滤器模块,使用布隆过滤器判断某个元素是否已存在。布隆过滤器通过哈希将所有元素映射到一个固定长度的数组,这样可以减少存储,但会有一定的误判概率

在误判概率0.01时,每个元素占用9.6bit

在误判概率0.00001时,每个元素占用28.8bit

布隆过滤器的基本原理是使用一个固定长度的位数组和多个哈希函数。当一个元素被加入集合时,这些哈希函数会生成多个哈希值,位数组对应这些哈希值的位会被置为1。当检测一个元素是否在集合中时,布隆过滤器会计算该元素的哈希值,检查位数组中相应的位是否都是1。如果是,元素可能存在于集合中;如果不是,元素一定不在集合中。

内存碎片

  • Redis内存碎片形成
    • 内存分配机制:内存分配器一半是按照固定大小进行分配的,而Redis每次请求分配的内存是不一样的
    • Redis的负载特征:使用过程中对键值对的修改和删除操作,会导致内存释放和不连续,也就是内存碎片
  • Reids清理内存碎片
    • Redis4之前:通过重启Redis服务
    • Redis4之后:Redis提供了内存碎片自动清理的方法

分布式锁

在多机数据库情况下,我们应该在多个进程中维护同一个对象,对同一个资源进行控制,这就需要用分布式的锁进行控制

非阻塞锁:进程抢到锁,返回true;没有抢到锁,返回false
阻塞锁:进程抢到锁,返回true;没有抢到锁,进程重试

生产问题

缓存穿透

缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了

举例:故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据,造成巨大压力

  1. 缓存无效 key
    1. 可以解决请求的 key 变化不频繁的情况
    2. 如果恶意攻击每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key,无法根本解决问题
  2. 布隆过滤器
    1. 把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端
    2. 添加到集合中的元素越多,误报的可能性就越大
  3. 接口限流
    1. 根据用户或者 IP 对接口进行限流,对于异常频繁的访问行为,还可以采取黑名单机制

缓存击穿

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了

举例:秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,造成巨大压力

  1. 永不过期(不推荐):设置热点数据永不过期或者过期时间比较长
  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
  3. 加锁(看情况):在缓存失效后,通过设置互斥锁确保只有一个请求去查询数据库并更新缓存

缓存雪崩

缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了

举例1:缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上

  1. Redis 集群:采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用
  2. 多级缓存:设置多级缓存,例如本地缓存+Redis 缓存的二级缓存组合,当 Redis 缓存出现问题时,还可以从本地缓存中获取到部分数据

举例2:数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力

  1. 设置随机失效时间(可选):为缓存设置随机的失效时间,例如在固定过期时间的基础上加上一个随机值,这样可以避免大量缓存同时到期,从而减少缓存雪崩的风险
  2. 提前预热(推荐):针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期
  3. 持久缓存策略(看情况):虽然一般不推荐设置缓存永不过期,但对于某些关键性和变化不频繁的数据,可以考虑这种策略