Redis
一、基础#
1 什么是Redis#
- redis是一个将数据存储在内存中的数据库,读写速度非常快,存储的是键值对
- redis支持数据的持久化,可以将内存中的数据保存到磁盘中,重启的时候可以再次加载进行使用
- redis不仅支持简单的kv类型的数据,还提供list,set,zset,hash等数据结构的存储
- redis支持数据的备份,即master-slave模式的数据备份
2 Redis为什么这么快#
- redis完全基于内存
- redis有高效的事件处理模型,主要是单线程事件循环和IO多路复用
- 单线程可以避免了上下文切换和竞争,不需要锁
- 是多路IO模型,非阻塞IO(阻塞IO会一直占用CPU,多路复用IO单个线程就可以同时处理多个IO请求)
- redis内置了多种优化过后的数据结构实现,性能非常高
3 为什么要用Redis#
- 高性能
Redis和传统数据库存在磁盘中相比,直接使用内存,速度非常快 - 高并发
MySQL能达到一万QPS,
Redis能达到十万QPS到三十万QPS
3.1 redis适合的场景#
- 缓存:减轻MySQL查询压力,提高性能
- 排行榜:利用redis的SortSet(有序集合)实现
- 计数器:利用Redis的自增操作,可以统计点赞数、页面访问数
- 限速器:
- 好友关系:利用集合的一些命令,求交集、并集、差集,解决共同好友、共同爱好问题
- 消息队列:用list完成异步解藕
- session共享:客户登录任意一台机器都可以获得session信息
3.2 redis常见的功能#
- 支持数据缓存
- 支持分布式锁
- 支持数据持久化
- 支持事务(不满足原子性和持久性)
- 支持消息队列(异步处理,应用解耦,流量削峰和消息通讯),一般没人用
二、内存#
1 设置过期时间#
- 设置过期时间减少内存消耗,绝大部分数据不需要一直保存
- 传统的数据库判断数据是否过期性能非常差
- 使用expire设置过期时间,字符串使用setex设置过期时间,persist移除一个键的过期时间,ttl查看还有多久过期
2 Redis如何判断数据是否过期#
过期字典
- 过期字典保存了数据库中所有键的过期时间
- 过期字典的key是一个指针
- value是long long类型,保存了该键过期时间
3 过期数据的删除策略#
- 定时删除
- 在设置键的过期时间的同时,创建一个定时器,通过定时器执行对键的删除操作
- 内存友好,对CPU时间不友好(执行删除占用时间)
- 惰性删除
- 每次从键空间获取键时,都检查是否过期,过期就删除
- CPU时间友好,内存不友好
- 定期删除
- 每隔一段时间,就对数据库进行一次检查,删除其中的过期键
- 通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响
4 内存淘汰机制#
- volatile-lru:从已设置过期时间的数据中最少使用淘汰
- volatile-ttl:从已设置过期时间的数据中挑选将要过期的数据淘汰
- volatile-random:从已设置过期时间的数据中随机淘汰
- allkeys-lru:从全部数据中最少使用淘汰
- allkeys-random:从全部数据中任意淘汰
- 禁止驱逐数据
5 内存碎片#
- redis存储存储数据的时候向操作系统申请的内存空间可能会大于数据实际需要的存储空间。
- 当redis中某个数据删除时,通常不会轻易释放内存给操作系统。
- info memory查到内存碎片率大于1.5需要清理碎片
- 碎片清理时,设置内存碎片清理所占用CPU时间的比例
三、性能优化#
1 性能瓶颈#
- 网络
- 内存
2 内存优化#
- 降低key和value的大小
- 维护共享整数对象池
- 选择合适的底层存储结构
2.1 如何解决大Key的问题#
- 定义:一般来讲string的value大于10KB的就是大key,其他类型field超过一万个
- 影响:
- 单线程,耗时增加会阻塞其他请求
- 大key导致分片内存不平衡,CPU使用率不平衡
- 查看:
- 看单分片监控,是否和平均一致
- bigkeys命令
- 解决:主要是业务角度
- 删除机制
- 做拆分,大key变小key
- 是否key设计不合理,比如用的不是随机值而是固定值
2.2 设置ziplist和hashtable#
- 区别
- ziplist是双向链表,占用空间比hashtable小,查询耗时比hashtable长
- hashtable是散列表,占用空间比ziplist大,查询耗时比hashtable短
- 使用ziplist的条件
- 又value字符串长度小于64(线上是8192)
- 又field-value对的数量小于512个
- 查看底层存储的方法
OBJECT ENCODIN
3 网络优化#
3.1 网络优化方法#
核心:使用批量操作
- 原生命令:hmget、hmset
- 问题1:无法保证所有的 key 都在同一个hash slot,仍需要多次网络传输(总体来说还是优化)
- 问题3:命令只能一种,都是set或者get这种
- 优点:可以保证原子操作
- pipeline:
- 问题一:需要控制一次批量操作的元素个数
- 问题二:也无法保证所有的 key 都在同一个hash slot
- 问题三:非原子操作
- 优点:可以使用多种命令
- lua:
- 问题:redis-cluster下无法保证原子性(也是哈希槽的问题)
- 优点一:支持简单逻辑处理
- 优点二:非redis-cluster可以保证原子性
- 并且一段lua执行过程中,不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。
四、线程模型#
1 单线程模型#
Redis 基于 Reactor 模式开发了自己的网络事件处理器,被称作文件事件处理器
- 文件事件处理器使用I/O多路复用,来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
- 当被监听的套接字准备好执行连接、读取、写入、关闭等操作时,与操作相对应的文件事件就会产生。这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,实现了高性能的网络通信模型。
I/O 多路复用技术能让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗。
单线程优点:
- 容易编程,且易于维护
- Redis瓶颈不在CPU,而在内存和网络
- 多线程可能会存在死锁、线程上下文切换问题,影响性能
2 新版本多线程#
- Redis4.0,在对一些大键值对的删除操作的命令时,会异步删除。
- Redis6.0,针对提高网络 IO 读写性能,使用多线程。
执行命令仍然是单线程顺序执行。
五、高并发#
1 高并发场景下使用缓存需要注意哪些问题#
- 缓存一致性问题
需要保证缓存中的数据与数据库中的一致,需要选择业务适合的缓存过期和更新策略。一般在数据发生更改的时,主动更新缓存中的数据。 - 缓存击穿问题
在某个高访问量的key过期时,大量的请求会直接落到数据库中。解决方法是加载缓存时采用互斥锁,保证只有一条请求落到数据库,其他请求先自旋,然后查询缓存,最后再申请锁。 - 缓存穿透问题
一些没有value的key被频繁查找,可以通过在缓存中缓存空对象来缓解。 - 缓存雪崩问题
大量的缓存同时失效或过期。解决方式:限流、降级、熔断、多级缓存。比如在设置过期时间时,合理增加随机性。
2 高并发下使用规范#
2.1 键值设计#
- key设计
- 有随机性,不要用固定值做key(防止哈希槽冲突)
- 以业务名为前缀,用冒号分隔
- 缩短key的长度
- 不包含特殊字符
- value设计
- 业务阻止大key(string类型10kb以内,其他元素小于5000)
- 选择合适的数据类型(比如使用hash拆分)
- 设置过期时间
2.2 命令使用#
- 尽量使用批量操作降低网络耗时(pipeline或者hgetall)
- 使用批量操作注意个数
- 禁止使用keys、flushall、flushdb,用rename禁用
- 尽量避免使用redis的事物(不支持回滚、集群版key必须在同一个slot里)
2.3 使用规范#
- 避免多个应用使用同一个Redis实例
- 使用连接池,控制连接数,提高效率
- 添加熔断机制
- 选择合适的内存淘汰策略(allkeys-lru),设置过期时间
六、持久化#
- 防止系统故障
- 重启机器之后数据还在
- 备份到其他位置
1 RDB#
快照
1.1 什么是RDB#
- 快照持久化是默认的持久化方式
- redis通过创建快照,来获得存储在内存里面的数据,在某个时间点上的副本。
1.2 RDB 创建快照时会阻塞主线程吗?#
- bgsave是默认命令,会fork出一个子进程,子进程创建快照,不会阻塞主线程
- save是同步保存操作,会阻塞主线程
2 AOF#
只追加文件
2.1 什么是AOF#
- 开启AOF持久化后,每执行一条更改数据的命令,会将该命令写入到内存缓存,根据配置决定何时将其同步到硬盘中的AOF文件
- 与RDB相比,AOF持久化的实时性更好
2.2 AOF是如何实现的#
aof会在执行完命令之后才记录日志。
优点:
- 这样可以避免额外的检查开销
- 命令执行完之后再记录,不会阻塞当前的命令执行
缺点:
- 执行完命令还没有AOF就宕机,会导致对应的修改丢失
- 会阻塞后续其他命令的执行(AOF记录日志是在Redis主线程中进行的)
2.3 AOF重写#
- 含义和条件
当 AOF 变得太大时,Redis 能够在后台自动重写 AOF 产生一个新的 AOF 文件,这个新的 AOF 文件和原有的 AOF 文件所保存的数据库状态一样,但体积更小。 - 实现方式
这个功能是通过读取当前的数据库状态来实现的 - 可能发生的问题和解决方式
- 问题1:创建新AOF时会出现进行大量写操作,主线程被长时间阻塞,无法处理其他命令
- 问题1的解决方案:将AOF放到子线程执行
- 问题2:在后台AOF重写时,服务器会对当前数据进行修改,会导致数据不一致
- 问题2的解决方案:使用AOF重写缓存。完成AOF文件重写后,将AOF重写缓存中的内容全部写入到新的AOF文件中,之后,改名覆盖旧的AOF文件
3 如何选择RDB和AOF#
3.1 RDB优点#
- RDB比AOF更小,适合做灾备
- 使用 RDB 文件恢复数据,直接解析还原数据即可,不需要一条一条地执行命令,速度非常快。
3.2 AOF优点#
- AOF实时性比RDB更好
- RDB在产生的时候会对CPU和资源影响比较大
- AOF易于理解,可以轻松导出进行分析
4 混合持久化#
- 结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。
- 默认关闭,需要通过配置项开启
七、集群/哨兵/主从#
八、应用#
1 分布式锁#
- 用lua脚本,先判断setnx是否结果为1,获取到了锁,再立即执行expire设置一个合理的超时时间,保证原子性。
- 同时在此线程开启守护线程,给锁续期。
- 能确保真的拿到锁,也可以避免线程异常结束,锁未被释放的情况。
2 购物车#
购物车信息一般用Hash存储,因为购物车中的商品频繁修改和变动
- 用户id为 key
- 商品id为field,商品数量为value 普通维护
- 用户添加商品就是往 Hash 里面增加新的 field 与 value;
- 查询购物车信息就是遍历对应的 Hash;
- 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
- 删除商品就是删除 Hash 中对应的 field;
- 清空购物车直接删除对应的 key 即可。
3 排行榜#
sorted set经常被用在排行榜上。
常见的命令有:ZRANGE (从小到大排序) 、 ZREVRANGE (从大到小排序)、ZREVRANK (指定元素排名)。
4 抽奖系统#
使用set
SPOP key count: 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。SRANDMEMBER key count: 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。
5 活跃用户#
使用bitmap
使用日期(精确到天)作为 key,然后用户ID为offset,如果当日活跃过就设置为 1。
….
6 页面访问量#
- 用
PFADD将访问指定页面的每个用户 ID 添加到HyperLogLog中。 - 用
PFCOUNT统计指定页面的 UV。
九、数据结构#
1 有哪些数据结构#
五种基础数据结构
- String:字符串类型,可以存储字符串、整数和浮点数。
- List:有序列表类型,可以存储多个元素,每个元素可以是字符串或者数字。底层是双向链表
- Set:集合类型,可以存储多个元素,每个元素必须是唯一的。底层是哈希表
- Hash:哈希类型,可以存储多个字段和对应的值,每个字段和值都是字符串类型。key field value
- Zset:有序集合类型,可以存储多个元素,每个元素可以关联一个分数,用于排序。底层实现是跳跃表和哈希表
三种特殊数据结构
HyperLogLogs(基数统计)
用概率统计数组中不重复的元素个数。用非常少的内存,存储非常多的数据,误差大概0.81%。
PFADD将value存进key中PFCOUNT返回该key的近似基数
Bitmap (位存储)
Geospatial (地理位置)。
2 String#
3 Hash#
3.1 Hashtable和ziplist#
4 ZSet#
4.1 跳跃表#
- 通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
- 跳跃表在链表的基础上增加了多级索引以提升查找的效率,是一个空间换时间的方案。
- 当节点本身比较大或者元素数量比较多的时候,空间的缺点可以忽略。
- Redis使用跳跃表作为有序集合键的底层实现之一,如果一个有序集合包含的元素数量比较多,又或者有序集合中元素的成员是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。
5 List#
6 Set#
十、事物#
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 bc's club!