一、数据类型
String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)、BitMap(二进制状态统计)、HyperLogLog(基数统计)、GEO(地理信息)、Stream(流)
String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。
List 类型的应用场景:消息队列(但是有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。
Hash 类型:缓存对象、购物车等。
Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。
Zset 类型:排序场景,比如排行榜、电话和姓名排序等。
BitMap(2.2 版新增):二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等;
HyperLogLog(2.8 版新增):海量数据基数统计的场景,比如百万级网页 UV 计数等;
GEO(3.2 版新增):存储地理位置信息的场景,比如滴滴叫车;
Stream(5.0 版新增):消息队列,相比于基于 List 类型实现的消息队列,有这两个特有的特性:自动生成全局唯一消息ID,支持以消费组形式消费数据。
string的实现方式是SDS,简单动态字符串
SDS 不仅可以保存文本数据,还可以保存二进制数据
SDS 获取字符串长度的时间复杂度是 O(1)
Redis 的 SDS API 是安全的,拼接字符串不会造成缓冲区溢出
list是链表实现的
二、单线程、多线程
使用单线程快的原因:I/O多路复用,基于内存
redis瓶颈不在于cpu,而在于内存大小和网络I/O的限制,所以用多线程没什么太大意义,多线程反而会增加开销
后来改成了多线程实现:因为随着网络硬件的性能提升,Redis 的性能瓶颈有时会出现在网络 I/O 的处理上
三、数据持久化
redis底层会把数据写入磁盘的(不一定是数据库),所以数据不会丢
现在有三种数据持久化方式:
1、AOF日志
每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,类似于mysql的日志
(1)Reids 是先执行写操作命令后,才将该命令记录到 AOF 日志里的,
好处:
避免额外的检查开销:日志是不检查语法正确性的,先写内存可以先检查
不会阻塞当前写操作命令的执行
风险:
数据可能会丢失:两步并不是原子的,写完内存可能没写进日志
可能阻塞其他操作:两步是同一个进程中完成的,写日志会阻塞下一次写内存
(2)写日志过程:
Redis 执行完写操作命令后,会将命令追加到 server.aof_buf 缓冲区;
然后通过 write() 系统调用,将 aof_buf 缓冲区的数据写入到 AOF 文件,此时数据并没有写入到硬盘,而是拷贝到了内核缓冲区 page cache,等待内核将数据写入硬盘;
具体内核缓冲区的数据什么时候写入到硬盘,由内核决定。
(3)回写磁盘策略
针对上述步骤中的最后一步,可以设置回写策略
Always:写完日志立马回写磁盘
Everysec:每秒写一次
No:交由操作系统完成
(4)AOF 日志过大,会触发重写机制
AOF日志随着时间的推移会越来越大,一旦redis重启,就要从AOF日志中重新读取数据,非常耗时
所以当AOF日志文件达到一个阈值时,会触发AOF重写机制,也就是读取数据库中所有的数据写一份新的AOF,然后替换旧的
比如,执行两次命令set a b; set a c;这时AOF日志会有两条记录,但数据库中只有一条,重建AOF日志后,就只有一条记录了
这个过程是由后台子进程bgrewriteaof完成的,这样可以避免阻塞主进程,但当跟主进程有冲突呢?使用的是AOF 重写缓冲区,当bgrewriteaof创建后,意味着要重写AOF了,那此时来的数据,除了写入AOF缓冲区外,还要写入AOF 重写缓冲区,完成AOF重写后,会把缓冲区的内容也追加进新的AOF文件的
2、RDB快照
AOF 日志记录的是操作命令,不是实际的数据,所以用 AOF 方法做故障恢复时,需要全量把日志都执行一遍,一旦 AOF 日志非常多,势必会造成 Redis 的恢复操作缓慢,所以增加了RDB快照,可以记录某一个瞬间的内存数据,方便恢复数据
会阻塞主线程吗
有两个命令来生成RDB:save、bgsave
save会在主进程中执行,所以会阻塞主进程
bgsave是后台的,不会阻塞,但会造成数据不一致,会通过写时复制来解决
bgsave是由主进程通过fork()指令创出来的,所以会有和主进程一样的内存数据,在生成快照时,如果主进程有写操作,会把这个操作也复制到RDB中
3、混合
RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。
AOF 优点是丢失数据少,但是数据恢复不快。
Redis 4.0 提出了混合持久化
把AOF中从数据库读取全量数据生成新的AOF文件改成了通过RDB快照生成,然后继续沿用AOF重写缓冲区来处理此时的主进程写操作
这样AOF前半段为RDB快照,不再是操作指令了,保证了快速读取。后半部分还是新增的写操作指令,保证了数据不会丢失。
这样的缺点就是要AOF可读性差(不是问题)
四、缓存过期删除
1、删除策略
redis可以对key设置过期时间,存储在过期字典中
有两种key过期后的删除策略
(1)惰性删除:不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。效率低
(2)定期删除:定期从所有key中取一部分,删除过期的,如果删除比例大于一个阈值,则循环此操作。可能会导致过期key没删
redis用两种结合的方法
2、持久化过程中对过期key的处理
AOF:写入的时候,AOF不会检查key是否过期,都会写上,过期了就向AOF文件中加个del操作;
重写的时候,会检查,过期的不会写到新的里面
RDB:生成的时候会检查key过期;
加载的时候只有主服务器会检查,从服务器不检查,但加载完后会去主服务器同步数据
主从模式下,从库不会主动操作的,AOF文件中添加del操作都是从主服务器同步过来的
五、内存淘汰
1、淘汰策略
策略一:noeviction(默认策略):不进行淘汰,也不再提供服务,来请求直接报错
策略二:淘汰:
对于设置了过期时间的key:
volatile-random:随机淘汰
volatile-ttl:优先时间老的淘汰
volatile-lru:淘汰最久未使用的
volatile-lfu:最少使用的键值
对于未设置过期时间的key:
allkeys-random:随机淘汰
allkeys-lru:淘汰最久未使用的
allkeys-lfu:最少使用的键值
2、redis的lru和lfu的实现
lru:给每个key设置一个最后一次访问时间,淘汰时的基数不是所有key,而是随机选一部分
原lru算法是用链表维护数据,访问了就把数据移到链表头,最后删除时直接从链尾删就行,但这样增加了链表的维护成本
lfu:redis又记录了每个key访问的频次
六、缓存读写策略(数据库和缓存不一致问题)
1、先删缓存再更新数据库,肯定不行
线程A删了缓存,线程B来读取数据,直接就去数据库读取并写入缓存返回,线程A再更新数据库,会导致缓存和数据库不一致
2、先更新数据库再删除缓存
虽然也会有问题,但操作缓存比操作数据库快的多
3、延迟双删,其实不如2,因为sleep的时间说不定
#删除缓存
redis.delKey(X)
#更新数据库
db.update(X)
#睡眠
Thread.sleep(N)
#再删除缓存
redis.delKey(X)
没有完全合适的方法,在于自己的选择
七、主从复制、哨兵模式
八、缓存击穿、雪崩、穿透
九、redis分布式锁应用
锁可以设置过期时间,但当过期时间到了,但是业务还是没有执行完,怎么办?
应该使用一个线程去定时检查业务线程是否执行完,如果锁快到期了,业务还没执行完的话,需要续期。
如果业务线程挂了,锁未释放,必须要等到自动过期吗?可以继续用一个线程去检查业务有没有挂,挂了就释放锁
十、增量同步和全量同步
1、全量同步
(1)从节点向主节点发送SYNC命令请求进行同步。
(2)主节点生成一个RDB快照,并将快照文件发送给从节点。
(3)从节点接收到快照文件后,会加载快照文件并将其中的数据集恢复到自己的内存中。
(4)主节点将从节点在同步开始时到现在所接收到的所有写命令(写命令包括set、del等操作)都记录到 Replication Buffer中,并将这些写命令发送给从节点。
(5)从节点接收到写命令后,将其重放到自己的数据集中,使从节点的数据和主节点一致。
(6)全量同步完成后,从节点成为主节点的完全复制品,并开始进行增量同步
2、增量同步
(1)主节点将发送给从节点的每个写命令都记录到内存中的复制积压缓冲区(Replication backlog)。
(2)从节点定期向主节点发送PSYNC命令进行增量同步请求。
(3)主节点比对从节点的复制偏移量(Replication offset),如果从节点的复制积压缓冲区中的数据还在,就执行部分复制(Partial Resynchronization),只发送从上次同步以后的写命令给从节点。
(4)从节点接收到写命令后,将其重放到自己的数据集中,使从节点的数据保持与主节点一致。