redis 是一种基于内存存储的 NoSQL 开源数据库,它提供了五种基本的数据类型:String、List、Hash、Set、Zset。
因为 Redis 基于内存存储,并且在数据结构上进行了大量的优化,所有它的 IO 性能比较好,因此,在实际开发中,我们会把它作为数据库和应用之间的缓存中间件。
并且因为它是非关系型数据库,所以不存在表结构之间的关联,这样能够很好的提升应用程序的数据 IO 效率。
在企业级开发中,它又提供了主从复制和哨兵,以及集群的方式去使用。在 Redis 集群中,它提供了 Hash 槽的方式去实现数据的分片,进一步提升了性能和可拓展性。
本地缓存:缓存在内存中。比如 LRUMap
优点:访问快
缺点:内存空间有限,缓存内容少
分布式缓存:
优点:解决了内存空间有限的问题
缺点:远程交互耗时
多级缓存:本地只保存需要高频率访问的热点数据,其他数据保存在分布式缓存中。
FIFO: First-In-First-Out,删除最先缓存的。
LRU: Least-Recently-Used,最近最少使用。删除掉最近不太可能访问到的数据。如果缓存中有些数据最近都没有被访问过,那就认为它之后被访问的概率低。
用 LinkedHashMap 实现
如果自己写节点的话,
LinkedNode
int key;
int value;
LinkedNode pre;
LinkedNode next;
LFU: Least-Frequency-Used:最不经常使用。删除掉最近访问频率很低的数据。如果缓存中有数据最近被访问的次数很少,那就认为它以后被访问的概率也低。
Redis 是基于内存的,而内存的读取速度要远大于磁盘的读取速度。
Redis 是单线程模型,所以没有线程切换和上下文切换的开销。
Redis 采用了 IO 多路复用模型,可以处理并发连接。
用一个过期时间字典来维护数据的过期时间。
过期删除策略
惰性删除:只有取出 key 的时候,才判断是否需要过期删除。对 CPU 友好,对内存不友好。
定期删除:定期取出一部分 key 进行过期删除。对内存友好,对 CPU 不友好。
Redis 采用的过期删除策略
定期删除 + 惰性删除 + 内存淘汰机制
Redis 内存淘汰机制了解么?
相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?
Redis 提供 6 种数据淘汰策略:
volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!
4.0 版本后增加以下两种:
volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key
基于 Reactor 模型开发的单线程模型, 称为文件事件处理器。它采用 IO 多路复用来监听多个套接字。
IO 多路复用就是一个 IO 模型,他可以实现一个线程监听多个文件句柄,只要有一个文件句柄就绪,就可以通知应用程序进行相应的读写操作。如果没有文件句柄就绪,那么就会阻塞应用程序,交出 CPU。
文件句柄:操作系统对于打开的文件的唯一识别依据。
虽然文件事件处理器是单线程的,但是使用IO 多路复用来监听多个套接字。
BIO:同步阻塞。一个线程处理一个请求(连接),一个线程在处理一个连接时,其他的请求会被阻塞。
NIO:同步非阻塞。指 IO 多路复用,一个线程处理多个请求(连接)。
AIO:异步非阻塞。用户态访问内核态,内核态会立即返回一个标记,待处理完所有事件后,发送信号返回给用户进程。
《对线面试官》 Redis 持久化 (qq.com)
是一个经过压缩后的二进制文件,默认开启。用于:
数据备份。
复制到从服务器进行主从复制。
redis 会启动定时任务,定时去 fork 子进程来进行 RDB 备份。
(1) 同步方式
bgsave 时,Redis 会 fork 一个子进程进行持久化,然后先将数据写入一个临时文件中,等数据全部写完后,再把临时文件替换掉上次持久化好的文件。
Fork
Fork 是指复制一个和当前进程一样的子进程。新进程所有的数据(变量,环境变量,程序计数器等)和原来进程一样。一般来说,父进程和子进程共用一段物理内存。
Fork 的时候,会阻塞主线程。会带来什么问题?
redis 是单线程的,fork 时,主线程被阻塞,可能会丢失数据,所以可以用一个缓存区来保存 redis 主线程阻塞时接受的数据。
写时复制技术
属于Linux的一种技术,即数据同步之前先创建临时文件,将数据保存进临时文件,等同步完成后,再将临时文件替换掉同步的数据内容。
为什么要写时复制技术?
如果不采用临时复制技术,在同步过程中,发生程序中断,那么同步的数据就不完整。
(2) 同步指令
save
:立即同步,同时阻塞 redis,当同步完成后,再恢复 redis
bgsave
:fork 出一个子线程来进行 RDB 同步。
Append only file,默认关闭。可以在 redis.conf 文件中,通过 appendonly yes
打开。
AOF 以日志形式保存 Redis 每个写操作(不保存读操作)。
只追加文件,不修改文件。
通过 AOF 缓冲区,而不是 fork 子进程来实现追加数据,但是文件太大时重写是通过 fork 进程实现的。
(1) 同步方式
客户端的请求写命令会被 append 到 AOF 缓冲区。
AOF 缓冲区会根据持久层策略(always, everysec, no)将写操作追加到磁盘的 AOF 文件末尾。这里只追加文件,不修改文件。
在 Redis 服务启动时,会读取这个 AOF 文件,并且执行上面的写操作。
当文件大小大于 server.aof_rewrite_min_size,并且当前 AOF 文件大小和上一次重写时 AOF 文件大小差的比例达到 auto-aof-rewrite-percentage 时,则重写文件。
(2) AOF 策略
always: appendfsync always
,只要有写变化,就写入 appendonly.aof 日志
everysec:appendfsync everysec
,每秒同步一次数据到磁盘。(推荐)
no:appendfsync no
,让操作系统决定何时将数据写入磁盘。
Redis默认选取AOF数据
String
常用指令:
SET key value
GET key
INCR key:key 保存的值+1
DECR key:key 保存的值-1
使用场景:
做用户的 Session 管理。
博客访问量情况
底层原理:
List
有序列表
常用指令
LPUSH
RPUSH
LPOP
RPOP
LRANGE key start stop
使用场景:可以用来实现分页查询,或者读取用户评论数
底层原理:
3.0以前:用ziplist + linkedlist保存
Hash
常用指令:
HSET key field value
HGET key field
HDEL key field
HEXISTS key field
HKEYS key
使用场景:保存一个对象,一个对象有多个属性。
我项目中用来保存用户分布地域情况
底层原理:
扩容
基于原 Hash 表的2倍创建一个新的哈希表,然后把旧哈希表的内容转移到新哈希表里。
渐进式 rehash:
旧哈希表的内容不会一下子转移到新哈希表,而是渐进式转移过去,否则一下子转移过去,可能会导致服务器在一段时间内停止服务。
用一个rehashidx字段来记录,rehashidx为0表示开始迁移。
在rehash期间,每次对字典执行增删改查操作是,程序除了执行指定的操作以外,还会顺带将 ht[0]
哈希表在rehashindex
索引上的所有键值对 rehash 到 新哈希表,当 rehash 工作完成以后,rehashindex
的值 +1
当所有数据都从旧哈希表里转移过去后,释放旧哈希表的空间。rehashidx置为-1。
收缩
类似于扩容,但是收缩成原哈希表1/2倍的空间。
负载因子
Redis中,
loader_factor
:哈希表中键值对数量 / 哈希表长度
。HashMap中,
loader_factor
:哈希 table 已用的数量 / 哈希表长度
。
当redis没有执行持久化操作(BGSAVE or BGREWAITEAOF),负载因子是1
当redis执行持久化操作(BGSAVE or BGREWAITEAOF),负载因子是5
Hash 底层和 Java HashMap 的区别?
渐进式 hash
负载因子不同
Set
常用指令:
SADD
:添加
SCARD
:获取集合成员数
SDIFF key1 [key2]
:差集
SINTER key1 [key2]
:交集
使用场景:Set可以做并集、交集、差集。可以用来查看好友的共同列表。
我在项目中用于文章点赞的信息
articleListKey 中保存了每个用户点过赞的文章(set 结构,articleListKey, articleId)
ARTICLE_LIKE_COUNT 保存了每个文章的点赞量(hash 结构,ARTICLE_LIKE_COUNT, articleId,赞数)
@Transactional(rollbackFor = Exception.class) @Override public void saveArticleLike(Integer articleId) { // 判断是否点赞 String articleLikeKey = ARTICLE_USER_LIKE + UserUtils.getLoginUser().getUserInfoId(); // 已经点过赞了, 删除文章id if (redisService.sIsMember(articleLikeKey, articleId)) { // 取消点赞 redisService.sRemove(articleLikeKey, articleId); // 文章点赞数-1 redisService.hDecr(ARTICLE_LIKE_COUNT, articleId.toString(), 1L); } else { // 未点赞则增加文章id redisService.sAdd(articleLikeKey, articleId); // 文章点赞量+1 redisService.hIncr(ARTICLE_LIKE_COUNT, articleId.toString(), 1L); } }
底层原理:
Zset
常用指令:
ZADD key score member
ZCARD key
:获取成员数
ZCOUNT key min max
: 获取指定分数区间(min到max)的成员数量
使用场景:
做排行榜
按照浏览量
按照视频播放量
按照点赞量
微博热搜榜,名称+热力值
底层原理:
(1) 查询
查询:时间复杂度是 跳表高度 h * 遍历的元素数量
那么跳表的每层都是上一层拿一半出来当索引,所以 L_1 = n,L_2 = n/2,L_3 = n/2^2,...,L_h = n/2^h
最后一层节点只有2个,于是 L_h = 2 = n/2^h,h=log_2n-1
又每层节点遍历次数不超过3,所以时间复杂度都为 O(3*(log_2N-1))=O(log_2n)
(2) 插入( 跳表何时增加高度?)
ZSET 底层维护了一 randomLevel()
函数,该方法有 1/2 的概率返回 1、1/4 的概率返回 2、1/8的概率返回 3,以此类推。
randomLevel() 方法返回 1 表示当前插入的该元素不需要建索引,只需要存储数据到原始链表即可(概率 1/2)
randomLevel() 方法返回 2 表示当前插入的该元素需要建一级索引(概率 1/4)
randomLevel() 方法返回 3 表示当前插入的该元素需要建二级索引(概率 1/8)
randomLevel() 方法返回 4 表示当前插入的该元素需要建三级索引(概率 1/16)
并且,建立二级索引的时候,同时也会建立一级索引....以此类推
怎么保证每层索引节点个数都是前一层索引的一半呢?
凡是建索引,无论几级索引必然有一级索引,所以一级索引中元素个数占原始数据个数的比率为 randomLevel() 方法返回值 > 1 的概率。那 randomLevel() 方法返回值 > 1 的概率是多少呢?因为 randomLevel() 方法随机生成 1~MAX_LEVEL 的数字,且 randomLevel() 方法返回值 1 的概率为 1/2,则 randomLevel() 方法返回值 > 1 的概率为 1 - 1/2 = 1/2。即通过上述流程实现了一级索引中元素个数占原始数据个数的 1/2。
同理,当 randomLevel() 方法返回值 > 2 时,会建立二级或二级以上索引,都会在二级索引中增加元素,因此二级索引中元素个数占原始数据的比率为 randomLevel() 方法返回值 > 2 的概率。 randomLevel() 方法返回值 > 2 的概率为 1 减去 randomLevel() = 1 或 =2 的概率,即 1 - 1/2 - 1/4 = 1/4。OK,达到了我们设计的目标:二级索引中元素个数占原始数据的 1/4。
(3) 删除
跳表删除数据时,要把索引中对应节点也要删掉
(4) 使用 ZSET 实现游戏排行榜
设 lb 是排行榜名,user 有 user1,user2
设置玩家分数:ZADD key score member
ZADD lb 89 user1
ZADD lb 91 user2
ZADD lb 70 user3
ZADD lb 98 user4
查看玩家分数:ZSCORE key member
ZSCORE lb user1 —> 89
按名次查看排行榜:ZREVRANGE key start end [WITHSCORES]
ZREVRANGE lb 0 -1 WITHSCORES
-1 表示整个排行榜
(1) user4 (2) 98 (3) user2 (4) 91 (5) user1 (6) 89 (7) user3 (8) 70
ZREVRANGE lb 0 2 WITHSCORES
查询前三的玩家
查看玩家的排名:ZREVRANK key member
增减玩家分数:ZINCRBY key incrScore member
移除玩家:ZREM key member
删除排行榜:DEL key
相同分数问题:
如果两个分数相同的用户想按照加入 zset 的先后顺序排序,可以考虑用时间戳
Bitmap
按bit位存储。
二进制数字位的 0、1 数组。数组下标用 offset 维护。
可以用来保存状态信息。
比如做用户画像,用 bitmap 来做标签:Bitmap的巧用 - 无敌小阿没 - 博客园 (cnblogs.com)
HyperLogLog
基数统计。
用于去重(合并的时候,相同的数据只计算一次。),可以用于统计登录的人数:hyperLoglog的使用 - 简书 (jianshu.com)
Geospatial
提供地理位置信息。
pub/sub
发布/订阅通信模式。可以用作消息队列(但性能不如 RabbitMQ、Kafka)
client1,client2,client5都订阅了channel1。
channel1广播message,订阅了它的client都能收到。
Pipeline
可以批量执行一系列操作,再一起返回结果。避免频繁请求响应。
Lua
redis可以使用Lua解释器来执行脚本。
multi
开启事务
执行一系列操作,每个操作都会进入队列。
exec
按顺序执行事务
Redis 的事务不支持回滚
入队前的错误会使事务失败
入队后,如果发生错误,只有发生错误的事务会失败,其他事务仍然成功
可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。
setnx(lock, uuid, 30, TimeUnit.second)
前面说了,如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在 JAVA 的 Redisson 包中有一个"看门狗"机制,已经帮我们实现了这个功能。
redisson原理:
在获取锁之后,redisson 会维护一个看门狗线程,当锁即将过期还没有释放时,会不断的延长锁 key 的生存时间(默认 30 秒)
redisson 分布式锁是一个可重入排他锁。
加锁机制:
线程去获取锁,获取成功:执行 lua 脚本,保存数据到 redis 数据库。
线程去获取锁,获取失败:一直通过 while 循环尝试获取锁,获取成功后,执行 lua 脚本,保存数据到 redis 数据库。
watch dog 自动延期机制:
看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用 redisson 进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。
redisson 在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的 1/3 处(默认 10 秒),如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是 30 秒,可以通过 lockWactchdogTimeout 参数来改变。
加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。
那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了。
redisson 分布式锁的关键点:
对 key 不设置过期时间,由 Redisson 在加锁成功后给维护一个 watchdog 看门狗,watchdog 负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效
通过 Lua 脚本实现了加锁和解锁的原子操作
通过记录获取锁的客户端 id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁。
Redisson的使用:
在方案三中,我们已经演示了基于Redisson的RedLock的使用案例,其实 Redisson 也封装可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等,具体使用说明可以参考官方文档:Redisson的分布式锁和同步器
当缓存和数据库里都没有一个数据时,用户恶意请求该数据,导致 redis 频繁请求数据库,给数据库造成极大的压力。比如,产品id不可能为-1,但用户频繁请求产品id为 -1 的数据,导致缓存穿透问题。
解决办法:
业务层增加filter接口,对请求进行合法性检查,过滤不合法的请求,比如请求 id 为 -1的数据。
缓存中设置一个 null 值,并设置较小的过期时间。当请求没有的数据时,就返回空。当数据库存入该数据后,及时更新缓存。
布隆过滤器
1. 对于一个数key,通过多个hash函数,算出多个值,每个值都在布隆过滤器对应的位置上置1。 2. 当进来一个数,通过多个hash计算,去找对应位置上的值,如果该位置上有0,则该数一定不存在 - 如果位置上都是1,该数==不一定==存在。
优点:
1. 能说明一个数一定不存在
缺点:
1. 不能说明一个数一定存在
2. 不能删除数据。
3. 数据量大的时候,会出现误判
数据库中有数据,缓存中的该数据过期了。大量用户请求该数据,导致redis频繁读取数据库,给数据库造成极大的压力,甚至崩溃。
解决办法:
给热点数据设置成永不过期。
对于缓存中没有的数据,如果收到大量请求,则进行加锁,只放一条请求进来,然后去读取数据库,并加载到内存中。接着,再放开这个锁。其他请求需要等待解锁,并且要设置一个阈值,如果等待时间过长,则直接返回空。
数据库里有数据,而缓存中正好有大量的数据过期。这时,大量用户请求这些数据,导致 redis 频繁读取数据库,给数据库造成极大的压力,甚至奔溃。
解决办法:
给热点数据设置成永不过期。
可以考虑给缓存的时间设置波动过期值,避免大量数据一起过期。
也可以考虑使用双缓存模式,A缓存中热点数据永不过期,B缓存中热点数据可以过期,当数据过期后,去A缓冲中读取数据。
特点
写读分离:主服务器写,从服务器读,从服务器不能写
从服务器挂掉:
master服务器下的一个 slave1 服务器 s1 挂掉,s1 重启后,会自动变成一个 master 服务器。
把挂掉的服务器重新赋成 master 服务器的 slave,slave 仍然能读到 master 中的数据。
主服务器挂掉:
master 服务器挂掉后,它的 slave 服务器仍然是它的 slave 服务器。
master 重启后,仍然是原来的 master。
CAP原理:
C - Consistent ,一致性
A - Availability ,可用性
P - Partition tolerance ,分区容忍性 分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」。
在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致性」将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务。
一句话概括 CAP 原理就是——网络分区发生时,一致性和可用性两难全。
Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。
主从复制过程
slave 开启主从复制:设置主服务器 master 的 ip 和 port。主从复制开启全是由从服务器 slave 发起。
可以通过 slaveof
命令来设置从服务器的主服务器。
有三种实现方式:
conf文件中写入:slaveof master_ip master _port
redis-server --slaveof master_ip master_port
redis-cli
后,输入slaveof master_ip master _port
建立 socket 连接:开启主从复制后,master 和 slave 之间建立 socket 连接。
检查连接:slave 向 master 发送一个 ping 请求,来检查两者的连接状态。
如果 slave 收到 pong,证明连接正常。
如果 slave 没有收到 pong,或者收到错误信息,则 m 和 s 断开socket连接,并重连。
身份验证:如果 master 和 slave 都没有设置密码或者密码相同,则可以进行同步。否则,断开socket,并且重连。
同步:连接正常并且完成身份验证后,slave 向 master 发送 psync
命令,然后 master 和 slave 之间同步数据。slave 把数据库状态同步到和 master 相同。
全量复制
部分复制
命令传播:同步完成后,master 进行的新操作都要传播给 slave。
延迟传播:master 积攒多个 tcp 包后,再发送给 slave,通常是 40ms 发一次。提高了性能,损失了一致性。
立即传播:master 每进行一次操作,立即把新操作发送给 slave。保证了一致性,降低了性能。
以上通过 master 配置中的 repl-disable-tcp-nodelay 来设置。yes为延迟传播,no为立即传播。
全量复制
slave 给 master 发送 sync 命令,请求全量复制
master 将执行 bgsave,fork 一个子进程来写 RDB 文件,并且使用一个缓存,缓存放入写完 RDB 之后的新操作。
master 给 slave 发送 RDB 文件,slave 收到以后,删除旧数据,执行 RDB,与 master 进行同步。
master 给 slave 发送缓存区的写操作,slave 执行并保持和 master 同步的状态。
如果 slave 开启了 AOF,则会执行 bgrewriteaof,更新AOF至最新的状态。
部分复制
Redis 2.8 以后,引入部分复制。slave 给 master 发送 psync 命令。然后决定是使用全量复制还是部分复制。
offset + runId
offset
master 和 slave 都维护了一个 offset 字段,保存的是现在数据的偏移量。如果 slave 的 offset 和 master 的 offset 一致,则不需要进行数据复制。如果 slave 的 offset 和 master 的 offset 不一致,则需要进行数据同步。
runid
master 和 slave 会各自维护一个runid,来决定是进行全量复制还是部分复制。slave 断线重连后,会将自己的 runid 和master 的 runid 进行比较,如果两者的 runid 一致,说明现在的 master 是 slave 断线重连前的 master,则可以进行部分复制。如果 runid 不一致,说明 slave 断线之前连接的 master 和现在的 master 不是一个,那应该进行全量复制。
复制挤压缓冲区
master 维护了一个复制积压缓冲区,这个复制积压缓冲区是一个先进先出的队列。master 在进行命令传播的时候,会把新的写操作写入这个复制挤压缓冲区。旧的写操作会在对头弹出。当 slave 要求进行部分复制的时候,如果需要复制的内容存在于复制积压缓冲区,那么 master 会进行部分复制,如果要复制的内容已经不在复制积压缓冲区,或者已经不完整,那么 master 会进行全量复制。
部分复制过程:
如果 master 返回 -err,说明 master 版本是2.8之前,无法识别 psync 命令。
哨兵每一秒都会向 master、slaves、其他哨兵发送 ping 命令。
如果节点回复时间超过设置的时间,则会被标记为主观下线。
如果 master 被标记为主观下线。那么其他的哨兵都会给 master 发送 ping。
如果多数哨兵都认为 master 主观下线,那么 master 会被标记为客观下线。
哨兵就要重新选举新的 master。新的 master 产生后,所有 slave 的配置文件进行修改,都变成新 master 的slave。
# 创建连接,可以用PING-PONG来检查连接是否OK redis-cli -h localhost -p 6379 # 监控Redis的连接和读写操作 redis-cli -h localhost -p 6379 monitor # Redis服务器的统计信息 redis-cli -h localhost -p 6379 info # 查找 bigkey redis-cli -h localhost -p 6379 --bigkeys # 阅读 redis 提供的 slowlog(慢日志),慢日志会记录运行较慢的指令。
使用了时间复杂度较高的命令,比如 SORT 之类的。
redis 返回数据时,有网络问题。
value 太大导致的 ”bigkey“ 问题,这样 redis 会花费很多时间在分配内存和删除内存上。
redis-cli -h localhost -p 6379 --bigkeys
来查找 bigkey
数据集中过期。
数据集中过期,会导致 redis 大量执行删除过期 key 的操作,影响速度。
内存不够了,所以每次写入内存时,redis 要先进行内存的清理。
在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒:这是进行持久化操作时,fork 一个子进程的花费时间。
AOF 的刷盘机制选取问题:
appendfsync always
:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高
appendfsync no
:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机
appendfsync everysec
:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据
redis 内存碎片太多。执行 INFO 命令,查看内存情况。
避免使用太多时间复杂度较高的命令。有些操作可以在客户端完成后,再写入 redis。
避免 "bigkey"。
给 key 设置随机过期时间。
开启 lazy-free
(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。
合理配置数据持久化策略。
降低主从库全量同步的概率。
选择合适的 AOF 的刷盘机制:一般来说,appendfsync everysec
比较合适。
适当地进行 redis 内存碎片整理。(内存整理也会影响性能,要评估后进行)
优化网络情况。
Redis Cluster 包含了 16384 个哈希槽,每个 Key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端。
主节点只有一个,主节点写,从节点读
redis cluster是Redis的分布式解决方案,在3.0版本推出后有效地解决了redis分布式方面的需求
自动将数据进行分片,每个master上放一部分数据
提供内置的高可用支持,部分master不可用时,还是可以继续工作的
数据分布算法
hash算法
比如你有 N 个 redis实例,那么如何将一个key映射到redis上呢,你很可能会采用类似下面的通用方法计算 key的 hash 值,然后均匀的映射到到 N 个 redis上:
hash(key)%N
如果增加一个redis,映射公式变成了 hash(key)%(N+1)
如果一个redis宕机了,映射公式变成了 hash(key)%(N-1)
在这两种情况下,几乎所有的缓存都失效了。会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。
一致性hash算法
一个master宕机不会导致大部分缓存失效,可能存在缓存热点问题
用虚拟节点改进
redis cluster 的 hash slot 算法
redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot
redis cluster 中每个master都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot
hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他master 上去
移动hash slot的成本是非常低的
客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现
127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 可以将槽0-5000指派给节点7000负责。
每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。
客户端向节点发送键命令,节点要计算这个键属于哪个槽。
如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。
Cache Aside Pattern(旁路缓存模式)
Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。
如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:
缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。