redis 学习笔记(一)

本笔记主要涉及学习 redis 过程中做的一些笔记,包含最初比较浅显的了解(所以会有一些比较简单的笔记描述),到后面对其应用的逐渐学习和补齐。其中主要参考书籍为 《redis in action Josiah L. Carlson》其他参考博客也会注明。

redis

文章目录

  • redis
      • redis 为什么这么快?
    • 存储的数据类型
    • redis 关键技术
      • 事件循环
      • 逐出
      • 过期
      • 持久化
      • 主从复制(异步操作:写入成功即成功)
      • pipeline
      • mget
      • redis集群:一致性 hash & redis cluster
    • redis 客户端: 服务发现
      • 什么是服务发现
    • redis 客户端: failover
    • redis 延迟删除&双机房删除
    • redis 分布式锁
      • 乐观锁和悲观锁
    • redis 发布与订阅
      • 关于 publish 和 subscribe 命令
      • list LPUSH+BRPOP 或者 基于Sorted-Set的实现
      • redis stream

定义

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 为什么这么快?

  1. 完全基于内存,数据存在内存中,绝大部分请求是纯粹的内存操作,非常快速,跟传统的磁盘文件数据存储相比,避免了通过磁盘IO读取到内存这部分的开销。

  2. 数据结构简单,对数据操作也简单。Redis 中的数据结构是专门进行设计的,每种数据结构都有一种或多种数据结构来支持。Redis 正是依赖这些灵活的数据结构,来提升读取和写入的性能。

  3. 采用单线程,省去了很多上下文切换的时间以及 CPU 消耗,不存在竞争条件,不用去考虑各种锁的问题,不存在加锁释放锁操作,也不会出现死锁而导致的性能消耗。

  4. 使用基于 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 最近访问的服务、排行榜等

Redis 数据类型

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是一个内存服务,会设定内容上限的。

Redis 之 LRU 与 LFU

逐出 - 当执行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不在积压队列中,那么就会发起一次全量的同步
    image

pipeline

优点:

  • 节省往返时间;
  • 减少了proxy、redis server的IO次数

mget

  1. client: 使用mget命令
  2. redis: 一个命令中处理多个key;等所有key处理完后组装回复一起发送
  3. twemproxy:拆key分发到不同redis
  4. 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 延迟删除&双机房删除

  1. 为什么延迟删除
    解决 DB 和 cache 数据不一致的问题
  2. 产生原因
    请求回源时,DB 主从延迟导致用 DB 的从节点的老数据更新了 cache
  3. 解决方案
    cache 延迟多次删除,当前删除一次,过几秒(大于 DB 主从延迟)后再删除一次

redis 分布式锁

references:

Redis的分布式锁详解

如何部署一个生产级别的 Kubernetes 应用

Redis setnx 原子操作:

最近做业务用到了消息队列,一般 mq 会保证 at least once, 但随之而来的问题就是有可能出现重复消费的现象,业务方需要做消费幂等。此时可以利用 redis 来做消费幂等。

references:

  1. redis 并发问题(setnx 事例)
  2. 消息队列三:消息重复消费问题(幂等性)
  3. kafka-重复消费-2(此文档中唯一 id 使用到了 offset 是合理的,因为从原理上看每条存在 broker 的消息都有 offset,并且不会发生改变,因此 consumergroup+topic+partition+offset 可以用于标识一条消息)
  4. 海量订单产生的业务高峰期,如何避免消息的重复消费?

问题背景

  • 原子性:如果这个操作所处的层(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

应用场景

  1. 当我们使用 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(8)——发布/订阅与Stream

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'

# 其他操作可以参考文章中介绍

注:

关于内存占用

  1. xdel 只是逻辑删除
  2. 消息消费后一定要 xack,否则 xtrim 也无法释放掉 pending 的那些消息,所以想要保证内存不会占用过大就需要既使用 xtrim 也使用 xack
    1. 对于消费了但是没有 ack 的消息会出现在 pending 列表中,使用 xreadgroup group consumer_group consumer1 COUNT 1 STREAMS key_name 0-0 能读到,但没在业务中使用,因为可能造成重复消费。

关于重复消费

  1. last_delivered_id 保证了一个消息只被消费一次,不会出现同一个消费者组重复消费的现象。(在业务使用中因为 consumer_group 只有一个所以没有做消费幂等)
  2. 不同消费组会重复消费

你可能感兴趣的:(BE,redis)