Basic
Concept
Config
Usage
多级缓存中,级别数字越小的越会被优先读取,数字大的是数字小的的兜底
- Cpu多级缓存
- Spring三级缓存
- Mybatis二级缓存
ps:还有程序的本地缓存优先于redis的集中式缓存
Key
key命名规范: set [namespace:]table:id:field value
ps:namespace可选,一般为appId
Type
String
List
Set
SortedSet
Hash
Geo
Bitmap
Bitmap:按位存储元素(又叫BitSet),不存储元素本身,比Set省空间
ps:Bitmap中数据如果稀疏的话,会比Set更浪费空间
ps:Bitmap支持的是不重复的数值,如果是字符串需要自己hash(因为会存在hash冲突所以会导致误判断)
BloomFilter
BloomFilter:底层是Bitmap,通过多个hash函数设置多个bit位,可以解决Bitmap稀疏数据造成的空间浪费问题
ps:BloomFilter解决了Bitmap稀疏造成的空间浪费问题,但引入了误判的问题
ps:BloomFilter由于不同的数据可能落在相同的位置,导致了删除困难的问题
BloomFilter的操作
- 可能在过滤器中:可能会误判(是不是真的在还需要去原始数据里面去进一步检查)
- 肯定不在过滤器中:肯定不会误判
BloomFilter的场景
- 可能在过滤器中:可能会误判(是不是真的在还需要去原始数据里面去进一步检查)
- 在白名单中:通过
- 在黑名单中:拒绝
- 垃圾邮件过滤:如果在黑名单里就放入垃圾箱,会误判,可能导致正常邮件被放到了垃圾箱
- 非法攻击过滤:如果在黑名单里就不允许访问,会误判,可能导致正常请求被错误的拒绝了
- 爬虫地址去重:如果在已抓取名单里就不抓取,会误判,可能导致未抓取过的链接不能被抓取
- 新闻推荐去重:如果在已推荐名单里就不推荐,会误判,可能导致未推送过的新闻不能被推送
- 肯定不在过滤器中:肯定不会误判
- 不在白名单中:拒绝
- 缓存穿透解决:如果不在数据集合里就拒绝掉,不会误判
- 不在黑名单中:通过
- 不在白名单中:拒绝
ps:爬虫如果想抓所有链接,还需要进一步去数据库里面检查(布隆过滤器则起到了减少数据库访问的作用)
ps:HBase\RocksDB\LevelDB等数据库内置布隆过滤器,可以提前过滤掉无效请求(key不存在的请求)
HyperLogLog
HyperLogLog:进行基数统计,存在误差,比Set省空间
Expire
- 惰性删除
- 定期删除
ps:字符串的
SET
和SETEX
也可以设置过期时间,过期的key会放在过期表里面
Eliminate
- noeviction:不淘汰,内存不够了就报错
- volatile-ttl:从设置了过期时间的key里面选择最先过期的进行淘汰
- volatile-random:从设置了过期时间的key里面随机选择进行淘汰
- volatile-lru:从设置了过期时间的key里面使用lru算法进行淘汰
- volatile-lfu:从设置了过期时间的key里面使用lfu算法进行淘汰
- allkeys-random:从所有的key里面随机选择进行淘汰
- allkeys-lru:从所有的key里面使用lru算法进行淘汰
- allkeys-lfu:从所有的key里面使用lfu算法进行淘汰
FIFO(First In First Out):先进先出算法,基于位置,淘汰最前面的数据,可以使用队列实现
LRU(Least Recently Used):最久未使用算法,基于时间,淘汰闲置时间最长的数据,可以使用链表实现
LFU(Least Frequently Used):最不常使用算法,基于频率,淘汰使用频率最低的数据,可以使用小顶堆实现
ps:LRU保留的是新访数据,LFU保留的是热点数据
ps:java中LRU可以用LinkedHashMap,LFU可以用PriorityQueue
redis中lru的近似性:采样选择一部分key而不是所有key,按照lru算法进行淘汰
redis中lfu的近似性:记录的不是真实频率,而是数量级
Update
- 旁路缓存(Cache Aside Pattern):数据写到数据库并删除缓存,业务模块负责
稍后同步
数据到缓存 - 穿透写入(Write Through Pattern):数据写到缓存,缓存模块负责
立即同步
数据到数据库 - 异步回写(Write Back Pattern):数据写到缓存,缓存模块负责
延后同步
数据到数据库
ps:旁路缓存会先删除缓存,等到下次读取缓存时发现不存在就会从数据库同步到缓存中
ps:写入操作会先更新缓存,然后将数据立即或者稍后同步到数据库
ps:穿透写入类似于cpu缓存的写通策略,异步回写类似于cpu缓存的写回策略
- 旁路缓存是以数据为主,缓存为辅的策略,追求数据的完整
- 穿透写入是以缓存为主,数据为辅的策略,追求系统的性能
- 异步回写是以缓存为主,数据为辅的策略,追求系统的性能
ps:
穿透写入
适合写少
的场景,异步回写
适合写多
的场景
Operation
Command
Batch
- 批量命令:MGET、MSET、HMSET、HMGET、MGETNX、MSETNX、HMGETNX、HMSETNX
- 管道(pipeline)
- 事务(transaction)
- 脚本(script)
Iterate
- KEYS
- SCAN、SSCAN、ZCAN、HSCAN
- LRANGE、SMEMBERS、ZRANGE、HGETALL
Monitor
Scene
- Normal
- 缓存
- 分布式锁
- Number
- 计数器
- 限流器
- 发号器(生成全局序列号)
- List
- 时间轴
- 消息列表
- Set
- 随机抽奖
- 点赞信息
- 签到信息
- 打卡信息
- 商品标签
- 社交关系
- SortedSet
- 热搜榜(TopK)
- 排行榜(TopK)
- 延时队列
- Hash
- 购物车
社交关系
存储设计
- A:idol:A关注的所有用户(follow)
- A:fans:关注A的所有用户(follower)
关系运算
- 跟A互相关注的所有用户:A:idol和A:fans求交集
- A和B共同关注的所有用户:A:idol和B:idol求交集
- A关注的人也关注了B:A:idol和B:fans求交集
Structure
- redis数据结构入门
- redis数据结构进阶
- redis数据结构详解
- redis数据编码入门
- redis数据编码详解
- redis数据结构之redisDb入门
- redis数据结构之redisDb详解
- redis数据结构之redisObject入门
- redis数据结构之redisObject详解
- redis数据结构之dict入门
- redis数据结构之dict详解
- redis数据结构之dict之渐进式rehash扩容入门
- redis数据结构之dict之渐进式rehash扩容进阶
- redis数据结构之dict之渐进式rehash扩容详解
- redis数据结构之sds入门
- redis数据结构之sds详解
- redis数据编码之embstr和raw的区别
- redis数据编码之embstr和raw的分界点时如何得出的
- redis数据编码之quicklist和ziplist的区别
- redis数据编码之intset和ziplist的区别
- redis数据编码之set为什么不使用ziplist编码
简介版
- String:intstr、embstr、rawstr
- List:ziplist、linkedlist、quicklist
- Set:intset、hashtable
- SortedSet:ziplist、skiplist
- Hash:ziplist、hashtable
ps:embstr和rawstr的区别是embstr和redisObject的空间是连续在一起的
ps:quicklist是ziplist和linkedlist的合体,将多个ziplist用linkedlist链接起来
- ziplist:压缩列表
- linkedlist:双向链表
- quicklist:快速列表
- intset:整型集合
- skiplist:跳跃表
- hashtable:哈希表
详细版
- String:对应java的String
- Structure:sds
- Encoding
- REDIS_ENCODING_INT:当数据是整数时
- REDIS_ENCODING_EMBSTR:当数据是字符串且长度小于44(3.0之前是39)个字节时
- REDIS_ENCODING_RAW:当数据是字符串且长度大于44(3.0之前是39)个字节时
- List:对应java的List
- Structure:ziplist、linkedlist、quicklist
- Encoding
- REDIS_ENCODING_ZIPLIST:redis3.2之前,默认的编码
- REDIS_ENCODING_LINKEDLIST:redis3.2之前,当元素大小大于配置的list-max-ziplist-value(默认64)或元素个数大于配置的list-max-ziplist-entries(默认512)时使用此编码
- REDIS_ENCODING_QUICKLIST:redis3.2之后使用此编码
- Set:对应java的Set
- Structure:intset、hashtable
- Encoding
- REDIS_ENCODING_INTSET:当元素类型是整数时
- REDIS_ENCODING_HT:当元素类型不是整数时或元素个数大于配置的set-max-intset-entries(默认512)时使用此编码
- SortedSet:对应java的SortedSet
- Structure:ziplist、skiplist
- Encoding:
- REDIS_ENCODING_ZIPLIST:默认的编码
- REDIS_ENCODING_SKIPLIST:当元素大小大于配置的zset-max-ziplist-value(默认64)或元素个数大于配置的zset-max-ziplist-entries(默认128)时使用此编码
- Hash:对应java的Map
- Structure:ziplist、hashtable
- Encoding
- REDIS_ENCODING_ZIPLIST:默认的编码
- REDIS_ENCODING_HT:当元素大小大于配置的hash-max-ziplist-value(默认64)或元素个数大于配置的hash-max-ziplist-entries(默认512)时使用此编码
Transaction
- 事务:执行多条命令,可以提交和回滚
- 管道:客户端批量发送,服务端按序执行单条命令(异常后不会回滚)后,服务端批量返回结果
ps:管道在服务端时通过队列来实现的
Atomicity
redis中单个操作是原子性的,复合操作不是原子性的(因为某个命令执行出错不会影响下一个命令的执行)
ps:redis事务中的命令如果有语法错误,事务会被取消,事务里的所有命令都不会执行
Isolation
redis执行命令是单线程的,不存在并发,所以不需要隔离性
Durability
- RDB快照:类似与mysql的dump文件
- AOF日志:类似于mysql的binlog文件
- RDB快照和AOF日志混合:AOF日志只记录上次RDB快照之后的变化,这次生成快照后会清空AOF日志
ps:非混合时启动时先考虑加载AOF文件,AOF文件不存在则加载RDB文件,因为AOF文件保存的数据比RDB文件更完整
ps:混合时启动会加载RDB文件,再加载AOF文件
RDB
- save:会阻塞其他操作直到RDB操作完成
- bgsave:fork出子进程在后台进行RDB操作
AOF
- Always:命令执行完立即将AOF日志同步写入磁盘
- Everysec:每隔一秒将AOF缓冲区中的内容写入磁盘
- No:由操作系统决定何时将AOF缓冲区中的内容写入磁盘
ps:appendfsync的默认值为Everysec
RDB和AOF混合
Problem
并发操作
- 读问题
- 并发读:不存在问题
- 写后读:不一致性(脏读、不可重复读、幻读)
- 写问题
- 并发写:插入冲突、更新丢失
- 读后写:写入偏差
- 死锁
数据丢失
- 持久化时appendfsync设置为Everysec或No会丢数据(未刷盘的数据在宕机的时候会丢数据)
- 主从切换时主从同步不完整时会丢数据
- 内存不足进行淘汰时会丢失
- 程序有bug误操作删除数据
- 人为误操作删除数据
缓存问题
缓存失效
- 缓存穿透:不存在的key(布隆过滤器、缓存空值、接口校验、IP黑名单)
- 缓存击穿:单个热点key失效了(热点key永不过期、监控热点key并延长过期时间、读数据库时加锁排队)
- 缓存雪崩:大量key同时失效了(过期时间随机、使用二级缓存、读数据库时加锁排队)
ps:二级缓存是指
一级本地进程缓存 + 二级redis缓存
,一级缓存的过期时间是随机的,二级缓存的过期时间比一级缓存的过期时间长
缓存一致性
缓存一致性方案
- 写缓存:写多时前面写入的缓存会被后面的覆盖从而浪费cpu资源,而且高并发写入会导致更新丢失的问题
- 先写数据库再写缓存
- 先写缓存再写数据库
- 删缓存:使用删除和懒加载的方式效率更高,而且删除比更新更快速和安全
- 先写数据库再删缓存
- 先删缓存再写数据库
ps:关键字:写浪费,写丢失,删除更快速和安全
综上所述应该选择 删缓存
的方案
主要的操作如下
写数据库 + 删缓存
或删缓存 + 写数据库
读数据库 + 写缓存
ps:读缓存读不到时(缓存已
删除
或缓存已失效
)会读数据库
和写缓存
缓存不一致的形成条件为
- B读数据库要在A写数据库之前(读脏数据)
- B写缓存要在A删缓存之后(写脏数据)
方案1:先写数据库再删缓存
- 场景1:写数据库成功,删缓存失败(会出现缓存不一致的情况,需要重试删除缓存操作)
- 场景2:B读数据库(缓存已失效),A写数据库,A删缓存,B写缓存
ps:A写数据库要在A删缓存之前(方案要求)
ps:B读数据库要在A写数据库之前(读脏数据)
ps:B写缓存要在A删缓存之后(写脏数据)
方案2:先删缓存再写数据库
- 场景3:删缓存成功,写数据库成功(不会出现缓存不一致的情况,缓存后面会被重新加载)
- 场景4:B读数据库(缓存已失效),A删缓存,A写数据库,B写缓存
- 场景5:B读数据库(缓存已失效),A删缓存,B写缓存,A写数据库
- 场景6:A删缓存,B读数据库(缓存已删除),A写数据库,B写缓存
- 场景7:A删缓存,B读数据库(缓存已删除),B写缓存,A写数据库
ps:A删缓存要在A写数据库之前(方案要求)
ps:B读数据库要在A写数据库之前(读脏数据)
ps:B写缓存要在A删缓存之后(写脏数据)
对比发现 先写数据库再删缓存
这种方案更优
- 因为方案1出现缓存一致性的场景比方案2更少
- 因为缓存已失效比缓存已删除低的出现概率低
- 因为B读数据库之后B写缓存出现在A写数据库之后的概率低
ps:一般来说B读数据库之后B写缓存的速度比A写数据库快
所以最终的方案是 先写数据库再删缓存
,再配合 延时双删
可以更好的解决缓存不一致的问题
Performance
- optimize(查询和性能优化)
- explain(查询分析工具)
- profile(性能分析工具)
- slowlog(慢查询日志)
- monitor(性能监控工具)
Optimize
查询优化
- 避免使用很大的key和value
- 避免使用耗时长的命令
性能优化
- 为key设置过期时间来减少内存占用
- 避免大量的key同时过期
- 尽量使用批量操作
- 启用延迟删除的特性
- 启用自动整理碎片的功能
操作优化
- 多次的操作优化为批量处理
- 长时间操作优化为分批处理
Analysis
Diagnosis
SlowLog
Monitor
Question
为什么要使用redis
- 高性能:redis的性能很高,可以用作缓存加快访问速度
- 高并发:redis的并发很高,可以支持更高的并发请求
ps:系统三高指标,高性能,高并发,高可用
redis为什么这么快
数据读写
部分基于内存访问,速度快数据结构
部分基于优化后的数据结构,效率高命令处理
部分基于单线程来避免上下文切换,效率高网络请求
部分基于多路复用,性能高
ps:redis是IO密集型,因此cpu不是瓶颈而网络IO是瓶颈,所以命令处理部分使用单线程更好
redis为什么QPS这么高
QPS为每秒处理的请求数和并发数的乘积,操作的速度越快,每秒处理的请求数越高
- 内存操作:redis是基于内存的,操作速度快
- 高效的数据结构:高效的数据结构,使得操作速度更快
- 单线程和事件机制:cpu不是瓶颈,使用单线程和事件机制避免了加锁和线程切换,使得操作速度更快
- IO多路复用机制:使用了IO多路复用机制,支持的并发数很高
ps:redis的QPS是10w+(50000~300000)级别的,mysql的QPS是1k+(5000左右)级别的
redis6.0之前真的是单线程吗
网络请求部分和命令处理部分是单线程,还存在其他线程
- close_file:负责关闭资源文件(日志文件和网络套接字)
- aof_fsync:负责对aof文件刷盘
- lazy_free:负责释放大对象空间
redis6.0之前为什么不使用多线程
- 单线程实现比较简单
- 多线程不是迫切需求
- 因为内存访问很快,所以并发和吞吐量也能满足早期的需求
- 单线程无需加锁和无需线程切换的特性使得单线程的性能比多线程更高
- 早期的主要操作是IO读写,cpu不是瓶颈,IO读写在单线程中可以通过多路复用来解决
redis6.0之后为什么要引入多线程
- redis6.0之后的网络IO部分是多线程的,命令执行还是单线程的
- 使用多线程是为了提高网络IO部分的性能,支持更高的并发量
redis键名长度会影响性能吗
键名太长的话会导致以下问题从而影响性能
- 内存占用更多
- 读写更加耗时
- 传输更加耗时
redis大key(BigKey)问题如何优化
BigKey的危害
- 操作时间长,阻塞其他请求,性能变差
BigKey的发现
- redis的bigkeys选项
BigKey的解决
- 使用二级缓存(本地缓存)
- 拆分成多个key进行读写
redis热key(HotKey)问题如何优化
HotKey的危害
- 请求集中在某个节点上,导致节点的压力大
HotKey的发现
- 根据业务特点预判
- 客户端收集访问信息
- 代理层收集访问信息
- redis的hotkeys选项
- redis的monitor监控
- redis的网络数据抓包
HotKey的解决
- 使用二级缓存(本地缓存)
- 分布式时增加副本数量来分摊读取压力
Architecture
Replication
主从复制
主从复制过程
建立连接阶段
- 读取主节点地址信息:读取配置文件中slaveof中配置的ip和port
- 建立Socket连接:根据slaveof中配置的ip和port与master建立连接
- 发送PING命令:收到PONG响应后说明连接正常
- 验证用户身份:如果配置了masterauth就会请求验证slave的身份
- 发送从节点地址信息:slave发送自己的ip和port
数据同步阶段
- 全量同步:发送
PSYNC ? -1
命令 - 增量同步:发送
PSYNC <runid> <offset>
命令
ps:runid为主服务器的ID
主从复制模式
- 全量复制:第一次连接时
- 增量复制:非第一次连接时
主从延迟
TODO:redis主从延迟
Cluster
HA(High Availability):高可用
主从模式
哨兵模式
Distributed
分布式数据核心对象
- 分片(shard):负责读写
- 副本(replica):负责备用
HA(High Availability):高可用
ps:分布式是系统一般自带高可用机制
redis扩容和缩容时迁移hash槽是同步的,会阻塞hash槽上的其他操作
redis请求时节点上不存在对应的hash槽时,会返回重定向让客户端重新请求
数据分片(Sharding)
redis为什么使用的是hash槽而不是一致性hash
- 一致性hash不灵活
- 一致性hash无法灵活的手动设置数据分布,比如有些硬件差的节点希望少存一点数据
- hash槽可以灵活的手动设置数据分布,hash槽能够配置每个节点使用的哈希槽范围
- 一致性hash更复杂
- 相对于hash槽,一致性hash更复杂
数据分区(Partition)
Query
读写分离
冷热分离
TODO:redis冷热分离
Backup
Application
Lock
redis实现分布式锁需要考虑的问题
- 锁的特性
- 锁需要超时释放:程序出错或者挂了不释放锁,会导致死锁,需要支持锁超时释放
- 锁不能提前释放:任务还没完成就释放了锁,会导致锁被重复获取,需要支持锁超时刷新
- 锁的可重入性:同一个使用者可以重复获取锁
- 锁的安全释放:锁只能被持有者释放
- 锁的安全
- 加锁和超时的原子性:
加锁
和设置超时
需要保证原子性 - 检查和加锁的原子性:检查加锁的操作者是否是持有者并允许
重入
需要保证原子性 - 检查和解锁的原子性:检查解锁的操作者是否是持有者并允许
删除
需要保证原子性
- 加锁和超时的原子性:
redis看门狗超时刷新原理:有一个后台线程定期(周期小于过期时间和延长时间)去延长锁的过期时间
ps:如果key不存在了,说明是主动释放了锁,这时候就不需要延长锁的过期时间
setnx
- 不支持锁超时释放
- 不支持锁的可重入性
- 不支持锁的安全释放
加锁
SETNX lock_key 1
解锁
DEL lock_key
setnx + expire
- 锁可能会提前释放
- 不支持锁的可重入性
- 不支持锁的安全释放
- 加锁和超时不具备原子性
加锁
SETNX lock_key 1
+ EXPIRE lock_key 10
解锁
DEL lock_key
set + ex + nx
- 锁可能会提前释放
- 不支持锁的可重入性
- 不支持锁的安全释放
加锁
SET lock_key 1 EX 10 NX
解锁
DEL lock_key
check + set(命令版)
- 锁可能会提前释放
- 检查和加锁不具备原子性
- 检查和解锁不具备原子性
加锁
1 | if (redis.get(lock_key) == unique_value) { |
解锁
1 | if (redis.get(lock_key) == unique_value) { |
check + set(脚本版)
- 锁可能会提前释放
加锁
1 | if redis.call("get", KEYS[1]) == ARGV[1] then |
解锁
1 | if redis.call("get", KEYS[1]) == ARGV[1] then |
check + set(看门狗)
脚本版 + 看门狗 就可以实现redis单机版的分布式锁了,redis的集群版分布式锁要用红锁
Counter
基于 incr count
实现
ps:发号器也可以通过计数器来实现
Limiter
- 计数
- 固定窗口计数:无法应对流量集中在窗口边界两侧的突发大流量
- 滑动窗口计数:可以应对流量集中在窗口边界两侧的突发大流量
- 漏桶:流量漏出速度恒定,多余的流量会被丢弃,无法应对突发大流量
- 令牌:令牌放入速度恒定,但取令牌的速度不限,可以应对突发大流量
PriorityQueue
优先级队列:SortedSet
阻塞式优先级队列:多个List + LPUSH + BRPOP
DelayQueue
SortedSet + 轮询
Publish-Subscribe
Producer-Consumer
redis为什么不适合做消息队列
- 持久化机制不完善
- 没有确认机制和重试机制
- 不支持多路消费
Theory
- 跳表插入和删除效率比红黑树高
- 红黑树的平衡操作会引起子树的操作,比较耗时
- 跳表只需要维护相邻节点,耗时较少
- 跳表的范围查询效率比红黑树高
- 红黑树通过二分法找到最小值后还需要中序遍历整棵树找到范围内的值,比较耗时
- 跳表通过二分法找到最小值通过向后遍历就能找到范围内的值,耗时较少
Other
Management
管理
Visual
Tools
Operation
运维
TODO:redis不停机迁移