string字符串
list列表
set集合
hash哈希字典
zset有序集合
Java中的ArrayList
append
操作来修改字符串, 也就不会存在字符串扩容导致的内存重新分配emb(embstr)
形式存储,长度比较长的情况下采用 raw
形式存储ziplist 压缩列表
的结构实现. 元素较多的情况下,使用的是普通双向链表LinkedList的结构。但因为链表附加的指针占用空间消耗问题,后面代替使用了quicklist快速链表
的结构quicklist
是ziplist
和linkedlist
的混合体,它linkedlist
按段切分,每一段使用ziplist
来紧凑存储,多个ziplist
用双向指针串接起来
list-max-ziplist-size
决定, 超出规定长度的话就会新起一个LZF算法
压缩, 压缩的实际深度可以通过配置参数list-compress-depth
决定常用操作:
Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理
> rpush
> lpop
> rpop
# 以下操作为O(n)操作,慎用
> lindex # 获取指定索引位置的值
> lrange # 遍历指定索引范围的值
> ltrim books 1 -1 # 保留books列表索引【1 到 -1】区间的值
Redis中的hash实现跟Java中的HashMap实现基本类似,底层也是基于数组+链表的数据结构
不同的是:
渐进式rehash策略
扩容条件:
一般来说,当元素个数达到一维数组的长度时就会进行扩容,扩容的新数组是原来的两倍。不过如果Redis正在做bgsave
(后台异步执行快照操作进行RDB持久化),为了减少内存页的过多分离(Copy On Write机制
,详细参考: COW奶牛!Copy On Write机制了解一下),Redis尽量不去扩容,但是如果hash表已经非常满了,达到了数组长度的5倍还是会进行强制扩容
扩容实现:
渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个 hash 结构,然后在后续的定时任务中以及 hash 操作指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中, 当搬迁完成了,就会使用新的hash结构取而代之
常用操作:
> hset
> hmset # 批量set
> hget
> hgetall # entries(), 同时获取所有的key、value
Redis中的set集合类似于Java中的HashSet
, 内部实现相当于一个特殊的字典,元素全部当作key来存放,value值全部为空值,其内部键值对是无序且唯一的。
常用操作: sadd,spop,smembers,sunion 等
set结构可以用来存储活动中奖的用户ID,因为有去重功能
类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部 value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。
实现方式:
常用操作: zadd,zrange,zrem,zcard等
链表+多层索引
思考:为什么使用跳表而不是红黑树(平衡二叉树)?
ZRANGE
命令这种范围查找的情况下利用跳表里面的双向链表,可以方便地操作。另外还有缓存区域化(cache locality),所以效率不会比平衡树差参考链接:
Redis是单线程的, 虽然在新版本中引入了多线程,但多线程部分只是用来处理网络数据的读写和协议解析, 提升网络IO效率
Redis为持久化提供了两种方式:
Redis4.0后提供了混合持久化的功能
相关配置:
# 时间策略, 定时自动触发
save 900 1
save 300 10
save 60 10000
# 文件名称
dbfilename dump.rdb
# 文件保存路径
dir /home/work/app/redis/data/
# 如果持久化出错,主进程是否停止写入
stop-writes-on-bgsave-error yes
# 是否压缩
rdbcompression yes
# 导入时是否检查
rdbchecksum yes
RDB持久化的触发有两种: 自己手动触发与Redis定时触发
save
: 会阻塞当前Redis服务器,直到持久化完成,线上禁止使用bgsave
: 会fork出一个子进程,由子进程负责持久化过程,因此阻塞只会发生在fork子进程的时候save命令不常用,bgsave的执行流程如下:
bgsave的原理
fork
: redis通过fork创建子进程来进行bgsave操作cow
:copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来自动触发的场景:
save m n
规则自动触发bgsave
debug reload
时shutdown
时,如果没有开启aof,也会触发相关配置:
# 是否开启aof
appendonly yes
# 文件名称
appendfilename "appendonly.aof"
# 同步方式
appendfsync everysec
# aof重写期间是否同步
no-appendfsync-on-rewrite no
# 重写触发配置
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
# 加载aof时如果有错如何处理
aof-load-truncated yes
# 文件重写策略
aof-rewrite-incremental-fsync yes
AOF的整个流程大体来看可以分为两步,一步是命令的实时写入(如果是 appendfsync everysec
配置,会有1s损耗),第二步是对aof文件的重写。
Redis 会在收到客户端修改指令后,进行参数校验进行逻辑处理后,如果没问题,就立即将该指令文本存储到 AOF 日志中,也就是先执行指令才将日志存盘。这点不同于leveldb、HBase等存储引擎,它们都是先存储日志再做逻辑处理。
Redis 在长期运行的过程中,AOF 的日志会越变越长。如果实例宕机重启,重放整个 AOF 日志会非常耗时,导致长时间 Redis 无法对外提供服务。所以需要对 AOF 日志瘦身,也就是重写。
Redis 提供了 bgrewriteaof
指令用于对 AOF 日志进行瘦身。其原理就是开辟一个子进程对内存进行遍历转换成一系列 Redis 的操作指令,序列化到一个新的 AOF 日志文件中。序列化完毕后再将操作期间发生的增量 AOF 日志追加到这个新的 AOF 日志文件中,追加完毕后就立即替代旧的 AOF 日志文件了
AOF 日志是以文件的形式存在的,当程序对 AOF 日志文件进行写操作时,实际上是将内容写到了内核为文件描述符分配的一个内存缓存中,然后内核异步将数据刷回到磁盘. 那么机器如果突然宕机,日志内容还没完全写入到磁盘中就会出现日志丢失的情况。
Linux 的glibc
提供了fsync(int fd)
函数可以将指定文件的内容强制从内核缓存刷到磁盘。在生产环境的服务器中,Redis 通常是每隔 1s 左右执行一次 fsync 操作,周期 1s 是可以配置的。这是在数据安全性和性能之间做了一个折中,在保持高性能的同时,尽可能使得数据少丢失
Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程
得益于Redis的单线程模型,所以其事务模型使用起来很简单。
每个事务的操作都有 begin、commit 和 rollback,begin 指示事务的开始,commit 指示事务的提交,rollback 指示事务的回滚。
Redis 在形式上看起来也差不多,分别是 multi/exec/discard
。multi
指示事务的开始,exec
指示事务的执行,discard
指示事务的丢弃
作为缓存系统都要定期清理无效数据,就需要一个主键失效和淘汰策略
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中,以后会定期遍历
这个字典来删除到期的 key。除了定期删除之外,它还会使用惰性策略
来删除过期的 key,所谓惰性策略就是在客户端访问这个 key 的时候,redis 对 key 的过期时间进行检查,如果过期了就立即删除。定时删除是集中处理,惰性删除是零散处理
当Redis内存超出 maxmemory
限制时,内存的数据会开始和磁盘产生频繁的交换 (swap),这会让 Redis 的性能急剧下降,因此Redis 提供了几种淘汰策略:
noeviction
: 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru
: 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl
: 淘汰设置了过期时间的 key, key 的剩余寿命 ttl
的值越小越优先被淘汰。
volatile-random
: 淘汰设置了过期时间的 key, 不过淘汰的 key 是过期 key 集合中随机的 key。
allkeys-lru
: 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random
: 从全体的 key 集合中随机挑选淘汰
Redis集群没有使用一致性hash,而是引入了哈希槽的概念,Redis集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽
将多个命令发送到服务器,而不用等待回复,最后在一个步骤中读取该答复,提升IO效率。
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。
Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储
要查询的数据在缓存和数据库中都不存在,可能因此恶意高并发请求攻击导致后台DB崩溃
解决思路:
设置缓存时采用了相同的过期时间,然后某一时刻出现大规模的缓存失效,从而导致去查询数据库,进一步可能导致数据库压力过大的情况
解决思路:
某个时间点这个缓存过期了,恰好此时有很多针对这个缓存的查询请求,发现缓存失效后会从后端DB加载数据并更新到缓存,请求量过大的情况下可能会导致DB压力过大而崩溃
注意:
缓存击穿和缓存雪崩是不同的,前者针对的是单个key,而雪崩是很多key
解决思路:
参考缓存雪崩,类似
系统上线后,先将相关的缓存数据加载到缓存中,避免了第一次请求查询从DB中加载更新缓存
不适用,有以下问题:
问题分析:
同时有一个请求A进行更新操作,另一个请求B进行查询操作,那么会出现如下情形:
上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据
解决方案: 延时双删策略
这样就可以将1s内产生的脏数据清除了,另外这个休眠时间需要根据业务实际场景评估
失效
:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。命中
:应用程序从cache中取数据,取到后返回。更新
:先把数据存到数据库中,成功后,再让缓存失效,删除缓存问题分析:
同样假设有两个请求,一个请求A做查询操作,一个请求B做更新操作,那么会有如下情形产生
发生数据不一致的前提条件是需要步骤3的写数据库操作比2的读数据库操作耗时更短,但数据库的读操作的速度远快于写操作的,这种概率还是很低的。