Redis
redis的特点
高性能
:读11万次每秒,写8万次每秒高可用
:分布式集群可以是多主多从,当主节点发生异常时,对应从节点可以顶替,保持高可用。易扩展
:redis3.0以后的所有数据都存储在16384个槽中,创建集群时需要将槽分配给各个主节点,需要扩容时,将槽重新分配给新的主节点,开启数据迁移即可,在扩容时redis仍然可用。数据结构丰富
:有string、list、set、sorted_set等原子性
:redis是单线程持久化
:redis可以将不用的内存写入磁盘中。重启时从磁盘加载进内存。灾难恢复
:当redis宕机时,可以利用RDB的全量备份和AOF的增量备份进行恢复。
数据结构
string
string的结构
string {
int len;//实际字符串的长度
int free;//剩余可用长度
char buf[];
}
- 有长度字段,求长度为O(1)。
- 预分配。当len小于1M时,每次分配空间为原来的2倍。当len大于1M时,每次分配空间增加1M。预留空间较多,append效率高,分配次数减少,但占用较多内存。
- 惰性释放空间。缩短字符串时,不立即释放因缩短而空出来的空间。
- 二进制安全。保留原始的字符串,不会受有’\0’的影响。可以用于存储图片、音频、视频等,图片等转化为字符串可能会中间出现’\0’。
- API安全,增长字符串时,增加了自动扩容防止缓冲区溢出。
适用
- 计数器,例如热门文章的访问次数、分享次数、点赞次数、收藏数。
命令
- set
- get
- strlen
- exists
- incr
- decr
- setex
- expire
list
redis3.2版本之前,list底层使用ziplist或者linked list,3.2版本之后使用quicklist,quicklist底层是ziplist+linkedlist。当存储的都是小整数或短字符串时使用ziplist。
ziplist使用连续内存,是顺序存储结构,类似于数组,但每个节点的大小不一样,使用尽量小合适的编码来存储节点实际数据,以此来节省内存,但这是以时间换空间的,需要额外计算节点长度。ziplist可用于存储整数、字符串。
查询、插入、删除平均都是O(n),
插入、删除最坏情况下是O(n^2),这是因为连锁更新问题,但连锁更新的情况不多,因此不会有性能问题。
ziplist的结构
ziplist{
zlbytes #节点占用字节数
zltail #尾节点指针
zllen #节点数
entry1
entry2
…
zlend #ziplist结束标识,值为255
}
entry结构
entry {
previous_entry_length #上一个节点长度,用于反序遍历
encoding #根据data大小选择合适的编码,以节省内存
data #数据
}
连锁更新问题:
一个关键是previous_entry_length,当上一个节点长度小于254时,previous_entry_length使用1B。当上一个节点长度大于等于254,使用5B。
253、253、253,连续多个节点为253长度时,他们的previous_entry_length都是1B,当在前两个253、253插入一个254的节点时,第二个253要更新previous_entry_length为5B,更新后变为257, 第三个253也因此需要更新previous_entry_length为5B,这就是连锁更新。
253、254、254、254。删除第一个254, 第二个、第三个254都要连锁更新为250.
linked list
双向链表,查询为O(n)。插入、删除只需修改两个节点的前后指针为O(1)。
quicklist
一个双向链表,每个节点是ziplist,具有压缩列表特性。
适用:
- rpush、lpop,消息队列
- rpush、lrange,排行榜,每天定时计算。
- rpush、lrange最新列表,如朋友圈评论列表,不适用频繁更新,不适用需要分页,更新频繁且分页可能用户会拿到重复数据。
命令
- lpush
- lpop
- rpush
- rpop
- lrange
- llen
hash
有两种实现,ziplist或hashtable。当键数量小于阈值且value值都小于阈值时使用ziplist,否则使用hashtable。hashtable底层是数组加链表。
适用
购物车
:(用户id,商品id,数量)对应(key, field, value)存储对象
:存储用户信息、商品信息等。(key, field, value)对应(对象id, 字段,值)。string + json也可能存储对象,但不便于修改某个字段值,但序列化简单。hash存储对象,能方便修改某个字段值,但字段是复杂结构如一个类对象时,要转为json后存入,多个这样的字段时序列化工作就很繁琐。频繁修改的对象用hash,一般对象用string + json
命令
- hset
- hmset
- hget
- hgetall
- hkeys
- hvals
- hexists
- hlen
- hdel
set
底层使用hashtable,类似于jdk1.8之前的HashMap,数组+链表
存储的数据无序,不重复。能方便的求两个set的并集、交集、差集。
适用
数据不重复的
sismember、scard好友、粉丝、关注集合
:将关注粉丝时,用smove将粉丝从粉丝集合移到好友集合。在集群下,sinter不适合用于求共同好友。操作多个key的如smove、sinter要求key在同一个槽中。使用sinter求共同好友,就要求大量用户的好友集合在同一个槽,这样数据就分布不均了。redis对key进行hash来确定放在那个槽,redis提供了Hash_Tag,即用{},key中出现了{},只会对花括号中的值hash,这样相同Hash_Tag的就会在同一个槽。sismember黑名单、白名单
srandmember随机展示
命令
- sadd
- spop
- scard
- sismember
- smembers
- sunion
- sinterstore
zset
有序集合,每个key都有一个score,score用于排序。
底层由压缩列表或 跳表+字典实现。
元素个数或元素最大长度超过阈值时,由ziplist压缩列表转为跳表+dict字典。使用压缩列表占用内存小,但增删效率低。字典是哈希表,用于key到分数的查询,跳表根据score范围查询。跳表是多层次的链表,也可以说是链表+索引,下一层是上一层的子集,根据score跳跃着比较,不断缩小范围,直到找到,查找效率高,增删为O(log n)。字典、跳表查询效率高,都是空间换时间的结果,需要较多的内存。
适用
数据需要根据某个权重进行排序
:如直播间的用户列表、礼物打赏榜。
命令
- zadd
- zrem
- zcard
- zscore
- zrange
- zrevrange
bitmap
1B有8位,能记录8个状态,因此能极大节省内存。
适用
统计当天在线人数
:用户id作为offset,在线就setbit online_key uid 1,bitcount online_key就能得到在线人数统计一段时间的活跃用户数
:用bitop and|or|xor|not 多个key。统计用户行为
:如用户对某篇文章点赞,setbit 文章id 用户id 1.
命令
- setbit
- getbit
- bitcount
- bitop
Redis单线程模型
Redis基于Reactor模式开发了一套高性能的事件处理模型。这套事件处理模型对应的是文件事件处理器,由于文件事件处理器是单线程运行的,因此也成为单线程模型。
文件事件处理器包括IO多路复用程序、事件分发器、事件处理器。
IO多路复用器监听大量客户端连接,它将感兴趣的事件注册到内核中并监听事件的产生。文件事件分派器将socket与相应的事件处理器关联起来。事件处理器对事件处理。
通过IO多路复用程序监听大量客户端连接,redis不用创建大量线程来监听,降低了资源消耗。这样既有高性能,又能与redis的其他单线程运行的模块对接,这保持了redis单线程设计的简单性。
Redis与多线程
redis4.0时加入了多线程,但主要是针对大键值对的删除等缓慢操作。总体上说redis6.0之前主要还是单线程处理。
为什么redis6.0之前不使用多线程
- 单线程简单容易实现、维护
- redis的性能瓶颈不在cpu,在内存和网络
- 多线程存在死锁,上下文切换等问题,甚至可能会影响性能
redis6.0之后,使用多线程来提高网络IO的读写性能。但命令的执行还是单线程顺序执行,因此不用担心线程安全问题。
多线程默认是关闭的,需要自己开启并设置线程数。
redis过期key
redis可以通过expire设置key过期的时间,实际上有一个过期字典,以key为键,以unix时间戳为value。redis采用惰性删除+定时删除对过期key处理。惰性删除是在访问key时判断key是否过期并处理,对cpu友好。定时删除是定时从过期字典抽取一部分key检查是否过期并处理,对内存友好。
redis采用惰性删除+定时删除,有可能数据增长过快,导致内存不足,这时就用上了内存淘汰机制
redis内存淘汰机制
- volatile-lru:从设置了过期时间的key中淘汰最近最少使用的数据
- allkeys-lru:从所有key中淘汰最近最少使用的数据
- volatile-random:从设置了过期时间的key中随机淘汰一些数据
- allkeys-random:从所有key中
- volatile-ttl:从设置了过期时间的key中淘汰临近过期的数据
- volatile-lfu:从设置了过期时间的key中淘汰最少使用的数据
- allkeys-lfu
- no-eviction:不淘汰
redis持久化机制
快照RDB
创建内存快照来保存某个时刻的状态。可以将快照传输给其他服务器来创建具有相同数据的服务器,主从结构。快照保留在原地,当redis崩溃后,重启redis重新加载进内存。
redis.conf
save 900 1 #900秒后至少1个key发生变化,执行BGSAVE创建快照
AOF(append-only file)
AOF持久化相比RDB实时性更好,已成为主流方案。
AOF默认不开启,通过appendonly yes开启,开启后,redis每条会对数据库有影响的操作都会记录到内存缓存aof_buf中,根据appendfsync的配置将aof_buf的内容写到磁盘AOF文件中。
appendfsync配置
appendfsync always
appendfsync everysec
appendfsync no
推荐使用everysec,每秒将aof_buf的内容写到磁盘aof文件中,不会对redis性能造成影响。最多只会丢失一秒钟的数据。
混合持久化
redis4.0开始后,支持RDB、AOF混合持久化。AOF重写时,将RDB放到AOF文件的开头,加速加载同时不会有过多丢失。缺点RDB的部分是压缩编码的,可读性差。
AOF重写
AOF重写,创建一个新的AOF文件,保证这个AOF文与现有的数据库状态一致,替换掉旧的AOF。
执行BGREWRITEAOF时,创建一个子进程,来执行创建新的AOF文件,在创建期间,数据库的所有写操作会被写入重写缓冲区,创建AOF文件完成后,将重写缓冲区的内容写入到AOF文件中,使得新的AOF与现有的数据库状态一致,替换到旧的AOF文件。
旁路缓存模式的问题
- redis启动时无缓存
可将热点数据提前放入缓存 - 写操作频繁时,缓存命中率下降
- 允许短时间内缓存与数据库非一致的情况,更新db后更新缓存,设置过期时间为较短。
- 强一致性要求,更新db后,加锁/分布式锁来更新db,防止线程安全问题。