Redis缓存学习笔记(一)

Redis 学习笔记(一)


简述.

  • Redis (Remote Dictionary Server) ,是一个基于内存的高性能Key-Value数据库;支持多种丰富的数据类型,而且由于纯内存访问和优秀的单线程架构,所以具备很高的响应性。支持主从模式,在单机多机环境下分别支持哨兵集群搭建高可用环境。

Redis 优点:

  • 访问速度快: Redis 将数据保存在内存中,存取速度比较可观;并且可以定期通过异步操作将数据库数据持久化到本地磁盘,作为灾难恢复和数据转移的准备。

  • 支持丰富的数据结构: Redis 大受欢迎的原因还在于它提供了丰富的数据结构:string,hash,list,set,zset,Pub/Sub,Geo等,可以很大程度上直接满足业务场景的需求。

  • 丰富的特性:

    • 支持简单的发布订阅

    • Key过期策略:在缓存一致性问题中可以提供帮助

    • 事务特性

    • 支持多DB:16个,从0开始

    • 原子计数

    • Session 共享

    • 分布式锁

  • 持久化存储: RDB,AOF

Redsi缺点

  • 内存存储所以受到机器本身的内存限制,需要制定数据的定期清除策略等
  • RDB会占用主机CPU,导致Redis 卡顿等。
  • 修改配置文件,重启服务,预热大量数据时间比较久,期间Redis 不能提供服务

Redis 数据结构:

  • 概述: Redis 提供了 string(字符串),hash,list(列表),set(集合),zset(顺序集合)这些基本数据类型,以及在此基础上构建的复杂类型。
  • string :
    • 字符串是 Redis 最基础的数据结构,在Redis 中,key 都是字符串类型,其他几种类型都是在字符串的基础上构建的。虽然是字符串类型,但实际的值可以是多种形式的字符串:json,xml,数字,二进制等,最大值限制是512M.
    • set key value [过期时间] [毫秒数] [NX|XX] : 因为在单机环境下,redis 的分布式锁可以通过它实现。
      • set 命令带上过期时间参数和NX ,表示设置一个具有时限的独占锁。
      • 过期时间防止忘记释放锁导致后来者无法获取锁;NX表示key不存在才会创建,也就是只有一个锁。
      • 这条命令的简化版本是 setnx,setex,可以保证原子性的形况下才能一起使用实现分布式锁。
    • mget/mset : 批量操作命令
      • 设想一个执行n次get命令的场景:每一次get都需要发送一次请求,等待一次返回(单线程模型),效率不高
      • mget 命令只会发送一次请求,然后等待服务端一次执行n次get之后将结果全部返回,节省了网络IO时间
      • 需要注意批量操作的数据量限制,如果超过带宽或者其他资源瓶颈,可能会出现网络拥塞等问题。
    • incr/incrby/decr/decrby key [num] : 对于内容是数字类型的key进行增减和指定参数的增减
      • 当 value 值是数字类型的时候,可以使用这些命令进行增减
      • 非数字类型value会报错
      • 不存在的key会被创建并增加,这一点需要注意,它并不会因为key不存在而返回null
    • string类型内部编码:根据实际存储的值类型和长度使用不同的内部编码实现
      • int : 8字节的长整型
      • embstr : 小于等于39个字节的字符串
      • raw : 大于 39 字节的字符串
  • hash :
    • 和其他数据结构可能有点不一样,Redis 本身是key-value存储,hash结构的键又是一个key-value类型,类似于:user:1:name -tom,适合用来表示对象类型。哈希的映射关系叫做 field-value,value 不是指键对应得值,而是field对应的值。
    • hget/hset/hdel/ key field value : hset user:name tom
      • 基本的命令,hdel 可以删除多个dield : hdel key [f1,f2,f3…]
    • hmget/hmset/hkeys/hvals : 批量设置/获取/获取键/获取值等命令
  • list :
    • 列表,有序存储多个字符串,是个双端列表,而且还提供了从左右两端阻塞获取元素的方法,其实就是可以作为消息队列使用。列表元素有序可重复。
    • rpush/lpush/linsert key value : 从左边/右边/插入位置添加元素
    • lrange/lindex/llen key [start] [end]/[index] : 获取指定范围的元素集/获取指定下表的元素/集合长度
    • lpop/rpop/lrem/ltrim key /[start] [end] : 左右端弹出元素/裁剪列表(返回指定顺序或者排序的列表)
    • blpop/brpop key : 阻塞式地弹出元素
      • 实现生产-消费者的方式
    • 使用场景
      • 消息队列 : lpush + brpop 可以实现阻塞队列,但是并不推荐使用。
      • 有限集合: lpush + ltrim 显示最新入队的元素信息
      • 栈: lpush + lpop
  • set :
    • 不允许重复元素的数据集合,无序存储。支持取交集,并集,差集等功能,最多存储2^32 - 1 个元素
    • sadd/srem/scard/smembers key : 添加,删除,计算元素个数,显示集合元素
    • sinter/sunion/sdiff key1 key2 key3... : 在多个key中取交集,并集,差集
    • sinterstore/sunionstore/sdiffstore destinationKey key1 key2 key3 : 将交集,并集,差集的结果保存在 destinationKey中
  • zset:
    • 有序集合,通过给key添加一个 score 达到排序的目的
    • zadd/zcard/zscore/zrank/zrem/ key score menber score1 menber1 score2 menber2: 增加,计数,排序删除
    • zrange/zrevrange/zrangebyscore/zrevrangebyscore key start end : 指定score范围的元素
    • zcount key min max : 返回指定分数范围的元素
    • zremrangebyrank key start end : 删除指定排名内的升序元素
      • zremrangebyrank user:ranking 0 2

持久化:

  • 概述:Redis提供两种持久化方式: RDB(全量复制),AOF(增量复制)。

  • RDB

    • RDB 持久化是把当前进程数据生成快照保存到硬盘的过程,触发RDB持久化的过程有手动和自动
    • 自动触发
      • save m n : 配置项,表示m秒内数据集存在n次修改时,自动触发bgsave
      • 从节点全量复制,主节点会自动执行bgsave生成RDB文件
      • debug reload 操作命令重新加载Redis ,也会触发 save 操作
      • 默认情况下执行 shutdown 命令,如果没有开启AOF持久化会自动执行bgsave
    • 手动触发:save 和 bgsave 命令
      • save : 阻塞当前Redis 服务器直到RBD持久化完成,很明显,不建议线上使用。
      • bgsave :
        • 执行 bgsave 命令,查看是否有其他子进程在执行bgsave,存在则直接返回。
        • 父进程执行 fork 操作创建子进程,fork 操作过程中父进程阻塞,info stats 命令的 latest_fork_usec 可以查看最近一次的fork操作耗时,单位为微秒。
        • 父进程 fork 完成,bgsave 返回 “Background saving started” ,并不再阻塞父进程
        • 子进程创建RDB文件,根据父进程内存生成临时快照文件,完成后对原有文件进行原子替换。执行lastsave命令可以获取最后一次生成RDB的时间,对应info_stats 的 rdb_last_save_time
        • 子进程告知父进程bgsave完成,父进程更新统计信息
    • RDB文件管理
      • dbflename : 指定rdb文件的存储位置
      • config set dir {newDir} ,config set dbfilename {newFilename} 动态设置RDB文件和存储位置,之后的RDB会保存到新的位置;动态设置适用于磁盘写满和坏盘的情况
    • 优点
      • RDB是一个紧凑的二进制文件,适合做备份和全量复制(数据转移,数据恢复)。
    • 缺点
      • 无法进行实时持久化和秒级持久化。bgsave每次都需要fork主进程,执行成本高。
  • AOF:Append only file

    • 概述:以独立日志的方式记录每次写命令,重启时再执行AOF文件的命令恢复数据;解决了RDB不能实时持久化和秒级持久化的问题

    • 配置

      • appendfileonly : yes|no : 这个配置项表示开启或关闭AOF持久化
      • appendfilename : AOF文件名称配置
    • AOF过程

      • 将所有的写入命令追加到 aof_buf 中

        Redis 使用单线程响应命令,如果每次写AOF文件都直接追加到硬盘,性能取决于当前硬盘负载。先写入缓冲区,Redis可以提供多种缓冲区同步硬盘的策略(always,everysec,no)。

      • AOF缓冲区根据对应的策略向硬盘做同步操作

      • 随着AOF文件越来越大,定期对文件进行重写,压缩文件

      • Redis 服务重启,加载AOF文件进行数据恢复

    • 缓冲区同步硬盘策略:

      • always : 每次写入命令都要同步AOF文件,不建议配置
      • everysec : 每秒写入一次命令到AOF文件,理论上系统宕机可能会丢失1秒的数据
      • no : 不添加额外控制,由操作系统同步,但是周期不可控,性能提高,没有数据安全性保证
    • 触发方式

      • 手动触发: 直接调用 bgwriteaof

      • 自动触发:

        • 根据auto-aof-rewrite-min-size 和 auto-aof-rewrite-percentage 参数确定自动触发机制

          • auto-aof-rewrite-min-size : 运行AOF重写时文件最小体积,默认64M
          • auto-aof-rewrite-percentage : 代表当前AOF文件空间(aof-current-size) 和 上一次重写后AOF文件空间(aof_base_size)的比值

          触发时机是:aof_current_size > auto_aof_rewrite_min_size && (aof_current_size-aof_base_size)

          /aof_base_size>=auto_aof_rewrite_percentage


复制

  • 概述 : 分布式系统为了解决单点问题,通常会把数据复制多个副本部署到其他机器,满足故障恢复和负载均衡需求。Redis 提供了复制功能,实现了相同数据的多个Redis副本。
  • 复制功能使用
    • 配置:
      • 建立复制 : 有三种方式,不过写入配置文件中
        • slaveof [masterHost] [masterPort] : 在从节点服务启动配置文件加入这项配置,指定主节点的IP和端口
        • redis-server --slaveof [masterHost] [masterPort] : 从节点server命令带有 --slave 命令启动
        • 运行期间执行 slaveof [masterHost] [masterPort]
        • 可以看出,使用方式的灵活性,常驻配置可以使用配置文件;测试可以使用命令行或者动态配置
      • 断开复制
        • 从节点执行slaveof no one,从节点会晋升为主节点
      • 从节点只读
        • 从节点配置 slave-read-only = yes,表示只读模式,也是推荐的使用方式
      • 传输延迟
        • repl_disable_tcp_nodelay : 关闭时主节点的所有命令都会及时发送给从节点,主从延迟较小;打开时,主节点会合并比较小的TCP数据包从而节省带宽。
  • 拓扑
    • 概述:主从模式之间可以有不同的结构,Redis主从复制支持单层或多层复制关系,主要分为三种:一主一从,一主多从,树状主从。
    • 一主一从
      • 最简单的主从结构,主节点故障时从节点提供故障转移支持。
      • 当主节点写命令并发较高且需要持久化时,可以只在从节点上开启AOF,这样既保证了数据安全性同时也避免了持久化对主节点的性能干扰。
      • 当主节点关闭持久化的时候,如果主节点宕机,需要避免重启,因为重启后的主节点数据集为空,从节点同步会清空数据,这种情况下应该先断开从节点的主从依赖,然后重启主节点
    • 一主多从 : 星形拓扑结构
      • 一个主节点具有多个从节点,实现读写分离。对于读占比较大的场景,可以把读命令发送到从节点来分担主节点压力,同时可以执行一些耗时的命令,例如查找所有key,sort 等慢查询。
      • 但是对于并发写比较高的场景,一主多从会导致主节点需要多次发送写命令到从节点,消耗更多的网络带宽,同时也增加了主节点的负载影响服务器的稳定性。
    • 树状主从
      • 从节点也具备从节点,这样的话主节点只需要将自己的写命令发送到自己的直接从节点,然后再由从节点复制到自身的从节点。挂载多个节点时,避免了对主节点性能的干扰,降低主节点压力。

哨兵(Sentinel):

  • 概述:Redis 主从模式下,一旦主节点故障不能提供服务,需要将从节点升级为主节点,同时更新主节点地址。这个过程如果手动完成的话,肯定是比较繁琐的,而且故障发生是不确定的。Redis 从2.8版本开始提供了哨兵(Sentinel) 来解决这个问题。
  • 哨兵节点:
    • 哨兵节点也是一个redis-server,只是做了一些特殊配置 ,它的功能不是专注于读写数据,而是监控主节点状态;当确认主节点下线(主观下线或者客观下线),会将从节点升级为主节点,其他的从节点从新的主节点进行复制;主节点降级为从节点,重连后从新的主节点复制
    • 它实现了故障发现和故障转移,并通知应用方,从而实现真正的高可用
  • 工作方式
    • 每个Sentinel节点以每秒钟一次的频率向它所知的Master(主节点),Slave(从节点)以及其他Sentinel节点实例发送一个PING命令,判断它们的上下线状态
    • 如果一个实例距离最后一次有效回复PING命令的时间超过 down-after-milliseconds 指定的值,这个实例就会被Sentinel节点标记为主观下线
    • 如果一个Master被标记为主观下线,则正在监视这个Master的所有Sentinel节点要以每秒一次的频率确认Master的确进入了主观下线状态
    • 当有足够数量的Sentinel节点在指定的时间范围内确认Master的确进入了主观下线状态,Master节点会被标记为客观下线
    • 一般情况下,每个Sentinel节点是以每10 s 一次的频率向它所有已知的 Master,Slave 节点发送INFO命令
    • 当Master被Sentinel节点标记为客观下线时,Sentinel节点向下线的Master的所有Slave发送INFO信息的频率会从10s一次改为1s一次
    • 如果没有足够数量的Sentinel节点同意Master节点已经下线,Master的客观下线状态就会被移除;同样,如果Master重新向 Sentinel 的 PING 命令返回有效回复,Master的主观下线状态就会被移除

Redis 数据淘汰策略

  • 概述:之前说到Redis 是基于内存存储所以访问速度快,同时也正是因为 内存存储,可存储的数据量受到单机内存大小的限制。因此通常会过期Redis 内的数据,以节省内存空间。

  • Redis 数据过期策略有三种:

    • 被动删除
    • 主动删除
    • 超过maxmemory配置项大小限制,触发主动清理策略
  • 被动删除

    • Redis 中可以设置带有过期时间的key,但是,这只代表key在过期之后,无法再从Redis 获得,值仍然是存在内存中的。对于这种形式过期的key值,当下一次访问它,发现它过期的时候,会从内存中删除。
    • 好处很明显,因为是个被动的动作,没有主动去扫描内存中过期的key,不会有额外的CPU占用;
    • 坏处也同样很明显,假设存在大量的短时间使用了一次就过期的key值,之后没有再访问它,那么直到他们被淘汰之前,都会占据内存空间。
  • 主动删除

    • 被动删除是惰性策略,可能会导致过期key值仍然存在在内存中一段时间。主动删除是通过服务器定期对自身资源和状态的检查,并进行必要的清理,使自身达到一个相对稳定健康的状态,这被称为常规操作(cron job)

    • 实现方式: 由配置文件的hz 配置项控制,参考 hz 配置参数的说明:

      Redis 会调用内部方法执行一系列的后台任务,包括关闭超时的客户端连接,清洗不再请求的过期key等。

      并不是所有的任务执行频率都一样,但是Redis 通过 hz 参数设置来控制他们的执行。

      hz 参数默认是10,表示10s运行一次。当Redis空闲的时候会使用大量的CPU去清理无效的键值,但是如果恰好存在大量的过期key被清除,会提高Redis 的响应度。

      hz 参数的取值范围在1~500,但是不要超过100。使用建议是,大部分情况下默认10即可,在低延迟的环境中可以设置到100.

    • 主动删除过期key,只是Redis后台任务的一部分,设置合理的hz参数,能够得到比较好的内存清理效果。

  • 超过 maxmemory 限制

    • 当前已使用内存超过 maxmemory限制时,会触发主动清理策略,有以下几种:
      • volatile-lru : 值对设置了过期时间的key进行LRU
      • allkeys-lru : 删除lru算法的key
      • volatile-random : 随即删除即将过期的key
      • allkeys-random : 随即删除key
      • volatile-ttl : 删除即将过期的key
      • noeviction: 永不过期,这时候对于所有的读写请求都会返回错误,并且会触发 freeMemoryIfNeeded(void) 函数清理超出的内存。这个过程是阻塞的。
    • 小结
      • 尽量不要超过 maxmemory的限制,可以适当提高hz参数的值,增加淘汰无效键值的频率。
      • 不建议将所有的key都设置为不过期,一定要根据实际的业务场景考虑:常驻的菜单项可以不过期,热门商品则实要定期更新的,需要设置过期时间。

缓存一致性

  • 概述:因为计算机不同存储介质存在读取速度的差异,所以需要通过某种方式弱化这种情况的影响。缓存的目的是把读写速度慢的介质的数据,保存到读写速度快的介质中,从而提高这一部分数据的读写速度。

  • 缓存常见问题: 其实就是要保证各种突发情况下,缓存仍然有效且能够提供正确的数据保障。

    • 缓存何时写入以及如何避免重复写入
    • 缓存如何失效
    • 缓存和DB的一致性如何保证
    • 缓存穿透
    • 缓存击穿
    • 缓存雪崩
  • 缓存穿透 : 1.key值不存在;2.同时发生大量并发请求

    • 从缓存写入的时机来考虑:先查询缓存,缓存不存在数据,查询数据库,然后写入缓存,返回数据。缓存穿透是指查询一个不存在的key,这样每次缓存都不会命中,每次都会去查询数据库,缓存也就没有任何意义,等于不存在。在大流量(高峰期和恶意攻击)的情况下,DB没有那么好的性能,可能就会挂掉。
    • 解决方式:
      • 因为查询不存在key,所以返回了空结果,需要去查询数据库。那么,如果DB中的数据也为空,可以直接对这个key缓存一个空对象,这样每次请求缓存都会获得空对象,不会再查询数据库。
      • 给缓存层额外增加一个BloomFilter过滤器//TODO
    • 可行性:
      • 缓存空对象 :这种方式的问题在于,如果存在大量key为空,仍然会占据内存,需要一定的缓存空间。适用于缓存命中不高的场景,代码实现也比较简单
      • BloomFilter 过滤器://TODO
  • 缓存击穿: 1.key存在,过期了 ; 2.大量的并发请求

    • 概述:和缓存穿透不一样的是,这个key是真实存在的,只是在某个时间点恰好不可用了,这个时候的大量请求缓存都无法命中,所有都会去查询DB,DB可能因此而挂掉。

    • 解决方式:

      • 互斥锁锁: 当缓存不存在的时候,在去查询DB之前要求先获得一个互斥锁,保证只有一个线程去查询DB,并且更新到缓存,步骤如下:

        • 获取分布式锁,直到请求成功

        • 再去查询缓存(注意,不是拿到锁就直接查询DB),如果值存在,直接返回;值不存在,再去查询DB,然后更新到缓存

        • 更新缓存

      • 手动过期:缓存不设置过期时间,将过期时间保存在key对应的value里面,如果定时扫描发现key将要过期,通过一个后台线程异步进行缓存更新,保证只有一个线程去更新缓存

    • 可行性:

      • 互斥锁:可以保证数据一致性,但是代码复杂,而且不正确的实现会导致死锁。

        单机Redis 分布式锁实现,可以通过 set(key,value,NX,EX,expireTime)的原子指令完成,NX表示key不存在进行新增,EX表示key会在expireTime之后过期。

      • 手动过期:异步线程所有用户无感,但是由于扫描任务不会十分精确,会有数据不一致问题。

  • 缓存雪崩:缓存不可用

    • 概述:缓存雪崩是指某种情况下缓存服务挂掉(进程被杀死,服务器停电),那么所有的请求都会去DB查询数据,大流量可能会压垮DB。
    • 解决方案:
      • 搭建缓存高可用,Redis 有 RedisSentinel 和 集群两种方式可以选择。
      • 本地缓存:等于拥有两层缓存,Redis挂掉还可以将缓存写入本地,等待Redis服务恢复重新提供服务。
      • //TODO
  • 缓存和DB的数据一致性保证

    • 概述:考虑这样的场景:当缓存被A线程过期,B线程发现缓存为空,查询DB更新到缓存,然后A线程更新DB再次更新缓存,B线程查询的就是旧数据,在并发的场景下这种情况有可能会发生的。或者更新DB和更新缓存的操作不是原子的,一个成功一个失败,也会导致错误数据的残留。
    • 缓存更新的方式
      • 先把数据存到数据库,成功后,失效缓存
        • 从并发的场景来考虑:当A线程查询数据,B线程更新数据,在B线程更新数据库之后,失效缓存之前,如果A线程从缓存中取到了值,那就会获取到一个旧值。而随后的线程都可以获取新的值
        • 但是,如果DB和缓存的操作不是原子的,更新数据库成功,失效缓存失败,会导致缓存中一致存在旧数据。
      • 先淘汰缓存,再更新数据库
        • 这个好处是,数据的最终一致性可以得到保障,只要先淘汰了缓存,之后至少得有一次成功的写数据库更新缓存,所以最终数据是一致的。
        • 但是DB和缓存之间还是存在数据不一致,例如A线程淘汰缓存之后,更新数据库之前,B线程查询缓存不命中,查询DB然后更新到缓存,随后A线程更新数据库,那缓存和DB还是数据不一致,导致不一致的原因就在于并发
        • 解决并发带来的出行顺序不一致问题,通常都会想到加锁,让并行执行变成串行执行,但是需要注意获取锁时机
          • 写请求时,先淘汰缓存之前,获取锁,这样就不存在其他线程查询缓存的情况
          • 读请求时,缓存不命中,先获取锁,然后去查询DB
    • 建议
      • 可以对缓采用双淘汰机制:更新数据库之前淘汰缓存,然后设置一定的短时间超时限制。这样首先保证缓存会被更新一次,假设存入了并发产生的脏数据,也会在段时间后过期,重新获取DB的新数据。(前提是业务场景能够接受短时间的数据不一致。)