数据结构
数据结构是Redis的实体,承载着内部数据的存储,理解数据结构有利于我们对Redis存储进行优化,所以需要重点去理解.
object encoding key
查看键值类型的编码.
数据结构 | 内部编码 | 说明 |
---|---|---|
string | raw | 小于39个字节字符串 |
int | 8个字节长整型,只有当key为整型才会被存储 | |
embstr | 大于39个字节字符串 | |
hash | hashtable | 无法满足ziplist的条件会启用hashtable,因为大的ziplist会影响读写效率会切换到hashtable |
ziplist | 压缩列表,所有值都小于hash-max-ziplist-value配置并且field的个数小于hash-max-ziplist-entries时会启用ziplist,占用内存小. | |
list | linkedlist | 无法满足的ziplist条件会被启用 |
ziplist | 压缩列表,所有元素的值都小于hash-max-ziplist-value配置并且元素的个数小于hash-max-ziplist-entries时会启用ziplist,占用内存小. | |
set | hashtable | 无法满足intset条件会启用hashtable进行存储 |
instset | 集合中的元素都是整数且元素个数小于set-max-intset-entries,减少内存的使用 | |
zset | ziplist | 压缩列表,所有元素值都小于zset-max-ziplist-value配置并且元素的个数小于zset-max-ziplist-entries时会启用ziplist,占用内存小. |
skiplist | 无法满足ziplist会启用skiplist | |
bitmaps | - | 本身的数据结构与string的数据结构一致,但是可以用来进行位操作运算 |
HyperLogLog | - | 基于基数算法实现的数据结构,占用内存很小但是存在误差,0.81%的失误率 |
注意:上述表中的hash-max-ziplist-entries,set-max-intset-entrie的默认个数是为512,zet-max-ziplist-entries默认个数为128,zset-max-ziplist-value,hash-max-ziplist-value的默认大小为64字节.
ziplist最大特性就是空间连续,只需要操作一次malloc,有新的元素会重新进行realloc.
intset内部为了减少内存开销将int类型分为了16位,32位,64位,所以当插入的整型不处于同一类型时会被转换成范围更大的类型.
linkedlist,双向链表,与ziplist相比,不需要进行realloc,但是内存不连续.
skiplist 从字面意思上看是是由跳跃的链表组成,skiplist首先是有序的并且为了更快的查找提供了跨越元素的指针,因此要比顺序链表查找要快很多,插入和删除操作比平衡树要高效一些,可以简单理解为平衡树另一种实现.除此之外Redis中的skiplist加入hashtable,加速了访问速度.
HyperLogLog与Bitmaps各有各的使用场景,对精度要求不高又要存储很大的数可以使用HyperLogLog.对精度要求高的可以选择string类型进行原子(incrby)操作.
从数据结构中不难得出结论的是,在Redis存储的数据尽量保持较小的字节,可以提高读写的性能.避免太大key写入以及查询.
需要注意的一些命令
由于Redis是单线程执行,避免过度耗时的命令,阻塞到线程,所以这里列举一些高时间复杂度的命令,在实际的生产环境当中需要注意.
1.hash:hgeall,hkeys,hvals 时间复杂度为O(n),n为field总数
2.string:mget ,mset, 时间复杂度为O(n) (实际上比多次执行单条get命令要快很多,减少了网络消耗)
3.list:lrem,lindex,lset 时间复杂度O(n),lindex,lset的n为索引偏移量,lrem为列表长度.
4.set:sinter,O(m*k)m是建的个数,k是多个集合元素中最少的个数,sunion,sdiff O(k),k是多个元素的和.
5.zset:zadd,zrem O(k*log(n))k是操作元素的个数,n是集合成员的个数, zremrangebyrank,zremrangebyscore O(log(n)+k)k是删除成员的个数,n是当前有序集合成员的个数,zinterstore O(n*k)+O(m*log(m)) n是成员数最小的有序集合成员个数,k是有序集合个数,m是结果集合成员的个数,zunionsotre O(n)+O(m*log(m))n是所有有序集合成员个数和,m是结果集中成员个数
从前面的时间复杂度可以看出不建议在redis当中执行一些集合操作如union,inter等,可以放在应用层进行操作.不仅如此我们还需要考虑网络开销,将多个命令转换成一条命令进行传输可以减少网络IO.
慢查询定位
1.slowlog get
线上可以通过如下命令获取到慢查询的命令,已经命令返回的值.
slowlog get [N]
- 1
我们也可以慢日志查询进行重置使用命令slowlog reset
.
线上配置更改slowlog-max-len
配置控制慢日志查询列表的长度,也就是slowlog
命令获取结果集的长度.
更改slowlog-log-slower-than
配置设置命令执行的时间超过指定毫秒数记录至慢查询.
config set slowlog-log-slower-than 1
,可以在线更命令,config rewrite
进行重写配置.
2.redis-cli
通过redis-cli --bigkeys
可以查看在redis存储的大键.可以很方便的定位具体的键,然后可以具体进行优化.
优化方案
Redis本质是内存存储的,而且运算效率很高.所以网络开销占了大部分比例.因此一般情况下网络层的优化是主要的重点方向.
主体思想是将多条命令合并成合适大小的命令.不能合并太多的命令,产生网络阻塞,也不能太小,就失去了优化的意义.
Pipeline:可以将多条命令进行合并传输,通过redis-cli --lantency
帮助检测网络延时,也可以用来进行Pipeline合并命令数量的估算.
注意: Pipeline操作命令是非原子性.
缓存穿透问题
缓存穿透可以引起数据库实例挂掉,因此在大量请求未命中缓存时,需要放止穿透的发生.
- 使用Redis获取锁单个进程去更新,其余同步等待:这样的好处实现简单,但是如果处理速度较慢应用可能会出现卡顿或超时现象.
- 使用Redis获取锁单个进程去更新,其余直接返回旧值:返回旧值不需要查库,但是对于实时性要求高的应用来说无疑是个严重的问题.还对于PHP这种FCGI实现,”很难”读一个旧值,对于这样的情况根据业务需求选择适合的方式去处理.
- key设置为永不过期,单独脚本进行key更新,但是实时性可能要差一些.
- 热点Key批量过期,尽量设置一个范围内随机过期时间.
以上两种方案都可以缓解大量请求与数据持久化层交互,防止重复写,穿透问题等.
事务
Redis的事务,运行时产生错误不支持回滚.还有一种情况是,执行事务时,观察别的客户端进行修改,如果键值被修改那么事务不会执行,使用watch
命令可以实现,这两种特性会导致业务层处理问题变的更加复杂.
因此一般推荐使用Lua进行事务操作,因为使用Lua的操作是原子性的.
执行 redis-cli script load script
加载script脚本代码,返回一个hash值用于执行.
eval script numkeys key [key ...] arg [arg ...]
,执行脚本.
script kill
可以终止执行中的脚本,但是需要注意一些死循环操作redis的脚本无法被kill掉,只能shutdown server.因此在编写脚本时需要额外注意.
但是实际中没有推行的原因很大一点是开发成本以及维护成本较高.
发布和订阅
Redis还有一个功能就是很方便自带发布订阅模式,类似于一个消息队列.但是实现方面还是很简单,实际生产中用的少,其多方面原因是现在的消息队列中间件都有着完善的消费机制有这高性能,可用性与可靠性的保证.
操作命令
实现逻辑与一般的消息队列一致,存在一个channel用于存储消息,发布消息.类似于管道的概念.
- 发布消息 : publish channel message
- 订阅: subscribe channel [channel…]
- 模式订阅: 类似于通配符的那种 psubscribe pattern
- 取消订阅: unsubscribe channel [channel…]
- 取消模式订阅:punsubscribe pattern
GEO
GEO底层实现是使用zset,地理信息定位查询提供如下操作:
1.增加地理位置信息: geoadd key longitude latitude member
2.获取地理位置信息: geopos key member
3.获取两个地理位置的距离: geo key member member [unit] unit可以是m,km,mi(英里),ft(代表尺)
4.获取指定位置范围内的地理信息位置集合:GEORADIUSBYMEMBER key member radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [ASC|DESC] [COUNT count]
GEORADIUSBYMEMBER 以成员的位置获取指定范围的成员,GEORADIUS 以经纬度的位置获取指定范围的成员.
客户端问题排查
Redis为每个客户端建立了输入缓存以及输出缓存,记录了每个连接的存活信息,推荐使用client list
命令进行排查问题.
id=2202 addr=127.0.0.1:19508 fd=7 name= age=5 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client
- 1
主要参数介绍:
id:连接id
addr:客户端连接地址
qbuf:输入缓存占用代销
qbuf-free:输入缓存空余大小
ool:输出缓存固定缓冲区
obl:输出缓存动态缓冲区列表的长度
omem:输出缓存使用的字节数
注意输入缓存大小不会记入maxmemory,如果内存使用过大可能会触发OOM机制,输入缓存超过1G会关闭客户端连接.
持久化存储
大家可能都知道Redis的持久化存储有两种方式:1.AOF 2.RDB
都有各自的使用场景,但是多数是基于两者之间结合使用.
RDB
根据save m n
m秒内n次修改数据进行一次bgsave.配置简单,在一些对数据持久化要求不高的场景可以使用.也可用于定期备份数据信息.
AOF
实时持久化存储,以独立日志的方式记录每次写命令.这种持久化方式会先写入aof_buf缓存后然后进行fsync操作.
根据appendfsync
同步文件策略,其配置有如下三个值:
1.always 每次写入缓存都会调用fsync
2.everysec 每次写入缓存后执行系统write操作,由专门的线程执行fsync
3.no 每次写入缓存后执行系统write操作,由系统进行fsync
**注意:**AOF同步硬盘由专门的线程执行,主线程会判断上一次的fsync时间是否大于两秒,如果时间大于两秒,会阻塞主线程,等待同步磁盘.
我们可以手动执行bgrewrite
进行命令的重写或者可以通过auto-aof-rewrite-minsize
AOF运行文件最小体积与auto-aof-rewrite-percentage
当前AOF文件与重写上一次AOF文件的比值.
重写机制:重写开启会fork一个进程,这时linux会针对内存进行copy on write的优化,子进程重写完AOF命令后,由于在父进程还在接收着新的命令,父进程会将新接收的命令写入另一块缓存区进行重写.
复制架构
主要实现是Redis通过复制字节的偏移量标识当前复制的位置,Redis一般都是采用主写从读的架构,架构的拓扑类型分为:
1.一主一从
2.一主多从
3.树形拓扑
数据同步策略
- 全量复制:将Redis的数据库中RDB全量复制到从节点.
- 部分复制:如果主节点中的复制积压缓存区内存在offset,那么会执行部分复制到从节点.如果偏移量没有在复制积压缓存区(从节点共享)内会执行全量复制.
注意:如果主从节点网络特别的慢以及数据量比较大,要注意全量复制可能会超过默认的配置时间60s.
问题: 最常见的可能就是主从延迟的
原因: 异步复制无法避免
解决: 可以通过监控手段判断主从节点复制的差值,如果超过指定值那么可以将客户端的连接切换到主节点.
高可用机制-哨兵
当主节点故障,哨兵机制能自动完成故障发现和故障转移.
每个哨兵节点会对主从节点与其余的哨兵点进行监控.当发生故障时,多个哨兵节点达成一致,会选出一个领导者进行故障切换.
多个哨兵节点可以保证了哨兵机制的高可用,个数大于等于三且为奇数.
监控机制
1.每隔10秒哨兵节点向主从节点获取最新拓扑结构.
2.每隔2秒通过__sentinel__:hello
频道获取以及发布对其它哨兵节点信息判断.
3.每个1秒向主从节点,其余哨兵节点发送检测,用于故障检测.
故障节点的判断
当哨兵节点发现主节点发送心跳检测如果超过一秒会被主观下线,如果哨兵节点判断下线当超过指定个数后,会被确认客观下线.
Redis 哨兵节点领导者选举是使用了Raft算法实现,这里不过多介绍.
客户端感知
可以通过Redis sentinel客户端,遍历其客户端信息得到当前的master信息,然后将客户端连接配置进行更新.
集群
Redis的集群是通过hash slot实现.也就是:crc16(key)%16383.可以动态扩展节点.从算法来看最多支持16384个节点.
采用了集群可以方便提升Redis的扩展性,但是也有一些限制.
集群之间的通过Redis独有的Cluster bus协议进行心跳检测,通过Gossip 协议以及更新配置技术来获取集群当中节点的状态信息,故障切换等.
1.无法支持 mget, mset命令,但是hashtag可以实现
2.不支持树形复制结构,支持一主多从架构
3.集群不支持强一致性,也就是说在主节点A写入的数据,A写入成功返回客户端,异步通知从节点A1写入,这样有可能在故障切换时造成数据的丢失.
重定向以及重新分区
如果Redis客户端发送一个查询请求,但是key不在当前的节点,集群节点会返回MOVED
错误.然后返回对应hash slot.说白了重定向和HTTP的重定向实现思路是一样的,客户端收到重定向后会再次向正确的集群节点发起查询请求.
数据在迁移的过程中,当被迁移的节点收到请求时,会返回ASK至客户端,客户端当前的请求仍然发送到被迁移的节点,但是随后发送ASKING(强制节点返回结果)请求发送到迁移目的节点,最主要的是收到ASK命令的客户端不能更新slot和节点的映射,因为这时候数据没有迁移完成.
故障切换
故障切换分为两个部分:
- 故障发现:分为主观下线和客观下线,如果主节点超过半数认为故障节点主观下线,那么该故障节点会被标记为客观下线.
- 故障恢复:从节点负责对客观下线的节点发起故障恢复.主要包含五个流程:
1.资格检查,主要是观察从节点与故障节点最后交互的时间是否满足资格
2.准备选举时间(延迟选举),所有从节点复制偏移量最大的提前出发该流程.
3.发起选举,更新纪元信息(配置信息,存在时间概念以最新为准),广播选举消息.
4.选举投票,主节点给故障节点的从节点进行投票.
5.替换主节点
</article>