本笔记主要涉及学习 redis 过程中做的一些笔记,包含最初比较浅显的了解(所以会有一些比较简单的笔记描述),到后面对其应用的逐渐学习和补齐。其中主要参考书籍为 《redis in action Josiah L. Carlson》其他参考博客也会注明。
redis
文章目录
定义:
Redis (Remote Dictionary Server) is an open-source in-memory data structure project implementing a distributed, in-memory key-value database with optional durability.
Redis 作为一种远程缓存服务,可以帮助DB抗一些请求来做高性能的数据查询
redis 为什么这么快?
-
完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。
-
数据结构简单,对数据操作也简单。Redis 中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis 正是依赖这些灵活的数据结构,来提升读取和写入的性能。
-
采用单线程,省去了很多上下文切换的时间以及 CPU 消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。
-
使用基于 IO 多路复用机制的线程模型,可以处理并发的链接。
存储的数据类型
数据类型 | 底层数据结构 | 应用 | 备注 |
---|---|---|---|
string | simple dynamic string or long(数字) | 粉丝数、关注的人数 | |
list | ziplist or linkedlist(双向无环链表) | 关注列表、粉丝列表 | |
hash | ziplist or dict | 存储对象 | |
set | intset(都是数字) or dict | 共同喜好、各自的喜好等 | |
sorted set | ziplist or skiplist+dict | 最近访问的服务、排行榜等 |
List: 按照插入的顺序排序的字符串链表。和数据结构中的普通链表一样,可以在其头部(left)和尾部(right)添加新的元素。在插入元素时,如果该键不存在,Redis将为该键创建一个新的链表。如果链表中所有的元素均被移除,那么该键也会从数据库中删除。
redis 关键技术
事件循环
redis整个是一个单线程服务。启动后即陷入巨大的while循环,不停地处理文件事件和时间事件。
- 文件事件: 在多个客户端中实现多路复用,接受它们发来的命令请求,并将命令的执行结果返回给客户端。【即响应请求】
- 时间事件:记录那些要在指定时间点运行的事件,多个时间事件以无序链表的形式保存在服务器状态中 【redis为了维持其作为数据库的状态进行的一些定时任务】
- 关闭、清理失效的客户端连接
- 检查是否需要RDB dump,AOF重写 Redis持久化 - RDB和AOF 进程和线程的区别介绍
- 数据库后台操作,key过期清理、数据库rehash等
整个流程是这样的:
beforeSleep -> epollwait -> 处理请求 -> 定时事件 ->xx
建议: 减少大 key,减少耗时命令。
beforeSleep的执行频率一般比定时事件更频繁一些。主要做以下几件事:
- cluster集群状态检查,ok->fail、fail->ok
- 处理被block住的的client,如一些阻塞请求BLPOP等
- 将AOF buffer持久化到AOF文件
逐出
redis是一个内存服务,会设定内容上限的。
逐出 - 当执行write但内存达到上限时,强制将一些key删除
- allkeys - 所有key
- volatile - 设置了过期的key
- LRU - Least Recently Use 最近最少使用
- LFU - Least Frequently Used,最不常用(4.0 引入)
- random - 随机
- ttl - 最快过期的
特点:
- 不是精准算法,而是抽样比对
- 每次写入操作前判断
- 逐出是阻塞请求的
建议:关注逐出qps,过高会影响正常请求处理
过期
过期 - 当某个key到达了ttl(time to live)时间,认为该key已经失效
两种方式:
惰性删除 - 读、写操作前判断ttl,如过期则删除
定期删除 - 在redis定时事件中随机抽取部分key判断ttl
特点
并不一定是按设置时间准时地过期
定期删除的时候会判断过期比例,达到阈值才退出
建议:打散key的过期时间,避免大量key在同一时间点过期
持久化
redis虽然作为一个缓存存在,通常作为业务和DB之间的一个衔接,如果只保存在内存中,不进行持久化机器宕机之后就会造成数据的丢失。
ps: 内存和磁盘有什么样的区别??
持久化 - 将内存中的数据dump到磁盘文件
-
RDB持久化(一次性写入)
- 经过压缩的二进制格式
- fork子进程dump可能造成瞬间卡顿
-
AOF持久化(总是在写入追加,因此这个文件会越来越大,因此也就有了AOF重写)
- 保存所有修改数据库的命令
- 先写aof缓存,再同步到aof文件
- AOF重写,达到阈值时触发,减小文件大小(利用替换的策略)
应用:利用AOF文件灾备
- 可将数据恢复到最近3天任意小时粒度
主从复制(异步操作:写入成功即成功)
主从模式
- 主、从节点都可以挂从节点
- 最终一致性
全量同步
- 传递RDB文件&restore命令重建kv
- 传递在RDB dump过程中的写入数据
部分同步
- 根据offset传递积压缓存中的部分数据
- 注:每一个master上都有对应的slave的output缓存区
- 注:如果slave向master请求的offset不在积压队列中,那么就会发起一次全量的同步
pipeline
优点:
- 节省往返时间;
- 减少了proxy、redis server的IO次数
mget
- client: 使用mget命令
- redis: 一个命令中处理多个key;等所有key处理完后组装回复一起发送
- twemproxy:拆key分发到不同redis
- server;需要等待、缓存mget中全部回复
优点:
- 节省往返时间
缺点:
- proxy缓存mget结果;
- mget延时是最后一个key回复时间,前面的key需要等待
建议:利用pipeline代替mget,且控制一次请求的命令数量(建议50以内)(因为proxy做分发也会产生一定的压力)
注:
和pipeline的区别
- pipeline处理完一个请求即返回
- mget 等所有key处理完后组装回复一起发送
redis集群:一致性 hash & redis cluster
一致性hash:
- 实例宕机、加节点容易造成数据丢失
- 注:如果要做水平扩容,即增加redis实例,不会对原有的数据进行搬迁,改变拓扑会造成原有的key miss掉
redis cluster(涉及缓存的业务尽量用这种集群方式):
- 节点之间两两通信,有节点数量上限
redis 客户端: 服务发现
什么是服务发现
服务发现即在微服务场景下,将容器应用部署到集群时,其服务地址是由集群系统动态分配的。那么,当我们需要访问这个服务时,如何确定它的地址呢?这时就需要服务发现(Service Discovery)有客户端发现和服务端发现(Kubernetes 和 Marathon 这样的部署环境会在每个集群上运行一个代理,将代理用作服务端发现的负载均衡器。客户端使用主机 IP 地址和分配的端口通过代理将请求路由出去,向服务发送请求。代理将请求透明地转发到集群中可用的服务实例。)
redis 客户端: failover
redis 延迟删除&双机房删除
- 为什么延迟删除
解决 DB 和 cache 数据不一致的问题 - 产生原因
请求回源时,DB 主从延迟导致用 DB 的从节点的老数据更新了 cache - 解决方案
cache 延迟多次删除,当前删除一次,过几秒(大于 DB 主从延迟)后再删除一次
redis 分布式锁
references:
Redis setnx 原子操作:
最近做业务用到了消息队列,一般 mq 会保证 at least once, 但随之而来的问题就是有可能出现重复消费的现象,业务方需要做消费幂等。此时可以利用 redis 来做消费幂等。
references:
- redis 并发问题(setnx 事例)
- 消息队列三:消息重复消费问题(幂等性)
- kafka-重复消费-2(此文档中唯一 id 使用到了 offset 是合理的,因为从原理上看每条存在 broker 的消息都有 offset,并且不会发生改变,因此 consumergroup+topic+partition+offset 可以用于标识一条消息)
- 海量订单产生的业务高峰期,如何避免消息的重复消费?
问题背景:
-
原子性:如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。 原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,即要么一起成功要么一起失败。将整个操作视作一个整体是原子性的核心特征。
-
由于 Redis 是单线程的,因此每一个指令都是原子性的,但这不意味着 redis 的事务就是原子性的,其不会回滚,即只保证了一致性和隔离性,不能保证原子性和持久性。
我在一开始使用时用 exists 判断 key 是否存在,然后 set 值,这样不能保证原子性,
但还好 redis 提供了 setnx 操作,可以检测不存在时再存入,同时可以用 set 指令添加参数。否则返回 0,把说明其他进程已经获得了锁,通过这种方式则可以实现「锁」的机制。
基于 setnx 实现方式:
总结为以下几点:
- set 设置 setnx 以及过期时间(以保证没有显式的释放锁的场景,比如某线程获得了锁之后,执行任务的过程中挂掉,那么就没办法显式的执行 del 命令释放锁),同时注意锁的粒度
- 用完锁之后解锁:
del key
- 如果手动释放锁没成功,这时候就依靠之前设置的过期时间来保证解锁
- 如果锁到期了但程序没执行完,可以给获得锁的线程设置守护线程,在锁快过期的时候自动续期
缺点:
- 当 redis 是单点的情况下且发生了故障,则整个业务的分布式锁都将无法使用。
- 为了提高可用性,我们可以使用主从模式或者哨兵模式,但如果在加锁的过程中 master 节点故障那么同步失败,slave 变成的新 master 节点没有相关信息造成锁丢失,会导致多个客户端可以同时持有同一把锁的问题。
基于多个 Redis 集群部署的高可用分布式锁解决方案:RedLock:
redis 官方给出了基于多个 redis 集群部署的高可用分布式锁解决方案:RedLock
- TODO
应用场景:
- 当我们使用 k8s 部署服务(service)时,理所当然会在不同工作节点上存在 service 的副本,当我们使用 Redis 做分布式锁的时候怎么保证每个节点的 redis 保存着互通的数据呢,必然是虽然服务有多个副本但数据库直接放在一个 pod 中,或者数据库直接是集群模式。
乐观锁和悲观锁
乐观锁
概念:很乐观,认为什么时候都不会出问题,所以只在更新数据的时候判断一下当前数据有没有被别人变更过。
redis 相关:redis watch 命令+ multi exec 事务就是一个乐观锁,如果 watch 到数据没有变化就执行事务,否则就直接返回错误。
适用场景:写比较少,也就是冲突比较少的情况
悲观锁
概念:很悲观,认为什么时候都会出问题,所以无论什么时候都要加锁(在写之前加锁,写数据的时候其他线程就不会对数据进行修改)。传统的关系型数据库中就用到了比较多悲观锁机制,比如行锁、表锁等
适用场景:写入操作比较频繁的场景。
redis 发布与订阅
关于 publish 和 subscribe 命令
这是 redis 实现发布与订阅的最简单的方式,但是也有其局限性:
- 如果读取消息不够快,就会积压消息导致 redis 速度变慢最终崩溃,新版 redis 中有 client-output-buffer-limit pubsub 配置选项来解决这个问题
- 如果客户端在执行订阅期间断线就会丢失断线期间发送的所有消息,简单讲就是无法持久化
list LPUSH+BRPOP 或者 基于Sorted-Set的实现
可以参考 《redis in action》pull messaging
使用这种方式能够实现消息的持久化,书中也提到了实现多播的方式
redis stream
记一次redis stream数据类型内存不释放问题 🌟🌟🌟🌟🌟
Redis Stream类型的使用🌟🌟🌟🌟🌟
把Redis当作队列来用,真的合适吗?🌟🌟🌟🌟🌟
Using Redis Stream with Python
# 追加消息
xadd key_name * field_name 'value'
# 从第一条开始消费
xgroup create key_name consumer_group 0-0
xreadgroup group consumer_group consumer1 COUNT 1 STREAMS key_name 0-0
# 从上条被消费的消息可以理解成从 last-delivered-id 开始消费
xreadgroup group consumer_group consumer_1 COUNT 1 STREAMS key_name >
# ack
xack cloud_resource consumer_group 1553585533795-0
# 查看消息列表
xrange key_name - +
# 修剪 stream 长度
xtrim cloud_resource MAXLEN 10
# 另一种是直接在 xadd 中定义即可
xadd key_name maxlen ~ 1 * field_name 'value'
# 其他操作可以参考文章中介绍
注:
关于内存占用:
- xdel 只是逻辑删除
- 消息消费后一定要 xack,否则 xtrim 也无法释放掉 pending 的那些消息,所以想要保证内存不会占用过大就需要既使用 xtrim 也使用 xack
- 对于消费了但是没有 ack 的消息会出现在 pending 列表中,使用
xreadgroup group consumer_group consumer1 COUNT 1 STREAMS key_name 0-0
能读到,但没在业务中使用,因为可能造成重复消费。
- 对于消费了但是没有 ack 的消息会出现在 pending 列表中,使用
关于重复消费:
- last_delivered_id 保证了一个消息只被消费一次,不会出现同一个消费者组重复消费的现象。(在业务使用中因为 consumer_group 只有一个所以没有做消费幂等)
- 不同消费组会重复消费
redis 集群方式
主从模式
三种集群方式中最简单的,核心是主从复制:通常设置一个主节点,N 个从节点;默认情况下主节点负责处理使用者的 IO 操作,从节点会对主节点的数据进行备份,并且也会对外提供读操作。
注:主从模式下必须保证主节点不宕机,否则其他节点不会竞争。
哨兵模式
在主从模式上做了一些变化:在主节点宕机不可写的情况下,能从所有的从节点中竞选出新的主节点,够为 redis 提供高可用性,这个过程也被称为「主备切换」。
依赖:sentinel 进程
当竞选出新的主节点后,被选为新的主节点的从节点的配置信息会被 sentinel 改写为旧的主节点的配置信息。完成改写后,再将新主节点的配置广播给所有的从节点。
tips:
- sentinel 是为了实现高可用,其本身作为一个组件也一定是高可用的即非单点部署
集群模式
即分布式存储,每台 redis 存储不同的内容,主要解决了单机 redis 容量有限的问题,将数据按一定的规则分配到多台机器。
注:主从模式和哨兵模式其实都是每台 Redis 服务器存储相同的数据。
haproxy
HaProxy 是一个开源的负载平衡器,可以路由请求到多个后端服务器,以实现高可用性和可扩展性。
在使用 Redis-HaProxy 组合时,HaProxy 可以被配置为将请求路由到多个 Redis 服务器,从而实现对 Redis 集群的负载平衡。此外,HaProxy 还可以提供对 Redis 集群的健康检查,以确保高可用性。
shell
# 查看 redis instance 角色信息
role