Java面试:Redis 概念 一个开源的使用 C 语言编写的支持网络,可基于内存也可持久化的日志型,key - value ,NoSQL数据库 数据存在内存中,间隔一段时间使用快照持久化备份数据到硬盘 Spring 项目中 注意点 整合了一个 RedisTemplate 配置类来访问 Redis 需要在 application.properties 中配置选择哪个数据库,ip,端口 自带的 RedisTemplate 的 key 是 Object 类型,影响使用,重写成了 String 类型 使用 点赞功能 原因 大量的点赞情况存在,需要较好的性能 具体使用 点赞存在 Redis.set 中,需要查询数量直接通过 size 方法即可 使用 isMember 方法,来查询是否点赞了 通过 opsForValue.get(theKey) 方法得到特定成员的点赞数 like 方法 在前端页面的按钮事件中被异步使用,通过 thymeleaf 实现 编写了一个 js 方法,用于提交相应的参数,并获得反馈 返回的参数绑定到子标签上,用于修改页面 点赞的状态在点在页面显示的时候会更新 在用户对其点赞后也会更新 都会查询点赞状态,放入对象 关注 具体 主页上的关注信息,由 UserController 的主页方法进行查询 使用场景 数据高并发的读写 海量数据的读写 对拓展性要求高的数据 “高性能”和“高并发” 通过 Redis 缓存实现 功能 数据缓存 分布式锁 数据持久化 事务与消息队列 为何是单线程 因为是基于内存的操作,所以Redis 的瓶颈 不是 CPU 而是内存或是带宽。可以采用单线程,容易实现 速度快 纯内存操作 单线程操作,避免了频繁的上下文切换 采用了非阻塞I/O多路复用机制 自己构建了 VM 机制,减去调用系统函数的时间 ps:非阻塞 IO 优点 速度快:数据存在内存中,查找时间发复杂度为 O(1) 支持丰富数据类型,支持string,list,set,sorted set,hash 支持事务,操作都是原子性 用于缓存,消息,按key设置过期时间 同样单线程举例: nginx ,nodejs 缓存 缓存雪崩 缓存同一时间大面积的失效 解决办法 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉 事后:利用 redis 持久化机制保存的数据尽快恢复缓存 plan B:通过加锁或队列保证不会有大量线程同时读写 缓存穿透 查询一个一定不存在的数据,不存在所以无法存入缓存,每次都需要去数据库查询,形成缓存穿透 解决方案:缓存这个空结果,给快速过期时间 并发竞争 Key 多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同 推荐一种方案:分布式锁 基于zookeeper临时有序节点可以实现的分布式锁 每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个 当释放锁的时候,只需将这个瞬时节点删除即可 缓存击穿 某个被频繁访问的 key 失效,导致大量请求击破缓存到数据库 解决方案 访问 key 之前,采用SETNX(set if not exists)来设置另一个短期 key 来锁住当前 key 的访问,访问结束再删除该短期 key。 缓存预热 系统上线后,将缓存数据直接加载到缓存系统 解决方案 写一个缓存刷新页面,上线时手动操作 项目启动时自行加载 定时刷新缓存 缓存更新 淘汰无用缓存 方案 定时清理过期缓存 维护起来较麻烦 请求时,根据缓存状态判断更新 逻辑较复杂 缓存降级 当访问量剧增或服务故障时,为保证服务可用,降级某些关键数据,不调用数据库 方案 不查询数据库,直接返回默认值 常见数据结构以及使用场景 String 简单的key-value类型 value其实不仅可以是String,也可以是数字 应用 常规计数:微博数,粉丝数等。 还可通过 mset 等指令批量获取 Hash string 类型的 field 和 value 的映射表 适合用于存储对象 后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值 List Redis最重要的数据结构之一 实现为一个双向链表,即可以支持反向查找和遍历 lpush:从左向右插入 应用 微博的关注列表,粉丝列表 简单的 消息列表 Set 与list类似 可以自动排重 有求交集,并集,差集的方法 应用 需要存储一个列表数据,又不希望出现重复数据 共同关注、共同粉丝、共同喜好 Sorted Set 增加了一个权重参数 score,使得集合中的元素能够按 score 进行有序排列。 应用 排行榜功能 内部结构 dict:字典 通过哈希表实现,每个节点表示字典的键值对 定义 哈希表:dictht dictEntry **table 哈希表数组 unsigned long size 哈希表大小 unsigned long sizemask 哈希表大小掩码 总是等于size-1 主要用于计算索引 unsigned long used 已使用键值对数 节点:dictEntry void *key 键 union v 值 void *val 值指针 uint64_t u64 int64_t s64 struct dictEntry *next 指向下个哈希表节点 用于防止hash值冲突 字典:dict dictType *type 代表何种类型的数据 void *privdat 私有 dictht ht[2] 当前一个负载过高 rehash 时使用 int rehashidx rehash目前进度 dictType 保存了操作特定类型的函数 hashFunction 计算哈希值 *(*keyDup) 复制键 *(*valDup) 复制值 *keyDestructor 销毁键 *keyCompare 对比键 sds:简单动态字符串 实现 以空字符’\0’结尾的字符数组 而非直接使用 C 字符串 用于存储二级制数据 定义 struct sdshdr int len buf已用字节数 int free buf未用字节数 char buf[] 内存管理 空间预分配 修改时,若未超过长度,则原空间操作 反之,按需修改 len 长度,并分配 min(1MB, new_len) 的空间给free字段 惰性释放 缩短后多出来的字节放在 free字段中,以备使用 skiplist:跳跃表 实现 单层多指针链表,查找效率高(堪比红黑树或平衡树) 元素过多或元素过长的有序集合键的基础 时间复杂度 O(logN) 定义 zskiplistNode level[] struct zskiplistLevel struct zskiplistNode *forward 前进指针 unsigned int span 跨度 struct zskiplistNode *backward 后退指针 double score 分数 robj *obj 成员对象 zskiplist struct zskiplistNode *header, *tail 指向跳跃表头和表尾 unsigned long length 表中节点数量 int level 跳跃表中除跳跃表头节点之外层数最大的节点的层数,最大为32 ziplist:压缩表 编码后列表,顺序型数据结构 定义 ziplist totalsize 保存当前ziplist所占用内存总量 offset 指向最后一个元素的位置 len 当前 ziplist中元素的个数 ZIP_END 标记ziplist的结束 zlentry prevrawlensize 前一个元素的内存大小的编码空间 prevrawlen 前一个元素的内存大小 lensize 当前元素value部分占用内存大小的编码空间 len 当前元素value部分占用内存大小 内存 模型 used_memory 划分 数据 统计在 used_memory 中 进程运行所需的内存 不由 jemalloc 分配,故不统计在 used_memory 缓冲内存 包括 客户端缓冲区 存储客户端连接的输入输出 复制积压缓冲区 用于部分复制功能 AOF缓冲区 用于在进行AOF重写时,保存最近的写入命令 由 jemalloc 分配,故统计在 used_memory 中 内存碎片 在分配、回收物理内存过程中产生 不会统计在used_memory中 支持的 Java 客户端 Redisson,Jedis,lettuce jedis 提供了比较全面的 Redis 命令的支持 redisson 实现了分布式和可扩展的 Java 数据结构,功能较为简单,不支持排序,事务,管道等 Redis 特性。 持久化 类型 RDB 概念 指定的时间间隔对数据进行快照存储 可自动或手动触发 优点 RDB 文件紧凑,体积小,网络传输快,适合全量复制 恢复速度快与 AOF 对性能影响较小 AOF 概念 每次写命令时追加数据到文件中 默认开启 RDB,关闭AOF,需要手动配置开启 优点 支持秒级持久化 兼容性好 缺点 文件大,恢复速度慢,对性能影响大 选择 若数据完全丢失可接受(Redis 为 DB 层的 cache),可不持久化 单机环境 可接受十几分钟的数据丢失,选择 RDB 只可接受秒级数据丢失,选择AOF 多数情况 多机,主从配置 分布式锁 缺陷 无法解决超时问题,超出锁的超时时间会出现问题 实现 使用SETNX命令实现分布式锁 再用expire给锁加一个过期时间防止锁忘记了释放 算法 Redlock 获取当前时间(start) 依次向 N 个 Redis节点请求锁 计算获取锁的过程总共消耗多长时间,若为超过锁的有效时间,记为获取成功 如果最终获取锁失败,客户端应该立刻向所有 Redis节点发起释放锁的请求。 异步队列 使用list结构作为队列,rpush生产消息,lpop消费消息 lpop没有消息的时候,要适当sleep一会再重试 缺点 在消费者下线的情况下,生产的消息会丢失 海量数据处理 SCAN系列命令(SCAN、SSCAN、HSCAN、ZSCAN)完成数据迭代 注意事项 SCAN的参数没有key,因为其迭代对象是DB内数据 返回值都是数组,第一个值都是下一次迭代游标 时间复杂度:每次请求都是O(1),完成所有迭代需要O(N),N是元素数量 淘汰策略 删除策略 定时删除 定时删除过期 key 优点 内存使用较好 缺点 CPU 占用过久 定期删除 隔一段时间删除一遍 优点 减少对 CPU 的影响 惰性删除 调用时判断是否过期,要不要删除 优点 占用 CPU 时间友好 缺点 内存占用过多 参数 volatile-lru:从已设置过期时间的数据集(server. db[i]. expires)中挑选最近最少使用的数据淘汰 volatile-ttl:从已设置过期时间的数据集(server. db[i]. expires)中挑选将要过期的数据淘汰 volatile-random:从已设置过期时间的数据集(server. db[i]. expires)中任意选择数据淘汰 allkeys-lru:从数据集(server. db[i]. dict)中挑选最近最少使用的数据淘汰 allkeys-random:从数据集(server. db[i]. dict)中任意选择数据淘汰 no-enviction(驱逐):禁止驱逐数据 集群方案 twemproxy 以一个代理的身份处理请求,使用一致性 hash 算法将请求转到具体的 Redis 缺点 节点改变时,数据无法自动移动到新的节点 codis 类似 twemproxy,支持在节点改变时自动移动数据 redis cluster 自带集群,分布式算法不是一致性 hash,而是 hash 槽。支持设置从节点 Redis事务 原语 MULTI 开启一个事务,它总是返回OK EXEC 执行所有事务块内的命令。返回事务块内所有命令的返回值 DISCARD 清空事务队列,并放弃执行事务 从事务状态中退出。 WATCH 监控一个或多个键,一旦其中有一个键被修改(或删除),之后的事务就不会执行, 监控一直持续到EXEC命令 将一个事务中的所有命令序列化,然后按顺序执行 特点 redis 不支持回滚,内部可以保持简单且快速 如果在一个事务中的命令出现错误,那么所有的命令都不会执行 如果在一个事务中出现运行错误,那么正确的命令会被执行 主从复制 作用 实现了多机备份,防止硬盘损坏导致的数据丢失 是高可用 Redis (哨兵和集群)的基础 完成对读操作的负载均衡 缺陷 故障恢复无法自动化 写操作无法负载均衡 存储能力受单机限制 哨兵 基于主从复制,实现了自动化的故障恢复 缺陷 写操作无法负载均衡 存储能力受单机限制 高级工具 慢查询日志 / 分析 系统在命令执行前后计算每条命令的执行时间,超出预设值则将相关信息(慢查询ID,发生时间戳,耗时,命令的详细信息)记入日志 性能测试 Pipeline 管道:用于批量执行命令 事务 Lua 自定义命令 Bitmaps 发布订阅 常用命令 管理 dbsize 返回当前数据库 key 的数量 info 返回当前 redis 服务器状态和一些统计信息 monitor 实时监听并返回redis服务器接收到的所有请求信息 shutdown 把数据同步保存到磁盘上,并关闭redis服务 config get parameter 获取一个 redis 配置参数信息 config set parameter value 设置一个 redis 配置参数信息 config resetstat 重置 info 命令的统计信息 包括:keyspace keyspace 命中数 ,错误数、 处理命令数,接收连接数、过期 key 数 debug object key 获取一个 key 的调试信息 工具 redis-server 服务器的 daemon 启动程序 redis-cli 命令行操作工具 redis-benchmark 性能测试工具,测试读写性能 redis-benchmark -n 100000 –c 50 模拟同时由 50 个客户端发送 100000 个 SETs/GETs 查询 redis-check-dump 本地数据库检查 其他 判断 key 是否存在 exists key +key名字 删除 key del key1 key2 … 问题 常见性能问题 主服务器写内存快照,会阻塞主线程的工作,当快照比较大时对性能影响是非常大的,会间断性暂停服务 主服务器最好不要写内存快照 Redis 主从复制的性能问题,为了主从复制的速度和连接的稳定性,主从库最好在同一个局域网内。 尽量避免在压力很大的主库上增加从库 如果数据比较重要,某个 Slave 开启 AOF 备份数据,策略设置为每秒同步一次 主从复制不要用图状结构,用单向链表结构更为稳定 为了主从复制的速度和连接的稳定性, Master 和 Slave 最好在同一个局域网内 如何在多机情况下保持数据一致 主从复制,读写分离 主数据库支持读写,写的时候同步数据到从数据库 从数据库只读 只有一个主数据库,可有多个从数据库 如何优化内存 尽量使用散列表 大量请求 IO 多路复用 为什么Redis的操作是原子性的,怎么保证原子性的 Redis的操作之所以是原子性的,是因为Redis是单线程的 如何保证缓存和数据库的数据一致 合理设置缓存过期时间 对数据库操作时同步更新 Redis,利用事务机制实现 增加缓存失败后的重试机制 与 memecache 的区别 后者的值都是简单的字符串,前者支持更丰富的数据类型 速度还是 Redis 快 Redis 可以持久化数据 Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。 如何解决并发竞争 使用队列将并发访问变成串行访问 没有锁,使用 setnx 实现 通讯协议 RESP 客户端和服务端之间使用 特点:实现简单、快速解析、可读性好