基本操作
string
- set key val
- get key
- incr key
- decr key
- incrby key decrement
- setnx key value
- del key
- set key value ex 60
- set key value px 60000
- incrbyfloat key 2.1
- setbit key offset value
- getbit key offset
- bitcount key
- append key value
- getrange key 0 100
- setrange key 2 bb,字符串改写,覆盖从offset开始value长度的字符串
说明
- int小于20且能转成整数
- embstr长度小于等于44
- raw长度大于44
list
- lpush key value value ...
- lpop key
- rpush key value value
- rpop key
- lrange key start end,end-1代表列表剩余所有数据
- lrem key count value,删除count次value(负数则从右侧扫描
- ltrim key start end,只保留区间内
- brpop key timeout
- lpushx,不存在列表则无法新增
- lindex key index,获取列表指定位置的元素
场景:
- 栈 LPUSH + LPOP
- 队列LPUSH + RPOP
- 阻塞队列LPUSH + BRPOP +timeout
底层:ziplist, quicklist
set
- SADD key member
- SREM key member
- SCARD key
- SISMEMBER key member
- SMEMBERS key
- SINTER key1 key2
- srandmember key count
- spop key count
- sdiff key1 key2
- sunion key1 key2
- smove key1 key2 value,移动
- sdiffstore destkey key1 key2
场景:1.抽奖;2.共同关注;3.推荐好友
- intset元素都为整数且节点数量小于等于512
- dict元素有一个不为整数或数量大于512
hash
- hget key field
- hgetall
- hkeys
- hvals
- hset key field value
- hmset key field1 value1 field2 value2
- hincrby key field increment
- hlen key
- hexists key field
- hdel key field
场景:购物车数据将用户id作为 key,商品id作为 field,商品数量作为 value
- ziplist节点少于512且字符串长度小于等于64
- dict节点数量大于512或字符串长度>64
zset
- ZADD key [NX|XX] [CH] [INCR] score member [score member …]
- CH:修改返回值为发生变化的成员总数
- zrem key member1 member2
- zscore key member
- zincrby key increment member
- zcard key
- zrank key member
- zrevrank key member
- zrange key start stop,获取排名范围内的元素
- zrangebyscore key strat stop
- zrevrange key start stop
- zcount key from to(分数范围元素数量,闭合区间)
- zremrangebyscore key startScore endScore,删除分数范围内的元素
- zremrangebyrank key startIndex endIndex,删除排名区间的元素
点击新闻:
- zincrby hot:20210203 1 10001
- zincrby hot:20210203 1 10002
- zrevrange hot:20210203 0 9 withscores
- 延迟队列
- 时间窗口限流
- ziplist子节点数量小于等于128且字符串长度小于64
- skiplist数量大于128或有一个字符串长度>64
bitmap
- setbit key offset value
- getbit key offset
- BITCOUNT peter
签到、日活、在线状态
redisObject 占用16个字节; sdshdr8 占用 3+x+1 个字节(后面加1是因为 char buf[] 要预留一个 \0 );
redis 内存分配器认为 大于 64个字节为大字符串;所以留给小字符串的大小为 64 - 16 - 3 - 1 = 44 ;
参考:
zhuanlan.zhihu.com/p/577241076
微博粉丝设计
| 功能 | redis数据结构 | 代码 | 说明 |
|---|---|---|---|
| 用户个人数据 | hash | HSET 'user:1' 'name' 'Jack'HSET 'user:1' 'sex' 'male'HSET 'user:1' 'follow' '10'HSET 'user:1' 'fans' '100' | |
| 粉丝排行榜 | zset | ZADD 'fans' 100 '1'ZADD 'fans' 50 '2'ZADD 'fans' 20 '3' | 分别代表1、2、3用户的粉丝量 |
| 计算粉丝排名 | ZREVRANK 'fans' '1' | ||
| 实现关注、被关注 | set | SADD 'user:1:follow' '2'SADD 'user:2:fans' '1' | |
| 计算共同关注 | SINTER 'user:1:follow' 'user:2:follow' | ||
| 计算粉丝增长 | INCRBY 'user:1:fans' 1 | ||
| 添加关注取关 | 用户1关注列表用户2粉丝列表 | ||
| 查看粉丝列表 | SMEMBERS 2:fans | ||
| 我的关注数量 | SCARD 1:follow | ||
| 实现按关注时间排序 | ZADD 1:follow 1457871625 2 | ||
Feed流
- 推方式
- 拉方式
- 推拉结合方式
快的原因
- 内存操作
- 单线程执行命令
- 数据结构高效
- IO多路复用
多线程
多线程处理数据的读写和协议解析
持久化
-
RDB
- 触发时机:手动、自动
- 内容格式:数据快照、压缩二进制文件
-
AOF
- 内容格式:写命令列表
- 同步策略:每次命令、每秒、跟随系统
IO多路复用
- select,poll:不仅多了拷贝FD的操作,还需要遍历
- epoll:事件来了之后,触发相应动作
主从复制
-
多个数据节点,主节点写,从节点读
-
数据单向从主节点复制
-
作用:
- 数据冗余
- 故障恢复
- 负载均衡
-
拓扑结构:
- 一主一从
- 一主多从
- 树状
-
流程
- 从节点感知到主节点、保存主节点信息
- 建立socket连接
- 发送ping
- 权限认证
- 同步数据集
- 命令持续复制
-
主从同步方式
- 全量同步
- 增量同步
哨兵模式
作用:
- 监控
- 故障转移
- 配置提供
- 通知故障转移结果
实现原理:
-
定时任务监听
- 10s/次info命令主从节点获取拓扑结构
- 2s主从节点哨兵频道发送自己对主节点的判断以及当前哨兵节点信息
- 1s,主从节点、哨兵节点,ping命令心跳检测
-
主观下线、客观下线
-
故障转移
- 哨兵raft选举领导者
- 故障转移
数据分片
缓存一致性
先更数据,再删缓存:更新数据时间长,先删缓存、缓存中key不存在的时间更长、造成脏数据的概率更大。
缓存不一致的可能原因:
- 缓存key删除失败:把删除失败的消息放到mq里进行重试
- 并发导致写入了脏数据
解决脏数据的方式:
- 延迟双删,第一次删除缓存之后,更新了数据再次删除缓存
- 设置缓存过期时间兜底
如何保证本地缓存和分布式缓存的一致
日常开发中,常常使用两级缓存:本地缓存+分布式缓存。
本地缓存与机器强绑定,可以采用消息订阅的方式删除本地缓存、本地缓存时间设置相对短过期时间
热key处理
短时间内被频繁访问的key.
判定方式:
- QPS集中在特定key
- 带宽使用率集中在特定key:HGETALL
- CPU使用率集中在特定key:zrange key from to
处理方式:
- 监控
- 打散到不同服务器(加上前缀、后缀
- 加入二级缓存
缓存预热方式
- 定时任务刷新缓存
- 写个缓存刷新页面/接口
热key重建?问题?
热点数据过期时,会有很多线程请求来重建数据,需要保证安全性、减少重建次数、数据尽量一致
- 互斥锁:只允许一个线程进行重建,其他线程等待重建获取缓存数据
- 较长的过期时间:使用单独的线程去构建过期的缓存
无底洞问题
添加节点做水平扩容,导致键值分布到更多的节点上,分布式批量操作设计多次网络时间。
解决方式:
- 优化命令
- 减少网络通信次数
- 降低接入成本,客户端使用长链接/连接池等
Redis内存不足
- 修改配置,增加可用内存
- 通过命令动态设置内存上限
- 修改内存淘汰策略,及时释放内存空间
- 使用集群,横向扩容
过期数据收集策略
- 惰性删除
- 定期删除
- 立即删除
内存溢出控制/内存淘汰策略
针对对象:
- volatile
- allkey
策略:
- LRU
- random
Redis阻塞
-
API或数据结构使用不合理
- 慢查询
- 大对象
-
CPU饱和
- 并发极限
- 命令/内存
-
持久化阻塞
- fork
- AOF刷盘
- hugepage写操作
大key
- 单个key存的value超过10KB
- hash,set,zset,list存储过多元素(万以上
问题:
- 耗时增加
- 占用带宽和CPU
- 造成集群中资源倾斜
- 主动删除、被动删除,可能阻塞
定位方式:
- redis-cli --bigkeys
- rdb分析工具
处理方式:
-
可删除
-
不可删除
- 压缩(序列化反序列化有性能要求
- 拆分
常见性能问题和解决方案
- master最好不要做持久化工作,包括内存快照和AOF日志文件,特别是不要启用内存快照做持久化
- 关键数据,某个slave开启AOF 备份数据,每秒一次
- 为了主从复制速度和连接稳定性,主从都在一个局域网内
- 尽量避免在压力较大的主库上增加从库
- Master 调用 BGREWRITEAOF 重写 AOF 文件,AOF 在重写的时候会占大量的 CPU 和内存资源,导致服务 load 过高,出现短暂服务暂停现象。
- 为了 Master 的稳定性,主从复制不要用图状结构,用单向链表结构更稳定,即主从关为:Master<–Slave1<–Slave2<–Slave3…,这样的结构也方便解决单点故障问题,实现 Slave 对 Master 的替换,也即,如果 Master 挂了,可以立马启用 Slave1 做 Master,其他不变。
使用redis实现异步队列
发布订阅模式
实现延迟队列
zset, 时间戳作为score,可以获取对应区间的数据
事务
不支持回滚
支持批量执行命令,实际上是线性执行的,一个失败也不会影响其他执行,除非命令本身有错误
批量命令存在一个事务队列中,收到exec后,开始执行
LUA脚本
利用lua扣减redis库存
Redis管道
打包命令发给服务端的方式
- 管道:减少多次请求的总时间,减少上下文切换
分布式锁
setnx key value ex 50
底层数据结构
内存优化
- 设计合理内存回收策略
- 删除过期键值
- 对大小超过阈值的value进行压缩,LZF算法
- 控制key的数量
- 优化字段大小
- 使用集群
Redis是单线程的,提高CPU利用率?
单台机器上部署多个Redis实例
epoll和reactor
epoll常用来实现reactor,reactor是一种处理IO并发的设计模式,epoll是IO多路复用的机制
Redis分区缺点
- 涉及多个key的操作通常不会被支持。例如你不能对两个集合求交集,因为他们可能被存储到不同的Redis实例(实际上这种情况也有办法,但是不能直接使用交集指令)。同时操作多个key,则不能使用Redis事务.
- 分区使用的粒度是key,不能使用一个非常长的排序key存储一个数据集
- 当使用分区的时候,数据处理会非常复杂,例如为了备份你必须从不同的Redis实例和主机同时收集RDB / AOF文件。
- 分区时动态扩容或缩容可能非常复杂。Redis集群在运行时增加或者删除Redis节点,能做到最大程度对用户透明地数据再平衡,但其他一些客户端分区或者代理分区方法则不支持这种特性。然而,有一种预分片的技术也可以较好的解决这个问题。
Redis 的并发竞争 Key 问题
多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
解决方法:分布式锁
分布式Redis最好前期就做
前期做,后期数据增长时只需要迁移实例,不需要重新对数据分片
永久有效设置
很大的超时时间,会消耗内存
主从服务器删除过期键处理
RDB:主服务器不会加载过期键;从服务器加载所有,同步时会清理所有;
AOF:不受影响,追加写,如果被删除,则加入一条删除记录
复制:主服务器删除过期键后,向从服务器发送DEL命令,此前从服务器收到get命令正常返回,收到DEL之后才会删除
AOF重写:过期键不会保存
限流算法
令牌桶可以在运行时控制和调整数据处理的速率,处理突发流量。放令牌的频率增加可以提升整体数据处理的速度,而通过每次获取令牌的个数增加或者放慢令牌的发放速度和降低整体数据处理速度。
而漏桶不行,因为它的流出速率是固定的,程序处理速度也是固定的。整体而言,令牌桶算法更优,但是实现更为复杂一些。
缓存降级
缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
Redis主从架构数据丢失
- 异步复制:复制过程中、主节点断开链接,数据没有到从节点
- 集群脑裂:master断开链接、实际上能正常使用,哨兵重新选出主节点,等master连接后、会成为新主节点的从节点,清除这段时间的数据。
解决方式:
min-slave-to-write:主节点至少要同步给指定数量的从节点成功后,才能写入成功
min-slave-max-lag:一旦slave复制数据和ack延迟时间过长,master拒绝写入
脑裂
- 哨兵脑裂
- 集群脑裂
关键的配置项分别是 min-slaves-to-write(最小从服务器数) 和 min-slaves-max-lag(从连接的最大延迟时间)。
min-slaves-to-write 是指主库最少得有 N 个健康的从库存活才能执行写命令。
这个配置虽然不能保证 N 个从库都一定能接收到主库的写操作,但是能避免当没有足够健康的从库时,主库无法正常写入,以此来避免数据的丢失 ,如果设置为 0 则表示关闭该功能。
min-slaves-max-lag :是指从库和主库进行数据复制时的 ACK 消息延迟的最大时间;
可以确保从库在指定的时间内,如果 ACK 时间没在规定时间内,则拒绝写入。
执行切换的那个哨兵在完成故障转移后会做什么
会进行configuraiton配置信息传播。
哨兵完成切换之后,会在自己本地更新生成最新的master配置,然后通过pub/sub消息机制同步给其他的哨兵。
同步配置的时候其他哨兵根据什么更新自己的配置
执行切换的哨兵a,会从要切换到的新master(salve->master)获取一个configuration epoch作为唯一的version号。
如果第一个选举出的哨兵切换失败了,那么其他哨兵,会等待failover-timeout时间、接替继续执行切换,重新获取一个新的configuration epoch 作为新的version号。
version号十分重要、各种消息都是通过一个channel去发布和监听的,所以一个哨兵完成一次新的切换之后,新的master配置是跟着新的version号的,其他的哨兵都是根据版本号的大小来更新自己的master配置
集群节点间的通讯机制
gossip协议:ping,pong,meet,fail等
- meet:某个节点在内部发送了一个meet 消息给新加入的节点,通知那个节点去加入集群。然后新节点就会加入到集群的通信中
- ping:每个节点都会频繁给其它节点发送 ping,其中包含自己的状态还有自己维护的集群元数据,互相通过 ping 交换元数据。
- pong:ping 和 meet消息的返回响应,包含自己的状态和其它信息,也用于信息广播和更新。
- fail:某个节点判断另一个节点 fail 之后,就发送 fail 给其它节点,通知其它节点说这个节点已宕机。
ping详解
- ping 时要携带一些元数据,过于频繁会加重网络负担。一般每个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通信的其它节点。
- 如果发现某个节点通信延时达到了 cluster_node_timeout / 2,那么立即发送 ping,避免数据交换延时过长导致信息严重滞后。比如说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的情况,就会有问题。所以 cluster_node_timeout 可以调节,如果调得比较大,那么会降低 ping 的频率。
- 每次 ping,会带上自己节点信息+ 1/10 其它节点的信息,发送出去进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。
Redis集群为什么至少需要三个master节点,并且推荐节点数为奇数?
因为新master的选举需要大于半数的集群master节点同意才能选举成功,如果只有两个master节点,当其中一个挂了,是达不到选举新master的条件的。
down-after-milliseconds
默认30秒,这段时间里哨兵没收到主节点消息,则主观认为断开连接。
RedLock
Redlock是一种算法,可用实现多节点redis的分布式锁。
RedLock官方推荐,Redisson完成了对Redlock算法封装。
特性:
- 互斥访问:即永远只有一个 client 能拿到锁
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使锁定资源的服务崩溃或者分区,仍然能释放锁。
- 容错性:只要大部分 Redis 节点存活(一半以上),就可以正常提供服务
原理:
- 获取当前Unix时间,以毫秒为单位。
- 依次尝试从N个实例,使用相同的key和随机值获取锁。在步骤2,当向Redis设置锁时,客户端应该设置一个网络连接和响应超时时间,这个超时时间应该小于锁的失效时间。例如你的锁自动失效时间为10秒,则超时时间应该在5-50毫秒之间。这样可以避免服务器端Redis已经挂掉的情况下,客户端还在死死地等待响应结果。如果服务器端没有在规定时间内响应,客户端应该尽快尝试另外一个Redis实例。
- 客户端使用当前时间减去开始获取锁时间(步骤1记录的时间)就得到获取锁使用的时间。当且仅当从大多数(这里是3个节点)的Redis节点都取到锁,并且使用的时间小于锁失效时间时,锁才算获取成功。
- 如果取到了锁,key的真正有效时间等于有效时间减去获取锁所使用的时间(步骤3计算的结果)。
- 如果因为某些原因,获取锁失败(没有在至少N/2+1个Redis实例取到锁或者取锁时间已经超过了有效时间),客户端应该在所有的Redis实例上进行解锁(即便某些Redis实例根本就没有加锁成功)。
Redisson