Redis是什么?
Redis 是一个 key-value 存储系统,它支持存储的 value 类型相对更多,包括 string、list、set、zset(sorted set --有序集合)和 hash。这些数据结构都支持 push/pop、add/remove 及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,Redis 支持各种不同方式的排序。为了保证效率,数据都是缓存在内存中,Redis 会周期性的把更新的数据写入磁盘或者把修改操作写入追加的记录文件,并且在此基础上实现了master-slave(主从)同步。
Redis 都有哪些使用场景?
- 缓存:这应该是 Redis 最主要的功能了,也是大型网站必备机制,合理地使用缓存不仅可以加 快数据的访问速度,而且能够有效地降低后端数据源的压力。
- 共享Session:对于一些依赖 session 功能的服务来说,如果需要从单机变成集群的话,可以选择 redis 来统一管理 session。
- 消息队列系统:消息队列系统可以说是一个大型网站的必备基础组件,因为其具有业务 解耦、非实时业务削峰等特性。Redis提供了发布订阅功能和阻塞队列的功 能,虽然和专业的消息队列比还不够足够强大,但是对于一般的消息队列功 能基本可以满足。比如在分布式爬虫系统中,使用 redis 来统一管理 url队列。
- 分布式锁:在分布式服务中。可以利用Redis的setnx功能来编写分布式的锁,虽然这个可能不是太常用。 当然还有诸如排行榜、点赞功能都可以使用 Redis 来实现,但是 Redis 也不是什么都可以做,比如数据量特别大时,不适合 Redis,我们知道 Redis 是基于内存的,虽然内存很便宜,但是如果你每天的数据量特别大,比如几亿条的用户行为日志数据,用 Redis 来存储的话,成本相当的高。
Redis为什么这么快?
-
基于内存存储实现
我们都知道内存读写是比在磁盘快很多的,Redis基于内存存储实现的数据库,相对于数据存在磁盘的MySQL数据库,省去磁盘I/O的消耗。 -
高效的数据结构
我们知道,MySQL 索引为了提高效率,选择了 B+ 树的数据结构。其实合理的数据结构,就是可以让你的应用/程序更快。先看下 Redis 的数据结构&内部编码图:
SDS简单动态字符串
- 字符串长度处理:Redis获取字符串长度,时间复杂度为O(1),而C语言中,需要从头开始遍历,复杂度为O(n);
- 空间预分配:字符串修改越频繁的话,内存分配越频繁,就会消耗性能,而SDS修改和空间扩充,会额外分配未使用的空间,减少性能损耗。
- 惰性空间释放:SDS 缩短时,不是回收多余的内存空间,而是free记录下多余的空间,后续有变更,直接使用free中记录的空间,减少分配。
- 二进制安全:Redis可以存储一些二进制数据,在C语言中字符串遇到’\0’会结束,而 SDS中标志字符串结束的是len属性。
- 合理的数据编码
Redis 支持多种数据数据类型,每种基本类型,可能对多种数据结构。什么时候,使用什么样数据结构,使用什么样编码,是 redis 设计者总结优化的结果。
- String:如果存储数字的话,是用int类型的编码;如果存储非数字,小于等于39字节的字符串,是embstr;大于 39 个字节,则是 raw 编码。
- List:如果列表的元素个数小于 512 个,列表每个元素的值都小于 64 字节(默认),使用 ziplist 编码,否则使用 linkedlist 编码
- Hash:哈希类型元素个数小于512 个,所有值小于 64 字节的话,使用ziplist编码,否则使用 hashtable 编码。
- Set:如果集合中的元素都是整数且元素个数小于 512 个,使用 intset 编码,否则使用 hashtable 编码。
- Zset:当有序集合的元素个数小于 128 个,每个元素的值小于 64 字节时,使用 ziplist 编码,否则使用skiplist(跳跃表)编码
3.4 合理的线程模型
I/O 多路复用
多路I/O复用技术可以让单个线程高效的处理多个连接请求,而Redis使用用epoll作为I/O多路复用技术的实现。并且,Redis自身的事件处理模型将epoll中的连接、读写、关闭都转换为事件,不在网络I/O上浪费过多的时间。
什么是I/O多路复用?
I/O :网络 I/O
多路 :多个网络连接
复用:复用同一个线程。
IO多路复用其实就是一种同步 IO 模型,它实现了一个线程可以监视多个文件句柄;一旦某个文件句柄就绪,就能够通知应用程序进行相应的读写操作;而没有文件句柄就绪时,就会阻塞应用程序,交出 cpu。
单线程模型
Redis是单线程模型的,而单线程避免了CPU不必要的上下文切换和竞争锁的消耗。也正因为是单线程,如果某个命令执行过长(如hgetall命令),会造成阻塞。
Redis是面向快速执行场景的数据库,所以要慎用如 smembers 和 lrange、hgetall 等命令。
Redis 6.0 引入了多线程提速,它的执行命令操作内存的仍然是个单线程。
- 虚拟内存机制
Redis直接自己构建了VM机制 ,不会像一般的系统会调用系统函数处理,会浪费一定的时间去移动和请求。
Redis的虚拟内存机制是啥呢?
虚拟内存机制就是暂时把不经常访问的数据(冷数据)从内存交换到磁盘中,从而腾出宝贵的内存空间用于其它需要访问的数据(热数据)。通过 VM 功能可以实现冷热数据分离,使热数据仍在内存中、冷数据保存到磁盘。这样就可以避免因为内存不足而造成访问速度下降的问题。
Redis支持的数据类型?
String(字符串)
格式: set key value、get key
string类型是二进制安全的。意思是redis的string可以包含任何数据。比如jpg图片或者序列化的对象 。
string类型是Redis最基本的数据类型,一个键最大能存储512MB。
应用场景:共享session、分布式锁,计数器、限流。
内部编码有3种,int(8字节长整型)/embstr(小于等于39字节字符串)/raw(大于39个字节字符串)
C语言的字符串是char[]实现的,而Redis使用SDS(simple dynamic string) 封装,sds源码如下:
struct sdshdr{ unsigned int len; // 标记buf的长度 unsigned int free; //标记buf中未使用的元素个数 char buf[]; // 存放元素的坑 }
SDS 结构图如下:
Redis为什么选择SDS结构,而C语言原生的char[]不香吗?
举例其中一点,SDS中,O(1) 时间复杂度,就可以获取字符串长度;而 C 字符串,需要遍历整个字符串,时间复杂度为 O(n)
Hash(哈希)
格式: hset key field value 、hget key field
Redis hash 是一个键值 ( key=>value ) 对集合。
Redis hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象。
List(列表)
Redis 列表是简单的字符串列表,按照插入顺序排序。你可以添加一个元素到列表的头部(左边)或者尾部(右边)
格式: lpush name value
在 key 对应 list 的头部添加字符串元素
格式: rpush name value
在 key 对应 list 的尾部添加字符串元素
格式: lrem name index
key 对应 list 中删除 count 个和 value 相同的元素
格式: llen name
返回 key 对应 list 的长度
Set(集合)
格式: sadd name value
Redis的Set是string类型的无序集合。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
zset (sorted set:有序集合)
格式: zadd name score value
zset 和 set 一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
zset的成员是唯一的,但分数(score)却可以重复。
Redis为什么是单线程的?
- 代码更清晰,处理逻辑更简单;
- 不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;
- 不存在多线程切换而消耗 CPU;
- 无法发挥多核 CPU 的优势,但可以采用多开几个 Redis 实例来完善;
Redis真的是单线程的吗?
Redis6.0 之前是单线程的,Redis6.0 之后开始支持多线程;
redis 内部使用了基于 epoll 的多路服用,也可以多部署几个 redis 服务器解决单线程的问题;
redis 主要的性能瓶颈是内存和网络;
内存好说,加内存条就行了,而网络才是大麻烦,所以 redis6 引入了多线程的概念,
redis6.0在网络 IO 处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的。
Redis 持久化?
redis提供了两种持久化的方式,分别是 RDB(Redis DataBase)和 AOF(Append Only File)。
- RDB,简而言之,就是在不同的时间点,将 redis 存储的数据生成快照并存储到磁盘等介质上;
- AOF,则是换了一个角度来实现持久化,那就是将redis执行过的所有写指令记录下来,在下次 redis 重新启动时,只要把这些写指令从前到后再重复执行一遍,就可以实现数据恢复了。
RDB 持久化
是指在指定的时间间隔内,执行指定次数的写操作,将内存中的数据集快照写入磁盘中,它是Redis默认的持久化方式。执行完操作后,在指定目录下会生成一个dump.rdb文件,Redis 重启的时候,通过加载dump.rdb文件来恢复数据。RDB触发机制主要有以下几种:
RDB 的优点
- 适合大规模的数据恢复场景,如备份,全量复制等
RDB缺点
- 没办法做到实时持久化/秒级持久化。
- 新老版本存在RDB格式兼容问题
在生成 RDB期间,Redis 可以同时处理写请求么?
可以的,Redis提供两个指令生成RDB,分别是 save 和 bgsave
- 如果是 save 指令,会阻塞,因为是主线程执行的。
- 如果是 bgsave 指令,是 fork 一个子进程来写入 RDB 文件的,快照持久化完全交给子进程来处理,父进程则可以继续处理客户端的请求。
AOF(append only file) 持久化
采用日志的形式来记录每个写操作,追加到文件中,重启时再重新执行AOF文件中的命令来恢复数据。它主要解决数据持久化的实时性问题。默认是不开启的。
AOF的工作流程如下
AOF的优点
数据的一致性和完整性更高
AOF的缺点
AOF记录的内容越多,文件越大,数据恢复变慢。
其实 RDB 和 AOF 两种方式也可以同时使用,在这种情况下,如果 redis 重启的话,则会优先采用 AOF 方式来进行数据恢复,这是因为 AOF 方式的数据恢复完整度更高。
如果你没有数据持久化的需求,也完全可以关闭 RDB 和 AOF 方式,这样的话,redis 将变成一个纯内存数据库。
缓存穿透/缓存雪崩?如何解决?
一个常见的缓存使用方式:读请求来了,先查下缓存,缓存有值命中,就直接返回;缓存没命中,就去查数据库,然后把数据库的值更新到缓存,再返回。
缓存穿透
一般的缓存系统,都是按照 key 去缓存查询,如果不存在对应的 value,就应该去后端系统查找(比如DB 数据库)。一些恶意的请求会故意查询不存在的 key,请求量很大,就会对后端系统造成很大的压力。这就叫做缓存穿透。
缓存穿透一般都是这几种情况产生的:
业务不合理的设计,比如大多数用户都没开守护,但是你的每个请求都去缓存,查询某个userid查询有没有守护。
业务/运维/开发失误的操作,比如缓存和数据库的数据都被误删除了。
黑客非法请求攻击,比如黑客故意捏造大量非法请求,以读取不存在的业务数据。
如何解决?
- 如果是非法请求,我们在API入口,对参数进行校验,过滤非法值。
- 对查询结果为空的情况也进行缓存,缓存时间设置短一点,或者该 key 对应的数据 insert 之后清理缓存。
- 对一定不存在的 key 进行过滤。可以把所有的可能存在的 key 放到一个大的 Bitmap 中,查询时通过该Bitmap 过滤。
缓存雪崩
当缓存服务器重启或者大量缓存集中在某一时间段失效,这样在失效的时候,会给后端系统带来很大的压力,导致系统崩溃。
如何解决?
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个 key 只允许一个线程查询数据和写缓存,其它线程等待;
- 做二级缓存,A1 为原始缓存,A2 为拷贝缓存,A1 失效时,可以访问 A2,A1 缓存失效时间设置为短期,A2 设置为长期;
- 不同的 key,设置不同的过期时间,让缓存失效的时间尽量均匀;
缓存击穿
指热点 key 在某个时间点过期的时候,而恰好在这个时间点对这个 Key 有大量的并发请求过来,从而大量的请求打到 DB。
缓存击穿和缓存雪崩看着有点像,其实它两区别是,缓存雪奔是指数据库压力过大甚至 down 机,缓存击穿只是大量并发请求到了 DB 数据库层面。可以认为击穿是缓存雪崩的一个子集吧
如何解决?
- 使用互斥锁方案。缓存失效时,不是立即去加载 DB 数据,而是先使用某些带成功返回的原子操作命令,如(Redis的setnx)去操作,成功的时候,再去加载 DB 数据库数据和设置缓存。否则就去重试获取缓存。
- “永不过期”,是指没有设置过期时间,但是热点数据快要过期时,异步线程去更新和设置过期时间。
怎么保证缓存和数据库数据的一致性?
1、淘汰缓存
数据如果比较复杂,进行缓存的更新操作就会变得异常复杂,因此一般推荐选择淘汰缓存,而不是更新缓存。
2、选择先淘汰缓存,再更新数据库
假如先更新数据库,再淘汰缓存,如果淘汰缓存失败,那么后面的请求都会得到脏数据,直至缓存过期。
假如先淘汰缓存再更新数据库,如果更新数据库失败,只会产生一次缓存穿透,相比较而言,后者对业务则没有本质上的影响。
3、延时双删策略
如下场景:同时有一个请求 A 进行更新操作,另一个请求 B 进行查询操作。
- 请求 A 进行写操作,删除缓存
- 请求 B 查询发现缓存不存在
- 请求 B 去数据库查询得到旧值
- 请求 B 将旧值写入缓存
- 请求 A 将新值写入数据库
public void write(String key,Object data){
redisUtils.del(key);
db.update(data);
Thread.Sleep(100);
redisUtils.del(key);
}
这么做,可以将1秒内所造成的缓存脏数据,再次删除。这个时间设定可根据俄业务场景进行一个调节。
4、数据库读写分离的场景
两个请求,一个请求 A 进行更新操作,另一个请求B进行查询操作。
- 请求 A 进行写操作,删除缓存
- 请求 A 将数据写入数据库了,
- 请求 B 查询缓存发现,缓存没有值
- 请求 B 去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
- 请求 B 将旧值写入缓存
- 数据库完成主从同步,从库变为新值
依旧采用延时双删策略解决此问题。
Redis 有哪些架构模式
单机版
特点:
- 简单
问题:
- 内存容量有限
- 处理能力有限
- 无法高可用。
主从模式
Redis 的复制(replication)功能允许用户根据一个 Redis 服务器来创建任意多个该服务器的复制品,其中被复制的服务器为主服务器(master),而通过复制创建出来的服务器复制品则为从服务器(slave)。
只要主从服务器之间的网络连接正常,主从服务器两者会具有相同的数据,主服务器就会一直将发生在自己身上的数据更新同步给 从服务器,从而一直保证主从服务器的数据相同。
特点:
- master/slave 角色
- master/slave 数据相同
- 降低 master 读压力在转交从库
问题:
- 无法保证高可用
- 没有解决 master 写的压力
哨兵模式
Redis sentinel 是一个分布式系统中监控 redis 主从服务器,并在主服务器下线时自动进行故障转移。其中三个特性:
- 监控(Monitoring):Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。
- 提醒(Notification):当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。
- 自动故障迁移(Automatic failover):当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作。
特点:
- 保证高可用
- 监控各个节点
- 自动故障迁移
缺点:
- 主从模式,切换需要时间丢数据
- 没有解决 master 写的压力
集群(代理型)
Twemproxy 是一个 Twitter 开源的一个 redis 和 memcache 快速/轻量级代理服务器;Twemproxy 是一个快速的单线程代理程序,支持 Memcached ASCII 协议和 redis 协议。
特点:
- 多种 hash 算法:MD5、CRC16、CRC32、CRC32a、hsieh、murmur、Jenkins
- 支持失败节点自动删除
- 后端 Sharding 分片逻辑对业务透明,业务方的读写方式和操作单个 Redis 一致
缺点:
- 增加了新的 proxy,需要维护其高可用。
- failover 逻辑需要自己实现,其本身不能支持故障的自动转移可扩展性差,进行扩缩容都需要手动干预
集群(直连型)## 标题
从 redis 3.0 之后版本支持 redis-cluster 集群,Redis-Cluster 采用无中心结构,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。
特点:
- 无中心架构(不存在哪个节点影响性能瓶颈),少了 proxy 层。
- 数据按照 slot 存储分布在多个节点,节点间数据共享,可动态调整数据分布。
- 可扩展性,可线性扩展到 1000 个节点,节点可动态添加或删除。
- 高可用性,部分节点不可用时,集群仍可用。通过增加 Slave 做备份数据副本
- 实现故障自动 failover,节点之间通过 gossip 协议交换状态信息,用投票机制完成 Slave到 Master 的角色提升。
缺点:
- 资源隔离性较差,容易出现相互影响的情况。
- 数据通过异步复制,不保证数据的强一致性
- 高可用Redis架构分析搭建,可以参考:
Redis 过期键的删除策略?
1.定时删除:在设置键的过期时间的同时,创建一个定时器 timer). 让定时器在键的过期时间来临时, 立即执行对键的删除操作。
2.惰性删除:放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是 否过期, 如果过期的话, 就删除该键;如果没有过期, 就返回该键。
3.定期删除:每隔一段时间程序就对数据库进行一次检查,删除里面的过期键。至 于要删除多少过期键, 以及要检查多少个数据库, 则由算法决定。
Redis 有哪几种数据淘汰策略?
- volatile-lru:当内存不足以容纳新写入数据时,从设置了过期时间的key中使用LRU(最近最少使用)算法进行淘汰;
- allkeys-lru:当内存不足以容纳新写入数据时,从所有key中使用LRU(最近最少使用)算法进行淘汰。
- volatile-lfu:4.0版本新增,当内存不足以容纳新写入数据时,在过期的key中,使用LFU算法进行删除key。
- allkeys-lfu:4.0版本新增,当内存不足以容纳新写入数据时,从所有key中使用LFU算法进行淘汰
- volatile-random:当内存不足以容纳新写入数据时,从设置了过期时间的key中,随机淘汰数据;。
- allkeys-random:当内存不足以容纳新写入数据时,从所有key中随机淘汰数据。
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的key中,根据过期时间进行淘汰,越早过期的优先被淘汰;
- noeviction:默认策略,当内存不足以容纳新写入数据时,新写入操作会报错。
热 Key 问题,如何解决
什么是热Key呢?在Redis中,我们把访问频率高的key,称为热点key。
**热点Key是怎么产生的呢?**主要原因有两个:
- 用户消费的数据远大于生产的数据,如秒杀、热点新闻等读多写少的场景。
- 请求分片集中,超过单 Redis 服务器的性能,比如固定名称 key,Hash 落入同一台服务器,瞬间访问量极大,超过机器瓶颈,产生热点 Key 问题。
那么在日常开发中,如何识别到热点key呢?
- 凭经验判断哪些是热Key;
- 客户端统计上报;
- 服务代理层上报
如何解决热key问题?
- Redis 集群扩容:增加分片副本,均衡读流量;
- 将热 key 分散到不同的服务器中;
- 使用二级缓存,即 JVM 本地缓存,减少 Redis 的读请求。
MySQL 与 Redis 如何保证双写一致性
- 缓存延时双删
- 删除缓存重试机制
- 读取 biglog 异步删除缓存
延时双删
什么是延时双删呢?流程图如下:
延时双删流程
- 先删除缓存
- 再更新数据库
- 休眠一会(比如1秒),再次删除缓存。
这个休眠一会,一般多久呢?都是1秒?
这个休眠时间 = 读业务逻辑数据的耗时 + 几百毫秒。为了确保读请求结束,写请求可以删除读请求可能带来的缓存脏数据。
这种方案还算可以,只有休眠那一会(比如就那1秒),可能有脏数据,一般业务也会接受的。但是如果第二次删除缓存失败呢?缓存和数据库的数据还是可能不一致,对吧?给 Key 设置一个自然的 expire 过期时间,让它自动过期怎样?那业务要接受过期时间内,数据的不一致咯?还是有其他更佳方案呢?
删除缓存重试机制
因为延时双删可能会存在第二步的删除缓存失败,导致的数据不一致问题。可以使用这个方案优化:删除失败就多删除几次呀,保证删除缓存成功就可以了呀~ 所以可以引入删除缓存重试机制
删除缓存重试流程
- 写请求更新数据库
- 缓存因为某些原因,删除失败
- 把删除失败的key放到消息队列
- 消费消息队列的消息,获取要删除的key
- 重试删除缓存操作
读取biglog异步删除缓存
重试删除缓存机制还可以吧,就是会造成好多业务代码入侵。其实,还可以这样优化:通过数据库的binlog 来异步淘汰 key。
以mysql为例吧
- 可以使用阿里的canal将binlog日志采集发送到MQ队列里面
- 然后通过ACK机制确认处理这条更新消息,删除缓存,保证数据缓存一致性
Redis 事务机制
Redis 通过 MULTI、EXEC、WATCH 等一组命令集合,来实现事务机制。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。
简言之,Redis事务就是顺序性、一次性、排他性的执行一个队列中的一系列命令。
Redis执行事务的流程如下:
- 开始事务(MULTI)
- 命令入队
- 执行事务(EXEC)、撤销事务(DISCARD )
Redis 的 Hash 冲突
Redis 作为一个 K-V 的内存数据库,它使用用一张全局的哈希来保存所有的键值对。这张哈希表,有多个哈希桶组成,哈希桶中的 entry 元素保存了 key 和 value 指针,其中 key 指向了实际的键,value 指向了实际的值。
哈希表查找速率很快的,有点类似于Java中的HashMap,它让我们在O(1) 的时间复杂度快速找到键值对。首先通过key计算哈希值,找到对应的哈希桶位置,然后定位到entry,在entry找到对应的数据。
什么是哈希冲突?
哈希冲突:通过不同的key,计算出一样的哈希值,导致落在同一个哈希桶中。
Redis为了解决哈希冲突,采用了链式哈希。链式哈希是指同一个哈希桶中,多个元素用一个链表来保存,它们之间依次用指针连接。
有些读者可能还会有疑问:哈希冲突链上的元素只能通过指针逐一查找再操作。当往哈希表插入数据很多,冲突也会越多,冲突链表就会越长,那查询效率就会降低了。
为了保持高效,Redis 会对哈希表做rehash操作,也就是增加哈希桶,减少冲突。为了rehash更高效,Redis还默认使用了两个全局哈希表,一个用于当前使用,称为主哈希表,一个用于扩容,称为备用哈希表。
Redis底层,使用的什么通讯协议?
RESP,英文全称是Redis Serialization Protocol,它是专门为 redis 设计的一套序列化协议. 这个协议其实在 redis 的 1.2 版本时就已经出现了,但是到了redis2.0 才最终成为 redis 通讯协议的标准。
RESP主要有实现简单、解析速度快、可读性好等优点。