Redis基础
三种启动方式
- 直接启动
- 动态参数启动
- 配置文件启动(推荐,在单机多实例场景下配置文件可以用端口区分)
daemonize // 是否是守护进程
port // 端口
logfile // 系统日志
dir // 工作目录
支持的数据类型
通用命令 | 功能 |
---|---|
keys * | 遍历所有的key,时间复杂度O(n) ,一般不使用 |
dbsize | 计算key的总数,时间复杂度O(1) |
exists KEY | 检查key是否存在,存在返回1,不存在返回0 |
del KEY | 删除指定的key-value |
expire KEY SECONDS | key在seconds秒后过期 |
ttl KEY | 查询key剩余的过期时间 |
persist KEY | 去掉key的过期时间 |
type KEY | 返回key对应value的类型 |
String**:字符串,不大于512MB
get/set/del
incr/decr/incrby/decrby
set/setnx/set xx
mget/msetHash:哈希,mapmap结构,
hget/hset/hdel
hexists/hlen
hmget/hmset
hgetall/hkeysList:有序,可以重复
rpush/lpush/lpop/rpop/llen
lrange/ltrim/lsetSet:无序集合,无重复
sadd/srem
scard/sismember/srandmember/smembers
sinter/sdiff/sunion
抽奖系统Zset:有序集合,无重复,
zadd/zrem/zscore/zcard
排行榜
Redis功能
慢查询
慢查询发生在执行命令阶段
客户端超时不一定是慢查询,但慢查询是客户端超时的可能原因
两个配置
slowlog-max-len // 慢查询队列长度
slowlog-log-slower-than // 慢查询阈值(us)
config set slowlog-max-len 128
config set slowlog-log-slower-than 100000
慢查询命令
slowlog get [n]:获取慢查询队列
slowlog len:获取慢查询队列长度
slowlog reset:清空慢查询队列
Pipeline
将多次的IO往返时间缩减为1次,前提是执行的命令没有因果相关性
Redis命令时间是us级别
由客户端提供支持
M操作(mset/mget)是原子的,pipeline操作非原子但是返回有序
发布订阅
发布者,订阅者,频道(publisher, subscriber, channel)
publish/subscribe/unsubcribe
和消息队列的区别
BitMap
type是string类型,最大512MB
setbit/getbit/bitcount
独立用户统计
setbit的偏移量太大时比较耗时
HyperLogLog
超小内存完成独立数量统计,本质还是字符串
pfadd key [element…]:向hyperloglog添加元素
pfcount key:计算hyperloglog的独立总数
pfmerge destkey [sourcekey…]:合并多个hyperloglog
局限
- 错误率:0.81%
- 不能取出单条数据
GEO
地理信息定位(存储经纬度,计算两地距离,范围计算等)
本质是zset
geoadd/geopos/geodist
Reids IO模型
Redis性能高效
Redis单机QPS的上限是10w+
完全基于内存,内存操作非常快速(100ns)
采用单线程,避免线程切换和竞态消耗
数据结构简单,Redis中的数据结构是专门设计的
使用非阻塞IO+IO多路复用模型
由C语言编写
一次只运行一条命令,拒绝长(慢)命令
多核机器部署
- 运行多个Redis实例
通信协议
Redis的瓶颈不在于网络流量,而是在内部逻辑处理上
RESP是Redis的序列化协议,实现简单,解析性能好
Redis持久化
Redis所有数据保存的内存中,持久化就是把数据的更新异步保存到磁盘中
RDB(Redis DataBase)
RDB是内存数据的快照
RDB三种触发方式
save:同步,替换旧的RDB文件,不会消耗额外内存,阻塞客户端命令
bgsave:异步,替换旧的RDB文件,不阻塞客户端命令,需要fork,消耗内存
自动生成:通过配置seconds和changes
RDB其它触发方式
- 全量复制
- shutdown
bgsave原理:fork和cow
Redis通过创建子进程进行RDB持久化,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,但写脏的页面数据会逐渐和子进程分离开来,子进程对内存数据(快照)进行遍历读取,然后写到磁盘中
RDB问题
数据量为n,时间复杂度
O(n)
,耗时fork的COW策略消耗内存
磁盘IO性能
不可控,丢失数据
AOF(Append Only File)
AOF三种策略
always:每条命令都fsync到硬盘
everysec:每秒把命令fsync到硬盘
no:由操作系统决定fsync
如果要求AOF不丢失数据,则每条写指令都要sync一次磁盘,这种条件下Redis的性能是非常低的,一般使用定时sync的方法,即每秒sync一次,保证性能的同时保证最多只丢失1秒的数据,在数据可靠性和性能之间进行折中
AOF重写
减少磁盘占用量,加快恢复速度
AOF是内存数据修改的命令记录,通过重放来恢复Redis当前的内存数据状态,需要定期重写
AOF重写两种实现
bgrewriteaof
开辟一个子进程遍历内存,然后转换为一系列的Redis命令,序列化到一个新的AOF文件中,序列化完毕后再把操作期间发生的增量AOF日志追加到新的AOF文件中重写配置
对比
RDB对Redis的数据进行周期性地持久化,AOF对每条命令以追加方式写入到日志,类似MySQL的binlog
RDB适合冷备,AOF适合热备,两种机制同时开启时,Redis默认使用AOF重新构建数据
RDB优点:对Redis性能影响很小,通过fork一个子进程去进行持久化,数据恢复速度比AOF快
RDB缺点:RDB是快照文件,数据完整性不如AOF,RDB在生成快照的时候可能会阻塞客户端
AOF优点:AOF每秒进行一次fsync操作,最多丢失1秒的数据,AOF以追加方式写日志,性能较高
AOF缺点:AOF文件比RDB大,使用AOF比使用RDB的QPS要低,因为每秒都要异步刷新一下日志
混合持久化
- 单独使用RDB会丢失较多数据,单独使用AOF恢复数据没RDB快,因此结合两者,先用RDB恢复,再用AOF做数据补全
Redis高可用和可扩展
主从复制
单机问题
机器故障,容量瓶颈,QPS瓶颈
作用
数据副本,扩展读性能
命令
slaveof
全量复制和部分复制
runId和偏移量的概念
启动slave时会发送psync命令给master ,如果这个slave是第一次连接到master,则会触发一个全量复制,master就启动一个线程,生成RDB快照,还会把新的写请求缓存在内存中,RDB文件生成后,master将这个RDB发送给slave,slave收到后写进本地磁盘,然后加载进内存,最后master会把缓存的新命令都发给slave
增量同步:内存环形buffer会出现覆盖的问题
快照同步:如果快照同步的时间过长或者buffer太小,可能会陷入死循环
无盘复制:主节点一边遍历内存,一般把序列化的内容发送到从节点
全量复制开销
bgsave时间
RDB文件网络传输时间
从节点清空数据时间
从节点加载RDB时间
可能的AOF重写时间
主从复制问题
- 手动故障转移
- 写能力和存储能力有限
补充
- Redis单机QPS的上限是100000+(每秒内查询次数),通过读写分离的手段,让master去写,同时数据同步到多个slave,让它们去读,这样就能分发大量的请求,同时可以容易进行水平扩容
Redis Sentinel
Redis Sentinel是Redis的高可用实现方案
Sentinel.jpg>)
Sentinel故障转移
- 多个sentinel发现并确认master有问题
- 选举出一个sentinel作为领导
- 选出一个slave作为master
- 通知其余slave成为新的master的slave
- 通知客户端主从变化
三个定时任务
每10s每个sentinel对master和slave执行info
发现slave节点,确认主从关系每2s每个sentinel通过master节点的channel交换信息
通过__sentinel__:hello
频道交互对节点的看法和自身信息每1s每个sentinel对其它snetinel和redis执行ping
心跳检测,失败判定依据
主观下线和客观下线
- 主观下线:某个sentinel认为Redis节点失败
- 客观下线:超过quorum个sentinel对Redis节点失败达成共识
领导者选举
只有一个sentinel完成故障转移
- 每个做主观下线的sentinel向其它sentinel发送命令,要求将自己设为领导者
- 收到命令的sentinel如果没有同意其它sentinel发送的命令,将同意请求,否则拒绝
- 该sentinel的票数如果超过quorum,将成为领导者,如果多个sentinel成为领导者将会重新选举
选择合适的slave
- slave-priority最高
- 复制偏移量最大
- runId最小
补充
Redis Sentinel集群一般由3~5个节点组成,可监控多个主从节点
客户端连接集群时,会先连接sentinel,通过sentinel来查询master的地址,然后再去连接master进行数据交互,当master发生故障时,客户端会重新向sentinel询问地址,sentinel会将最新的master地址告诉客户端
Redis Cluster
Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务
Redis Cluster着眼于可扩展,在单个Redis内存不足时,使用Cluster进行分片存储
解决单机瓶颈
主从同步读写分离,如果要支撑更大数据量和并发量,就需要集群部署
数据分区的两种方式
顺序分区:可顺序访问,支持范围操作,数据分布可能不均匀
哈希分区:不能顺序访问,不支持范围操作,数据分布均匀
节点取余:哈希 + 取余,扩容时迁移的数据量太大
一致性哈希:哈希 + 顺时针,扩容时数据迁移只影响相邻节点
虚拟槽:每个槽映射一个数据子集
Redis Cluster结构
通过原生命令安装部署理解架构
- 配置开启节点
- meet
- 指派槽
- 主从复制
Redis一致性问题
Consistent:一致性
Availability:可用性
Partition tolerance:分区容忍性
CAP原理:网络分区(网络断开)发生时,一致性和可用性难以两全
Redis满足了可用性和最终一致性
最终一致性表示网络分区发生时,为了满足可用性导致主从节点数据发生不一致,一旦网络恢复,从节点会努力追赶主节点,最终保持数据一致
如果严格要求缓存和数据库必须保持一致性,将读请求和写请求串行化到一个内存队列
串行化会导致系统的吞吐量大大降低,同时队列也可能成为系统的一个瓶颈
Redis应用场景
- 缓存
- 分布式锁
- 消息队列
- 计数器:HyperLogLog
- 排行榜:zset
- 社交网络
- 实时系统(布隆过滤器)
Redis缓存
降低后端负载,加速请求响应,大量写合并为批量写
数据不一致:缓存层和数据层存在时间窗口不一致,和更新策略有关
缓存更新策略
- 内存淘汰策略
- 过期策略
- 主动更新
内存淘汰策略
淘汰策略 | 详细 |
---|---|
noeviction | 返回错误,当达到内存限制条件并且客户端尝试执行写入命令 |
allkeys-lru | 尝试回收最近最少使用的键 |
volatile-lru | 尝试回收最近最少使用的键,但仅限于过期的键 |
allkeys-random | 回收随机的键 |
volatile-random | 回收随机的键,但仅限于过期的键 |
volatile-ttl | 回收过期的键,并且优先回收存活时间(TTL)较短的键 |
过期策略
定期删除:每隔一段时间就随机选择一些设置了有效时间的Key,检查是否过期,过期就删除
扫描所有设置了有效时间的Key,性能太低惰性删除:等到有查询的时候判断是否过期,过期就删除且不返回
缓存穿透,缓存击穿,缓存雪崩
缓存穿透:大量请求不命中,恶意攻击
在接口层增加校验,比如用户鉴权校验,参数校验等;
在存储层没有找到时,更新缓存对应Key的Value为NULL,失效时间可以设置短一点
Nginx网关层对IP的请求次数进行监控,超过阈值就拉黑处理
使用布隆过滤器快速判断Key是否存在缓存击穿:高并发的请求只针对某一个热点,在该热点失效的瞬间,请求就会击穿缓存,落到数据库上
设置逻辑过期时间:基本解决了热点重建问题,不保证一致性,增加维护和内存成本
使用互斥锁:保证了一致性,但是可能会出现死锁缓存雪崩:缓存同一时间大面积失效
在往Redis存数据时,在每个Key的失效时间加上一个随机值,保证不会同一时间大面积失效
设置热点数据永不过期,需要时再去更新缓存
分析思路
事前:Redis高可用,使用主从 + 哨兵或者Redis Cluster避免全盘崩溃
事中:避免MySQL被打死,本地ehcache缓存 + Hystrix限流 + 降级
事后:快速恢复缓存数据,Redis混合持久化,重启自动从磁盘上加载数据
分布式锁
事务
multi:事务开始
exec:事务执行
discard:事务丢弃,丢弃事务缓冲队列里所有的命令
Redis的事务不具有原子性,事务在遇到命令执行失败后,后面的命令还会继续执行
Redis的事务满足隔离性,因为是单线程模型,类似于串行化
两种处理并发冲突的方式
Redis分布式锁(悲观锁)
watch机制
在事务开始前watch某个变量,然后开始事务,在事务执行时Redis会先检查watch的变量是否被修改,如果被修改了exec命令就会返回null通知客户端事务执行失败
> watch books
OK
> incr books # 被修改了
(integer) 1
> multi
OK
> incr books
QUEUED
> exec # 事务执行失败
(nil)