著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
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的过期时间
127.0.0.1:6379> set hello world OK 127.0.0.1:6379> expire hello 20(integer)1 127.0.0.1:6379> ttl hello(integer)17 127.0.0.1:6379> get hello"world" 127.0.0.1:6379> ttl hello(integer)12 127.0.0.1:6379> ttl hello(integer)-2 127.0.0.1:6379> get hello(nil) 127.0.0.1:6379>
-2表示过期
127.0.0.1:6379> set hello world OK 127.0.0.1:6379> expire hello 20(integer)1 127.0.0.1:6379> ttl hello(integer)18 127.0.0.1:6379> persist hello(integer)1 127.0.0.1:6379> ttl hello(integer)-1 127.0.0.1:6379> get hello"world" 127.0.0.1:6379>
-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 #设置下标所有对应的值
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)
记录网站每个用户个人主页的访问量
#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; }
小心使用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 #列出被订阅模式的数量
127.0.0.1:6379> publish sou:tv "hello world"(integer)1 127.0.0.1:6379> publish sou:tv "hello world333"(integer)1 127.0.0.1:6379>
127.0.0.1:6379> subscribe sou:tv Reading messages... (press Ctrl-c to quit1) "subscribe"2)"sou:tv" 3)(integer)11)"message"2)"sou:tv" 3) "hello world"1)"message"2)'"sou:tv" 3) "hello world333"
位图
127.0.0.1:6379> set hello big OK 127.0.0.1:6379> getbit hello 0(integer)0 127.0.0.1:6379> getbit hello 1(integer)1 127.0.0.1:6379> setbit hello 7 1(integer)0 127.0.0.1:6379> get hello“cig" 127.0.0.1:6379>
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
redis> pfadd 2017_03_06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4”(integer)1 redis> pfcount 2017_03_06:unique:ids(integer) 4 redis> pfadd 2017_03 06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-90'(integer) 1 redis> pfcount 2017_03 06:unique:ids (integer)5
redis> pfadd2016 03 06:unique:ids "uuid-1" "uuid-2" "uuid-3" "uuid-4"(integer) 1 redis> pfcount 2016 03_06:unique:ids(integer) 4 redis> pfadd 2016_03_05:unique:ids "uuid-4" "uuid-5" "uuid-6" "uuid-7"(integer)1 redis> pfcount 2016 03 05:unique:ids(integer)4 redis> pfmerge 2016_03_05_06:unique:ids 2016_03_05:unique:ids2016_03_06:unique:ids OK redis> pfcount 2016_03_05_06:unique:ids (integer) 7
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
127.0.0.1:6379> geoadd cities:locations 116.28 39.55 beijing(integer) 1 127.0.0.1:6379> geoadd cities:locations 117.12 39.08 tianjin114.29 38.02 shijiazhuang 118.01 39.38 tangshan 115.29 38.51 baoding (inteaer)4
127.0.0.1:6379> geopos cities:locations tianjin1) 1) "117.12000042200088501" 2) "39.0800000535766543"
127.0.0.1:6379> geodist cities:locations tianjin beijing km"89.2061"
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宕掉故障处理