API的理解和使用
通用命令
keys
dbsize #计算key的总数
exists key #检查key是否存在
del key [key] #删除指定key-value
type key #返回key的类型
expire key seconds #key在seconds过期
ttl key #查看key剩余的过期时间
persist key #去掉key的过期时间
复制代码
-2表示过期
-1代表key存在,并且没有过期时间
kyes基本不在生产环境使用
keys * #遍历所有key
key [pattern]
复制代码
命令 | 时间复杂度 |
keys | O(n) |
dbsize | O(1) |
del | O(1) |
exists | O(1) |
expire | O(1) |
type | O(1) |
数据结构和内部编码
redis为什么这么快?
- 纯内存
- 非阻塞IO
- 避免线程切换和竞态消耗
单线程需要注意什么?
- 一次只运行一条命令
- 拒绝长(慢)命令
keys, flushall, flushdb, slow lua script, mutil/exec, operate big value(collention)
- 其实不是单线程
fysnc file descriptor
close file descriptor
复制代码
字符串
get key #获取key对应的value O(1)
set key value #设置key-value O(1)
del key #删除key-value O(1)
mset key value key value #批量设置key-value O(n)
mget key key #批量获取key-value O(n)
incr key #key自增1,如果key不存在,自增后get(key)=1 O(1)
decr key #key自减1,如果key不存在,自减后get(key)=-1 O(1)
incrby key k #key自增k,如果key不存在,自增后get(key)=k O(1)
decrby key k #key自减k,如果key不存在,自减后get(key)=-k O(1)
set key value #不管key是否存在,都设置 O(1)
setnx key value #key不存在,才设置 O(1)
set key value xx #key存在,才设置 O(1)
getset key newvalue #set key newvalue并返回旧的value
append key value #将value追加到旧的value
strlen key #返回字符串的长度(注意中文)
incrbyfloat key 3.5 #增加key对应的值3.5
getrange key start end #获取字符串指定下标的所有的值
setrange key index value #设置下标所有对应的值
复制代码
记录网站每个用户个人主页的访问量
#redis实现
incr userid:pagevies(单线程,无竞争)
hincrby user:1:info pageview count
#java模拟代码
public VideoInfo get(long id){
String redisKey = redisPrefix + id;
Map<String,String> hashMap = redis.hgetAll(redisKey);
VideoInfo videoInfo = transferMapToVideo(hashMap);
if(videoInfo == null){
videoInfo = mysql.get(id);
if(videoInfo != null){
redis.hmset(redisKey, transferMapToVideo(videoInfo))
}
}
return videoInfo;
}
复制代码
hash
哈希键值结构
hmset key field value field value #批量设置 O(n)
hmget key field field #批量获取 O(n)
hget key field #获取hash key对应的field的value O(1)
hset key field value #设置hash key对应field的value O(1)
hdel key field #删除hash key对应的field的value O(1)
hexists key field #判断hash key 是否有field O(1)
hlen key #获取hash key field的数量 O(1)
hgetall key #h返回hash key对应所有的field和value O(n)
hvals key #返回hash key对应所有field的value O(n)
hkeys key #返回hash key对应所有field O(n)
hsetnx key field value #设置hash key对应field的value(如field存在,则失败) O(1)
hincrby key field intCounter #hash key 对应的field的value自增intCounter O(1)
hincrbyfloat key field floatCounter #hincrby浮点数版 O(1)
复制代码
小心使用hgetall(redis单线程) 例子:如保存一个用户的信息的实现,下面说3种情形,当然还有更多种其他方式
- String v1
- String v2
- hash 比较
命令 | 优点 | 缺点 |
string v1 | 编程简单,可能节约内存 | 1. 序列号开销 2. 设置属性要操作整个数据 |
string v2 | 直观,可以部分更新 | 1. 内存占用较大 2. key较为分散 |
hash | 直观、节省空间、可以部分更新 | 1. 编程稍微复杂 2. ttl不好控制 |
list
特点:有序、可以重复、左右两边插入弹出
rpush key value value ...valueN #从列表右端插入值(1-N个)
lpush key value value ...valueN #从列表左端插入值(1-N个)
linsert key before|after value newValue #在list指定的前|后插入newValue
lpop key #从列表左侧弹出一个item
rpop key #从列表右侧弹出一个item
#根据count值,从列表中删除所有value相等的项
#count > 0,从左到右,删除最多count个value相等的项
#count < 0,从右到左,删除最多Math.abs(count)个value相等的项
#count = 0,删除所有value相等的项
lrem key count value
ltrim key start end #按照索引范围修剪列表 O(n)
lrange key start end #获取列表指定索引范围所有item O(n)
llen key #获取列表长度 O(1)
lset key index newValue #设置列表指定索引值为newValue O(n)
blpop key timeout #lpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远不阻塞 O(1)
brpop key timeout #rpop阻塞版本,timeout是阻塞超时时间,timeout=0为永远不阻塞 O(1)
##小建议-数据结构类比
lpush + lpop = stack
lpush + rpop = queue
lpush + ltrim = capped collection
lpush + brpop = message quere
复制代码
慢查询
- slowlog-max-len
- 先进先出队列
- 固定长度
- 保存在内存中
慢查询命令
slowlog get [n] #获取慢查询队列
slowlog len #获取慢查询队列长度
slowlog reset #清空慢查询队列
复制代码
- 慢查询阀值(单位:微妙)
- slowlog-log-slower-than=0 记录所有命令
- slowlog-log-slower=than<0 不记录任何命令 配置方式
1. 默认值
config get slowlog-max-len = 128
config get slowlog-log-slower-than = 1000
2. 修改配置文件重启
3. 动态配置
config set slowlog-max-len 1000
config set slowlog-log-slower-than 1000
复制代码
运维经验
- slowlog-max-len不要设置过大,默认10ms,通常设置1ms
- slowlog-log-slower-than不要设置过小,通常设置1000左右
- 理解命令生命周期
- 定期持久化慢查询
pipeline
批量网络命令通信模型
什么是流水线
流水线作用
命令 | N个命令操作 | 1次pipeline(n个命令) |
时间 | n次网络 + n次命令 | 1次网络 + n次命令 |
数据量 | 1条命令 | n条命令 |
- redis的命令时间是微秒级别
- pipeline每次条数要控制(网络原因)
从上图举例,redis命令的执行时间是很快的,但是由于数据需要通过网络传输,由于2个地区相隔很远,数据以光速度传播也需要时间,然而这个时间有可能比redis执行时间要长。
pipeline-Jedis实现
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
<type>jar</type>
</dependency>
#没用pipeline, 1W次hset需要50s
Jedis jedis = new Jedis("127.0.0.1", 6379);
for(int i=0;i<10000;i++){
jedis.hset("hashkey:"+i,"field"+i, "value"+i);
}
#使用pipeline
Jedis jedis = new Jedis("127.0.0.1", 6379);
for(int i=0;i<100;i++){
Pipeline pipeline = jedis.pipelined();
for(int j=i*100; j<(i+1)*100;j++){
pipeline.hset("hashkey:"+j,"field"+j, "value"+j);
}
pipeline.syncAndReturnAll();
}
复制代码
使用建议
- 注意每次pipeline携带的数据量
- pipeline每次只能作用在一个Redis节点上
- M操作和pipeline的区别
发布订阅
角色 发布者(publisher) 订阅者(subscriber) 频道(channel) 模型
publish channel message #发布消息
subscribe [channel] #一个或多个
unsubscribe [channel] #一个或多个
psubscribe [pattern...] #订阅模式
punsubscribe [pattern...] #退订指定的模式
pubsub channels #列出至少有一个订阅者的频道
pubsub numsub [channel...] #列出给定频道的订阅者数量
pubsub numpat #列出被订阅模式的数量
复制代码
位图
setbit key offset value #给位图指定索引设置值
getbit key offset #获取位图指定索引的值
bitcount key [start end] #获取位图指定范围(start到end,单位为字节,如果不指定就是获取全部)位值为1的个数
bitop key targetBit [start] [end] #计算位图指定范围第一个偏移量对应的值等于targetBit的位置
复制代码
独立用户统计
- 使用set和Bitmap两种方式
- 1亿用户,5千万独立
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部存储量 |
set | 32位(假设userid用的是整型,实际场景很多用长整型) | 50000000 | 32位*50000000=190.7348633MB |
Bitmap | 1位 | 100000000 | 1位*100000000=11.920929MB |
一天 | 一个月 | 一年 | ||
set | 200M | 6G | 72G | 大约值 |
Bitmap | 12.5M | 375M | 4.5G | 大约值 |
只有十万独立用户呢?
数据类型 | 每个userid占用空间 | 需要存储的用户量 | 全部存储量 |
set | 32位(假设userid用的是整型,实际场景很多用长整型) | 100000 | 32位*1000000=0.3814697MB |
Bitmap | 1位 | 100000 | 1位*100000000=0.0119209MB |
使用建议
- type = string,最大512MB
- 注意setbit的偏移量,可能有较大耗时
- 位图不是绝对好
HyperLogLog
- 极小空间完成独立数量统计
- 本质还是字符串
- pfcount 统计有一定错误率0.81%
- 无法取出单条数据
pfadd key element [element...] #向hyperloglog添加元素
pfcount key [key] #计算hyperloglog的独立总数
pfmerge destkey sourcekey [sourcekey] #合并多个hyperloglog
复制代码
geo地理信息定位
- 3.2版本以后才有geo
- geoKey的类型是zset,
type geoKey = zset
- 没有删除的API,可以使用
zrem key member
geo key longitude latitude member [longitude latitude member...] #增加地理位置信息
geopos key member [member...] #获取地理位置信息
geodist key member1 member2 [unit] #获取两个地理位置的距离,unit:m、km、mi、ft
复制代码
Redis持久化的取舍和选择
redis持久化RDB
触发机制
save 阻塞的 文件策略:如果存在老的RDB文件,替换 时间复杂度O(n)
bgsave
save与bgsave
命令 | save | bgsave |
IO类型 | 同步 | 异步 |
阻塞 | 是 | 是(阻塞发生再fork) |
复杂度 | O(n) | O(n) |
优点 | 不会消耗额外内存 | 不阻塞客户端命令 |
缺点 | 阻塞客户端命令 | 需要fork,消耗内存 |
# 配置redis.conf
save 900 1
save 300 10
save 60 10000
dbfilename dump.rdb
dir ./
stop-write-on-bgsave-error yes
rdbcompression yes
rdbchecksum yes
#最佳配置
dbfilename dump-${port}.rdb #指定对应哪个redis的备份
dir /bigdiskpath #指定具体文件目录
stop-write-on-bgsave-error yes
rdbcompression yes
复制代码
触发机制
- 全量复制
- debug reload
- shutdown
RDB总结
- RDB是redis内存到硬盘的快照,用于持久化
- save通常会阻塞redis
- bgsave不会阻塞redis,但是会fork新进程
- save自动配置满足任一就会被执行
- 有些触发机制不容忽视
AOF
RDB有什么问题 耗时、耗性能 不可控、丢失数据
AOF运行原理-创建
AOF运行原理-恢复
AOF的三种策略 always
everysec
no
命令 | always | everysec | no |
优点 | 不丢失数据 | 每秒一次fsync丢一秒数据 | 不用管 |
缺点 | IO开销较大,一般的sata盘只有几百TPS | 丢一秒数据 | 不可控 |
AOF重写
- 减少硬盘占用量
- 加速回复速度
AOF重写2种方式
- bgrewriteaof命令
- 自动
配置名 | 含义 |
auto-aof-rewrite-min-size | AOF文件重写需要的尺寸 |
auto-aof-rewrite-percentage | AOF文件增长率 |
统计名 | 含义 |
aof_current_size | AOF当前尺寸(单位:字节) |
aof_base_size | AOF上次启动和重写的尺寸(单位:字节) |
自动触发实际(同时满足)
- aof_current_size > auto-aof-rewrite-min-size
- aof_current_size - aof_base_size/aof_base_size > auto-aof-rewrite-percentage
#配置redis.conf
appendonly yes
appendfilename "appendonly-${port}.aof"
appendfsync everysec
dir /bigdiskpath
no-appendfsync-on-rewrite yes
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb
复制代码
AOF重写流程
AOF阻塞问题
大于2秒会造成主线程阻塞,无法进行后续客户端发来的命令 每秒刷盘的策略不止是只丢失1秒的数据,也有可能是几秒
如何定位
- Redis日志
- reids命令
info Persistence
(无法看到具体时间点) - linux top 命令观察IO使用率
RDB和AOF选择
命令 | RDB | AOF |
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢数据 | 根据策略决定 |
轻重 | 重 | 轻 |
最佳策略
- 小分片
- 缓存或存储
- 监控(硬盘、内存、负载、网络)
- 足够的内存
fork操作
- 同步操作(阻塞)
- 与内存量息息相关:内存越大,耗时越长(与机器类型无关)
- info:latest_fork_usec
改善fork
- 优先使用物理机或者高效支持fork操作的虚拟化技术
- 控制redis实例最大可用内存:maxmemory
- 合理配置Linux内存分配策略:vm.overommit_memory=1
- 降低fork频率:例如放宽AOF重写自动触发机制,不必要的全量复制
子进程开销与优化
- CPU:
- 开销:RDB和AOF文件生成,属于CPU密集型
- 优化:不做主reids CPU绑定,不和密集型CPU部署在一起
- 内存
- 开销:fork内存开销,Linux:copy-on-write
- 优化:Linux:echo never > /sys/kernel/mm/transparent_hugepage/enabled(关闭增加fork速度)
- 硬盘
- 开销:RDB和AOF文件写入,可以结合iostat,iotop分析
- 优化:
- 不和高硬盘负载服务部署再一起:存储服务,消息队列等。
- no-appendfsync-on-rewrite = yes
- 根据写入量决定磁盘类型:例如SSD
- 单机多实例持久化文件目录可以考虑分盘存储
redis复制的原理与优化
单机有什么问题? 机器故障 容量瓶颈 QPS瓶颈
简单总结
- 一个master可以有多个slave
- 一个slave只能有一个master
- 数据流向是单向的,master到slave
slaveof ip port
slave-read-only yes
slaveof on one
复制代码
全量复制
开销:
- bgsave时间
- RDB文件网络传输时间
- 从节点清空数据时间
- 从节点加载RDB的时间
- 可能的AOF重写时间
部分复制
Redis主从复制和集群配置
主从复制的常见问题 读写分离
- 读流量分摊到从节点,提高访问速度
- 可能遇到问题:复制数据延迟、读到过期数据、从节点故障
配置不一致
- 例如maxmemory不一致,丢失数据
- 例如数据结构优化参数(例如hash-max-ziplist-entries):内存不一致
规避全量复制
- 第一次全量复制
- 第一次不可避免,从节点必须全量
- 解决:小主节点(maxmemory)分数据量,访问低峰时刻
- 节点运行ID不匹配
- 主节点重启(运行ID改变)
- 解决:故障转移,例如哨兵或集群
- 复制积压缓冲区不足
- 网络中断,部分复制无法满足
- 解决:增大复制缓冲区配置rel_backlog_size,网络增强
规避复制风暴
- 单主节点复制风暴:
- 问题:主节点重启,多从节点复制
- 解决:更换复制拓扑 slave-1从master复制数据之后,接下来的slave都从slave-1复制数据,减轻master压力
- 单机器复制风暴
- 如图:机器宕机后,大量全量复制
- 主节点分散多机器
Redis Sentinel
主从复制-master宕掉故障处理
Redis Cluster
缓存设计与优化
Redis云平台CacheCloud
阿里云Redis开发规范
内存管理
Redis 数据结构与内存管理策略(上)Redis 数据结构与内存管理策略(下)原理、方法双管齐下,大神带你细解Redis内存管理和优化
开发运维常见坑
redis调整内核参数Redis安全redis 热点Key的发现与解决之道redis4.0之基于LFU的热点key发现机制