¶Redis 学习笔记(一)
¶简述.
- Redis (Remote Dictionary Server) ,是一个基于内存的高性能Key-Value数据库;支持多种丰富的数据类型,而且由于纯内存访问和优秀的单线程架构,所以具备很高的响应性。支持主从模式,在单机多机环境下分别支持哨兵和集群搭建高可用环境。
¶Redis 优点:
访问速度快: Redis 将数据保存在内存中,存取速度比较可观;并且可以定期通过异步操作将数据库数据持久化到本地磁盘,作为灾难恢复和数据转移的准备。
支持丰富的数据结构: Redis 大受欢迎的原因还在于它提供了丰富的数据结构:string,hash,list,set,zset,Pub/Sub,Geo等,可以很大程度上直接满足业务场景的需求。
丰富的特性:
持久化存储: 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重写时文件最小体积,默认64Mauto-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进行LRUallkeys-lru
: 删除lru算法的keyvolatile-random
: 随即删除即将过期的keyallkeys-random
: 随即删除keyvolatile-ttl
: 删除即将过期的keynoeviction
: 永不过期,这时候对于所有的读写请求都会返回错误,并且会触发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的新数据。(前提是业务场景能够接受短时间的数据不一致。)