简介
- 开源的内存键值对存储数据库。
- 支持数据结构:字符串、哈希表、列表、集合、有序集合等。
- 特点:高性能、高可用、可扩展、可持久化。
- 应用场景:缓存、分布式锁、消息队列、统计系统等。
数据操作
- 键的类型:字符串
- 值的类型:字符串、哈希表、列表、集合、有序集合等。
字符串
- 写入
- set key value,设置键值
- setnx key value,设置键值,如果键已存在时设置失败返回0,否则返回1。
- setex key seconds value,设置键值,过期时间(秒)
- mset key value [key value ...],批量设置键值
- incr key,自增,如果无法解析为整数将报错。
- incrby key increment,加上整数。
- decr key,自减,如果无法解析为整数将报错。
- decrby key decrement,减去整数。
- append key value,追加字符串到末尾,如key不存在等于set。
- del key [key ...],删除键。
- expire key seconds,设置过期时间(秒)。
- ttl key,查看剩余过期时间(秒)。
- 读取
- get key,获取键值
- mget key [key ...],批量获取键值。
- strlen key,获取长度。
- keys pattern,查找键,支持正则。
- exists key [key ...],判断键是否存在,存在1,不存在0。
- type key,查看 value 类型。
哈希表
- 写入
- hset key field value,设置哈希表字段值。
- hmset key field value [field value ...],批量设置哈希表字段值。
- hdel key field[field ...],删除哈希表字段。
- 读取
- hget key field,获取哈希表字段值。
- hmget key field [field ...],批量获取哈希表字段值。
- hgetall key,获取哈希表所有字段和值。
- hkeys key,获取哈希表所有字段。
- hvals key,获取哈希表所有值。
- hlen key,获取哈希表字段数量。
- hexists key field,判断哈希表字段是否存在。
列表
- 写入
- lpush key value [value ...],将值插入列表头部。
- rpush key value [value ...],将值插入列表尾部。
- lpop key,移除并返回列表第一个元素。
- rpop key,移除并返回列表最后一个元素。
- lset key index value,设置指定索引的元素值。
- ltrim key start stop,裁剪列表,仅保留子集。
- 读取
- lrange key start stop,获取列表指定范围内的元素。
- lindex key index,获取指定索引的元素。
- llen key,获取列表长度。
集合
- 写入
- sadd key member [member ...],将一个或多个成员元素加入到集合中,已经存在将忽略。
- 读取
- smembers key,获取集合所有成员。
- scard key,获取集合成员数量。
- sismember key member,判断 member 元素是否是集合 key 的成员。
- sinter key [key ...],获取多个集合的交集。
- sdiff key [key ...],获取某个集合与其他集合的差集。
- sunion key [key ...],获取多个集合的并集。
有序集合
- 写入
- zadd key score member [score member ...],将一个或多个成员元素及其分数值加入到有序集 key 中,如果成员已经存在,将更新其分数值。
- 读取
- zrange key start stop [withscores],获取有序集 key 中指定区间内的成员。
- zcard key,获取有序集 key 的成员数。
- zscore key member,返回有序集 key 中,成员 member 的分数值。
- zrank key member,返回有序集 key 中成员 member 的排名。
- zcount key min max,计算在有序集 key 中,指定闭区间分数范围的成员数量。
实现原理
高性能的关键
- 内存数据库的特点,cpu占用少,瓶颈在网络IO。
- 高并发的基础:IO多路复用,linux的epoll。
- 命令执行采用单线程模型串行化,避免上下文切换和加锁。
- 4.0版本之前网络IO和命令执行都是在一个线程中完成。
- 4.0版本开始支持网络IO的多线程,命令执行单线程。
字符串存储
- int,字符串长度小于等于12字节且可以表示为整数时,采用int类型。
- embstr,字符串长度小于40字节时,数据内嵌存储。
- raw,字符串长度大于等于40字节时,数据存储到另外一个结构体中。
- 字符串值最大容量512M。
哈希表存储
- ziplist,元素较少时使用压缩列表,节省内存占用,线性查找。
- hashtable,元素较多时使用哈希表。
列表存储
- ziplist,元素较少时使用压缩列表,节省内存占用。
- linkedlist,元素较多时使用双向链表。
持久化机制
- RDB:定期写入,基于快照恢复,数据丢失。
- AOF:每次追加,基于日志重放恢复。
- 同步策略:
- 每次写操作都追加,安全性最高,但性能差。
- 每秒写入。
- 同步策略:
- RDB AOF:混合实现。
内存淘汰策略
- 何时触发:超过最大内存限制时。
- 淘汰范围:
- allkeys所有键
- volatile存在过期时间的键。
- 淘汰算法:
- LRU:最近最少使用,淘汰最长时间未使用的数据。
- LFU:最不经常使用,淘汰使用次数最少的。
- ttl:剩余时间最短的先淘汰。
- 随机:随机淘汰。
应用场景
缓存
缓存的更新策略
- 内存淘汰,内存不足时淘汰数据。
- 超时剔除,ttl 时间到期删除。
- 主动更新,应用中数据库写入同时更新缓存。
- 根据场景选择更新策略:
- 低一致性或固定数据,使用内存淘汰。
- 高一致性或频繁更新的数据,使用主动更新,超时剔除做备用方案。
双写一致性
- 主动更新需确保双写一致性。
- 先修改数据库,再删除缓存。
- 如果先删除缓存,再修改数据库,高并发时可能存在一致性问题。
- 强一致性:分布式事务处理写数据库与缓存。
- 最终一致性:消息队列异步更新,缓存更新失败重试机制。
- 如果数据库读写分离,需要延迟更新缓存避免读取到旧数据。
缓存穿透
- 定义:数据不在缓存也不在数据库中,导致每次都会查数据库。
- 问题:如果利用该漏洞高并发穿透,可能导致数据库崩溃。
- 解决:
- 缓存空置,设置较小ttl。
- 使用布隆过滤器,可以用极少内存代价,避免不需要的查询。
布隆过滤器
- 定义:空间效率极高的概率型数据结构,检测元素是否在一个集合里。
- 概率:有可能有,无肯定无。
- 原理:
- 结构由初始值0的位图数组和n个哈希函数组成。
- 每次通过n个哈希取模,映射到位图上。
- 写入时标记1,读取是判断n个位置是否都为1,不支持删除。
- 使用:4.0后提供的功能,插件形式,需配置开启。
- 命令:
- bf.add key value,将元素添加到布隆过滤器。
- bf.exists key value,判断元素是否在布隆过滤器中。
- bf.reserve key error_rate initial_size,设置布隆过滤器参数。
- error_rate是错误率,默认 0.01,即百分之一。
- initial_size是初始化大小,默认100。
- 初始化大小尽可能满足实际长度,否则会导致错误率上升。
- 当超过容量时,扩容需要将历史数据再添加一遍。
- 当 key 存在时不可设置。
- 错误率越低、元素数越多,占用内存越大。
缓存击穿
- 定义:某个热点key过期,大量请求同时访问该key,导致数据库压力剧增。
- 解决:
- 互斥锁,缓存未命中时,先获取锁,获取成功则读取数据库,获取失败则等待后再读取缓存。
- 逻辑过期,在物理过期前先逻辑过期刷新数据。
- 预热,在系统启动时,提前加载缓存。
- 热点数据延长过期时间。
缓存雪崩
- 定义:同时大量请求无法在redis处理,导致数据库压力剧增直至崩溃。
- QPS:redis几W,数据库几K,量级差10倍。
- 解决:
- 避免大量缓存同时过期:
- 微调过期时间,TTL添加随机值(1到3分钟)。
- 服务降级,非核心业务缓存失效返回预定义信息。
- 避免缓存实例故障:
- 健康检查,熔断或限流。
- 利用集群提高可用性。
- 避免大量缓存同时过期:
分布式锁
- 基本方法
- 获取锁:
- 先 setnx 再 expire。
- 原子操作:set key value ex seconds nx。
- redis 2.6.12 版本开始支持用 set 设置过期时间和实现 not existed。
- 释放锁:del命令
- 获取锁:
- 误删问题:
- 原因:多线程中,如果某线程加的锁被超时释放锁,则可能释放到其他线程的锁。
- 解决:锁的值存入 UUID,释放锁时先判断锁标识,再释放锁。
- 极端情况:判断锁标识与释放锁非原子性仍然可能导致误删,lua脚本可以解决原子性问题。
- 可重入锁:
- 定义:同一个线程外层加锁,内层再次加锁不会阻塞,加锁多少次就需要解锁多少次。
- 实现:需使用计数器,lua脚本可以实现。
- 实操:
- go 可以使用 redsync 实现 redis 分布式锁。
- java 可以使用 redisson 实现 redis 分布式锁。
消息队列
- 使用list:
- 基本操作:lpush 生产者入队,rpop 消费者消费。
- 阻塞消费:brpop,阻塞等待。
- 缺点:只支持单消费者。
- 使用发布订阅:
- 基本操作:subscribe 订阅,publish 发布。
- 优点:支持多消费者,支持广播。
- 缺点:不支持消息持久化,消息无法保证顺序。
- 一般不建议使用。
- 使用stream:
- 5.0 版本起支持的功能。
- 基本操作:xadd/xread等。
- 具备消息中间件的基本能力。
- 局限性:
- 消息丢失:redis宕机,主从切换未完成同步时。
- 消息积压时消耗内存。
- 应用:适用于消息丢失不敏感且消息积压概率小的情况,否则使用专业的消息队列中间件。
集群架构
集群作用
- 提高可用性,避免单点故障。
- 提高吞吐能力。
集群模式
主从读写分离
- 主从复制原理:
- 采用异步方式。
- 全量复制:将 RDB 发给从节点。
- 增量复制:从节点向主节点请求增量数据。
- 哨兵机制:监控与故障恢复,将从节点提升为主节点。
分片集群
- 将数据分散到多个节点上,各个节点再实现主从。
- 哈希槽:将key映射到16384个槽上,再分配到不同节点上。
- 支持故障转移和数据迁移。