1. 基础知识
1.1 NoSQL
答:NoSQL(Not Only SQL),泛指非关系型的数据库,目的是解决高并发、高拓展和大数据储存问题。细分为:键值型(Redis),列存储(HBase),文档型(MongoDB),图形(Neo4j)。
1.2 Redis
答:Redis(Remote Dictionary Server远程字典服务)是一款高性能、高并发的key-value型分布式内存数据库,基于内存运行且支持持久化的NoSQL数据库。常被用于缓存和消息队列。
1.2.1 优缺点
优点
- 高性能/读写性能优异。用户第一次访问数据后,数据存储在缓存中,之后再访问直接从缓存中获得,相当于直接操作内存。
- 高并发。操作缓存能承受的请求远大于直接访问数据库,将部分数据放在redis中,能提高并发程度。
- 支持数据持久化,AOF和RDB两种方式。
- 支持事务,所有操作都是原子性的。
- 数据类型丰富,string、hash、set、list、sorted set。
- 支持集群模式,如主从复制做到读写分离。
缺点
- 数据库容量受到物理内存的限制;
- 不具备自动容错和恢复功能。如主机宕机,部分数据未能同步到从机,切换IP后会导致数据不一致;
- 扩容难。
1.2.2 为什么执行速度快
答:总结如下:
- 基于内存实现,轻量级数据库;
- 单线程操作,避免切换上下文,也没有线程安全问题;
- 多路IO复用模型,一个线程监控多个IO流,事件放队列,派发器分发,对应处理器处理。
1.2.3 Redis和map区别
答:缓存分为本地缓存和分布式缓存。
- map是本地缓存,优点是轻量快速,缺点是多实例情况下,每个实例都有一个缓存。
- Redis是分布式缓存,多实例情况下,每个实例共用一个缓存,具有一致性,缺点是架构复杂。
1.2.4 Redis和memcached区别
答:主要是四点:
- Redis数据类型丰富,memcached只支持String类型;
- Redis支持数据持久化,RDB和AOF,memcached全部存在内存;
- Redis支持集群模式(主从,哨兵),memcached没有原生的集群模式;
- Redis是单线程的多路IO复用模型,memcached是多线程的非阻塞IO复用模型
1.3 数据类型和应用场景
答:支持五种数据类型作为value,key值都是字符串类型。
- string:最大为512M。用作计数器,缓存,用户的Session等;
- list:有序列表,基于链表实现。用作分页查询,列表功能,消息队列/异步队列(左进右出);
- set:自动去重的列表。用作全局去重等功能,比如共同好友;
- sorted set:多了权重参数。用来做排序,比如排行榜Top10功能,延时队列(时间戳做排序,内容做key,消费者根据score获取数据);
- hash:存储特定结构的信息/结构化数据。比如用户信息。
- pub/sub:主题订阅者模式,实现1:N的消息队列,但消费者下线后,生产的消息就丢失了。
1.4 Redis线程模型
答:Redis是单线程的。
- 内部使用了单线程的文件事件处理器 file event handler。
- 采用IO多路复用机制同时监听多个socket,将socket上的事件放入队列,事件派发器每次从队列中取出一个事件,交给对应的事件处理器处理。
- 文件事件处理器包含4个部分:多个socket,IO多路复用程序,文件事件派发器,事件处理器。
总结:一对多监听,事件放队列,派发器分发,对应处理器处理
2. 持久化
答:把内存数据写入磁盘,保证宕机再重启数据能恢复。有RDB和AOF两种持久化方式。
2.1 RDB-快照持久化
答:将内存中的数据集以快照形式写入磁盘,恢复时载入快照到内存。 Redies默认采用。
2.1.1 触发方式
- 自动触发:每隔多少秒,有多少数据发生变化,就自动触发持久化;
- 手动触发:bgsave命令,异步进行创建快照。
2.1.2 优缺点
- 优点:只有一个.rdb文件,恢复快,随时可以转移,性能好。
- 缺点:安全性差,没法实时持久化。(隔段时间持久化一次,频繁手动性能低)
2.1.3 原理
- fork:redis通过创建子进程来进行RDB操作
- cow:copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。
2.2 AOF-增量持久化
答:将redis执行的所有写命令记录到日志文件中,恢复时将AOF文件载入内存。一般采用每秒钟同步一次everysec的方式。默认关闭,appendonly yes 开启配置。
2.2.1 AOF重写
答:AOF文件随着服务器运行时间增长而越来越大,AOF重写能减小文件大小,而且数据库状态一致。
- 读取数据库现有的键值对状态,用一条命令替代之前对键值对操作的多条命令,再用bgrewriteaof实现重写。
- 在子进程中执行。redis会维护一个AOF重写缓冲区,在子进程创建AOF期间,记录写命令;当子进程重写完成后,服务器将重写缓冲区内的内容添加到AOF文件的末尾,以保持状态一致。
流程
- 从主进程中fork出子进程,并拿到fork时的AOF文件数据写到一个临时AOF文件中
- 在重写过程中,redis收到的命令会同时写到AOF缓冲区和重写缓冲区中,这样保证重写不丢失重写过程中的命令
- 重写完成后通知主进程,主进程会将AOF缓冲区中的数据追加到子进程生成的文件中
- redis会原子的将旧文件替换为新文件,并开始将数据写入到新的aof文件上
总结:用个缓冲区,暂时存操作,完成重写后,再添文件末。
2.2.2 优缺点
- 优点:安全性好,秒级持久化。
- 缺点:需要更多的IO资源,AOF文件也较大,恢复慢;
注:redis4.0后,支持RDB和AOF的混合持久化,RDB作为全量备份,AOF作为增量备份。
3. 过期策略和内存淘汰
Redis是键值型数据库,缓存的key总是会有过期时间,过期策略就是回收这些过期的key。
内存淘汰是在Redis缓存内存不足时,又有新写入,需要淘汰过期内存的策略。
注:过期回收针对过期key,内存淘汰针对内存不足。
3.1 过期回收策略
答:redis中的数据过期使用了定期删除和惰性删除相结合的方式。
- 定期删除:redis默认每隔100ms就随机抽取一定量的数据判断是否过期,过期就删除;
- 惰性删除:在获取一个key时,redis会检查这个key是否过期,若过期则删除。
如果定期删除漏掉很多过期key,用户也没及时去查,没用惰性删除,导致大量过期key积压在内存中,消耗资源,所以需要内存淘汰。
3.2 内存淘汰/保存热点数据
3.2.1 淘汰策略
答:redis提供了6种数据淘汰策略。
设置过期时间的key
- volatile-lru:用LRU算法移除设置了过期时间的key;
- volatile-ttl:移除设置了过期时间的有更早过期时间的key;
- volatile-random:随机移除设置了过期时间的key;
全局的key - allkeys-lru:内存不足时,用LRU算法移除任意key;
- allkeys-random:随机移除任意key;
- no-eviction:不移除任何key,只返回错误信息。默认。
3.2.2 回收流程
- 客户端执行新命令
- Redis检查内存使用情况,大于maxmemory则按设定的内存淘汰策略回收内存
- 继续执行命令
4. 事务
4.1 Redis事务
答:redis事务本质是通过MULTI、EXEC、WATCH和DISCARD等一组命令集合执行。执行过程是一次性的(不会中断一直运行),顺序性的(串行化执行),排他性的(不会在中间插入别的命令)。
4.2 相关命令
- MULTI:标记一个事务块的开始。
- EXEC:执行所有事务块内的命令。
- DISCARD:取消事务,放弃执行事务块内的所有命令。
- UNWATCH:取消 WATCH 命令对所有 key 的监视。
- WATCH key [key …]:监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
4.3 内部实现
- redis不支持回滚,不保证原子性,事务失败也继续执行。
- 单条命令是原子性,但整个事务不是原子性。
- 命令语法错误或操作类型不规范才停止执行。
5. 集群方案
5.1 主从复制机制
答:主从(Master-Slave)机制,主机以写为主,从机以读为主,有效避免单点故障导致的数据丢失,主从数据库的数据实时同步。
过程
- 从数据库启动,向主数据库发送同步请求;
- 主数据库收到后,进行RDB快照,并将快照过程中的收到命令缓存起来;
- 快照完成后,将.rdb文件和所有缓存的命令发给从数据库;
- 从数据库收到后,进行快照同步并执行收到的缓存命令。
5.2 哨兵模式
答:哨兵(Sentinel)模式,用于管理多个Redis服务器。哨兵至少需要3个实例才能保持健壮性。一般都使用哨兵 + 主从架构保证高可用。
5.2.1 功能
主要有三个功能:
- 监控:哨兵会不断检查主机和从机是否运行正常;
- 通知:当被监控的某个redis发生问题,哨兵会发送通知;
- 故障迁移:当主机不能正常工作,哨兵会选择主机的一个从机升级为主机,让其他从机改为复制新主机。旧主机复活时,将其变为新主机的从机,最后向客户端通知主机的变化。
5.2.2 节点下线
- 主观下线:哨兵认为此redis节点发生了故障,就主观下线该节点。通过心跳包检测实现。
- 客观下线:所有哨兵中的多数(>quorum)认为此redis节点主观下线,则该节点客观下线。
5.2.3 Leader选举
故障转移时,需要哨兵选出一个leader进行后续操作。
流程为:每个主管下线的哨兵向其他哨兵发出设置他为Leader的命令,当票数达到设定值时,称为领导者。若有多个哨兵当选Leader,则等待一段时间再选举。
6. 分布式锁
当多客户端并发操作Redis,可能出现并发竞争,需要分布式锁帮助管理顺序。
6.1 实现
- 用 setnx (set if not exists)争抢锁。key不存在就设为value,存在不做动作。
- 抢到后,用 expire 给锁加一个过期时间防止锁忘记了释放。
- 可以通过参数将setnx和expire合成一条指令。
6.2 举例
- 每个系统通过 Zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操作某个Key。
- 写入/查询MySQL时必须保存一个时间戳,每次都要判断当前 Value 的时间戳是否比缓存里的 Value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。
7. 缓存异常
7.1 缓存雪崩
答:缓存雪崩是指缓存同一时间大面积失效,这时有一波请求访问数据库,导致数据库崩掉。
解决方案
- 事前:保证redis服务器的高可用,发现宕机就立马补上,内部选择恰当的内存淘汰机制,设置随机的key失效时间(保证不会同时大面积失效);
- 事中:本地ehcache缓存 + 限流和降级处理,避免数据库崩溃;
- 事后:redis持久化尽快恢复缓存数据。
7.2 缓存穿透
答:故意请求不在缓存也不在数据库中的数据,导致大量请求直接打到数据库上,导致崩溃。
解决方案
- 参数校验:将不合法的参数请求直接抛异常。
- 缓存无效key:若缓存和数据库都查不到数据,就写一个到redis中并设置过期时间。
- 布隆过滤器:把所有可能的请求值放在布隆过滤器中,用户请求时先判断是否存在,不存在直接返回错误信息。
7.3 缓存击穿
答:缓存击穿是指缓存中没有但数据库中有的数据,同时大并发去数据库查询同一条数据,导致崩溃。比如缓存热点数据key失效后的处理。
解决方案
- 设置热点数据永不过期
- 对缓存查询加互斥锁。key不存在就加锁查DB写缓存,然后解锁;其他进程发现有锁就等待,等解锁后从缓存中拿数据。
7.4 缓存降级
答:缓存降级是指服务出现问题或非核心服务影响到核心流程的性能时,弃车保帅,保证核心服务可用,即使是有损服务。
解决方案
- 必须对系统进行梳理排序,从而确定可以降级的模块。
- 一般对不重要的缓存数据,可以直接返回默认值,而不去访问数据库。
7.5 双写一致性
答:高并发情况下很容易因为操作失败而导致数据不一致。
解决方案
- 强一致性
读写请求串行化,每次只能进行一个操作,保证数据库和缓存时刻相同。 - 最终一致性
数据库和缓存可以存在不一致的情况。
- 双删延迟:先删除缓存数据,再更新数据库数据,最后隔段时间再删除缓存。
- 原理:如果数据库更新失败,那么数据库中是旧数据,缓存中是空的,数据不会不一致。因为读的时候没有缓存,所以去读了数据库中的旧数据,然后更新到缓存中。